一文看懂Nacos如何实现高效、动态的配置中心管理
一文看懂Nacos如何实现高效、动态的配置中心管理
在微服务架构中,配置管理是一个核心环节。传统的配置管理方式存在诸多局限性,而分布式配置中心则提供了一个更高效、灵活的解决方案。本文将详细介绍Nacos如何实现高效、动态的配置中心管理,包括其集成方法、一致性协议以及动态配置原理。
一、分布式配置中心
在微服务架构中,我们会使用一个中心化的分布式配置中心作为配置文件的管理者。在应用程序端,我们只将一些必要的配置项添加到配置文件中,而大部分的配置项都保存在配置中心集群里。客户端在启动的时候从配置中心获取所有的配置项,用于各个组件的初始化。
分布式配置中心具有如下特性:
- 高可用性:微服务组件的高可用性是首要目标。配置中心并不是一个中心化的单点应用,而是通过一个集群对外提供服务,在一致性算法的基础上,集群中各节点间相互同步数据。即便有节点挂掉,配置中心仍然能对外提供服务。
- 环境隔离:Nacos 支持通过 Namespace 属性指定环境,将各个环境进行隔离。
- 多格式支持:Nacos 支持多种文件格式,包括JSON、XML、YAML、Proterties、Text多种文件格式。
- 访问控制:Nacos 实现了权限管理,可以再控制台创建不同的账户,然后分配不同的权限(读写、只读),通过这种方式来保护敏感信息。
- 职责分离:配置项从代码中分离出来,修改配置无需修改代码打包部署,完全实现了代码与配置隔离。
- 版本控制与审计:配置项也是一种代码,而且配置 bug 往往比代码中的 bug 造成的影响更大。因此在微服务架构中需要确保配置中心具备完善的版本控制和审计功能。
通过 Nacos 的“历史版本”功能,可以查看任何一个配置文件的历史修改记录,包括改动的时间和操作人。针对每一个改动记录,我们可以查看这一版本的配置详情,或者做线上配置项的回滚操作。
二、Nacos config
2.1 集成 Nacos config
第一步,添加配置
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>${latest.version}</version>
</dependency>
第二步,在 bootstrap.properties
中配置 Nacos server 的地址和应用名
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.application.name=example
第三步,添加配置后即可在代码中引用配置了,如下代码,其中 @RefreshScope
实现配置自动更新。
@RestController
@RequestMapping("/config")
@RefreshScope
public class ConfigController {
@Value("${useLocalCache:false}")
private boolean useLocalCache;
@RequestMapping("/get")
public boolean get() {
return useLocalCache;
}
}
2.2 为什么集成 Nacos 必须用到 bootstrap 配置文件
在第二步中,将 Nacos 的地址信息等配置在了 bootstrap 中,那为什么要在这里配置呢?
在 Spring Boot 规范中,bootstrap 文件通常被用于应用上下文引导,bootstrap.yml 文件加载的优先级要高于 application.yml。而在 Nacos 配置中通常会添加数据库等配置,这些数据库配置用于初始化其他组件,所以必须保证在其他组件启动之前就获取到这些基础配置,随后才能初始化。
所以将 Nacos 连接信息放到 bootstrap 中,就能确保程序在启动阶段优先执行 Nacos 远程远程配置项的读取。
配置案例
spring:
# 必须把name属性从application.yml迁移过来,否则无法动态刷新
application:
name: example
cloud:
nacos:
config:
# nacos config服务器的地址
server-addr: localhost:8848
file-extension: yml
# prefix: 文件名前缀,默认是spring.application.name
# 如果没有指定命令空间,则默认命令空间为PUBLIC
namespace: dev
# 如果没有配置Group,则默认值为DEFAULT_GROUP
group: DEFAULT_GROUP
# 从Nacos读取配置项的超时时间
timeout: 5000
# 长轮询超时时间
config-long-poll-timeout: 10000
# 轮询的重试时间
config-retry-time: 2000
# 长轮询最大重试次数
max-retry: 3
# 开启监听和自动刷新
refresh-enabled: true
# Nacos的扩展配置项,数字越大优先级越高
extension-configs:
- dataId: redis-config.yml
group: EXT_GROUP
# 动态刷新
refresh: true
- dataId: rabbitmq-config.yml
group: EXT_GROUP
refresh: true
这里介绍一下超时和重试配置里提到的长轮询机制的工作原理。
当 Client 向 Nacos Config 服务端发起一个配置查询请求时,服务端并不会立即返回查询结果,而是会将这个请求 hold 一段时间。如果在这段时间内有配置项数据的变更,那么服务端会触发变更事件,客户端将会监听到该事件,并获取相关配置变更;如果这段时间内没有发生数据变更,那么在这段“hold 时间”结束后,服务端将释放请求。
采用长轮询机制可以降低多次请求带来的网络开销,并降低更新配置项的延迟。
如果要从多个配置文件中获取配置项,那么可以使用 extension-configs 配置多源读取策略。extension-configs 是一个 List 的结构,每个节点都有 dataId、group 和 refresh 三个属性,分别代表了读取的文件名、所属分组、是否支持动态刷新。
2.3 一致性协议
Nacos 是一个需要存储数据的组件,因此,为了实现这个目标,就需要在 Nacos 内部实现数据存储。单机下问题不大,但是集群模式下就需要考虑如何保障各个节点之间数据一致性以及数据同步,而要解决这个问题就要引入共识算法。
为什么 Nacos 选择了 Raft 和 Distro 共识算法呢?Nacos 在单个集群中同时运行 CP 协议和 AP 协议,这需要从应用场景来分析。
服务注册发现场景
在微服务体系下,服务的注册发现是十分重要的,服务感知对方服务当前可正常提供服务的实例信息,必须从注册中心获取,因此对于注册中心的可用性要求很高,需要在任何场景下,尽最大可能保证服务注册发现可哟对外提供服务。
因此为了满足注册中心的可用性,强一致性算法就不太合适了,因为强一致性算法能否对外提供服务是有要求的,需要集群中大多数节点可用,而最终一致性算法,更多保证服务的可用性,并且能保证在一定时间内各个节点达到最终一致性。
针对服务注册发现的场景,Nacos 采用了AP协议,用 Distro 协议来实现。
Distro 是 Nacos社区自研的一种 AP 分布式协议,是面向临时实例设计的一种分布式协议,其保证了在某些节点宕机后,整个临时实例处理系统依旧可以正常工作。设计思想:
- Nacos 每个节点是平等的,都可以处理写请求,同时把数据同步到其他节点
- 每个节点只负责部分数据,定时发布自己负责数据的校验值到其他节点来保持数据一致性
- 每个节点独立处理读请求,及时从本地发出响应
配置管理场景
配置数据,是直接在 Nacos 服务端进行创建并管理的,必须保证大部分节点都保存了此配置数据才能认为配置成功了,否则就会丢失配置变更,如果出现这种情况,问题就很严重,如果是发布重要配置出现了丢失变更的情况,多半会引起严重故障,因此对于配置数据管理,是必须要求集群中大部分的节点是强一致的,即 CP 协议,而这里只能使用强一致性共识算法,即 Raft 算法。
关于 Raft 共识算法的详细内容,请看往期文章:
2.4 动态配置原理
在需要动态更新的地方添加 @RefreshScope
注解即可实现配置的动态刷新,那它是如何实现的呢?
RefreshScope
注解的 Bean 可以再运行期刷新,在启动时所有带有 @RefreshScope
注解的Bean 会被正常加载到 Spring 容器中,并处于活跃状态。
如果配置没有发生变化,那这个 Bean 一直处于活跃状态,每次请求这些 Bean 时,Spring 都会直接返回现有的活跃实例,不会重新创建新的实例。
当配置发生变更时,可以通过调用 /actuator/refresh
端点来触发配置刷新,刷新会使所有带有 @RefreshScope
注解的 Bean 被标记为非活跃状态,下次当有请求获取这些 Bean 时,Spring 会检查是否是活跃状态,不是的话,则会创建一个新的实例来替换旧的实例。
RefreshScope
用到的是 spring bean 的 scope 模式能力。scope 模式在获取 bean 时和单例的 Bean 以及多例的 bean 不太一样。scope 里自定义了一个 scope 范围来隔离不同的 scope,在获取 bean 时会优先从实现了 Scope 接口的类中获取,简单来说,scope 模式的 bean 的获取方式通过scope接口实现类获取,是一种代理模式。
今天就介绍到这里,欢迎留言讨论。