nacos 配置中心实现原理
在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。
命名空间(Namespace)
用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如数据库配置、限流阈值、降级开关)隔离等。如果在没有指定 Namespace 的情况下,默认使用 public 命名空间。
配置组(Group)
Nacos 中的一组配置集,是配置的维度之一。通过一个有意义的字符串对配置集进行分组,从而区分 Data ID 相同的配置集。当您在 Nacos 上创建一个配置时,如果未填写配置分组的名称,则配置分组的名称默认采用 DEFAULT_GROUP 。配置分组的常见场景:不同的应用或组件使用了相同的配置项,如 database_url 配置和 MQ_Topic 配置。
配置ID(Data ID)
Nacos 中的某个配置集的 ID。配置集 ID 是划分配置的维度之一。Data ID 通常用于划分系统的配置集。一个系统或者应用可以包含多个配置集,每个配置集都可以被一个有意义的名称标识。Data ID 尽量保障全局唯一,这里一般在nacos配置中心配置就是指的文件名。
配置中心依赖包spring-cloud-starter-alibaba-nacos-config,通过SPI机制来完成配置的初始化。
启动初始化加载
其中自动装配配置类NacosConfigBootstrapConfiguration会在springboot启动时进行依次配置文件的加载。该配置类负责把 NacosConfigManager、NacosPropertySourceLocator、NacosConfigProperties 等 bean 注入到 Spring 容器,NacosConfigManager里根据nacos config的配置信息,创建维护了ConfigService,和配置中心的通讯通过ConfigService。
Spring Cloud Config 的核心机制是实现 PropertySourceLocator 接口,Nacos 也不例外。NacosPropertySourceLocator就实现了PropertySourceLocator 接口。springboot Environment初始化时会调用其locate()方法从nacos配置中心拉取配置文件内容,拉到的配置会封装成 PropertySource,加入到 Spring 的 Environment 里。所以应用一启动,配置就能直接注入到 @Value 或 @ConfigurationProperties 里。
NacosPropertySourceLocator#locate()
public PropertySource<?> locate(Environment env) {
nacosConfigProperties.setEnvironment(env);
//获取ConfigService
ConfigService configService = nacosConfigManager.getConfigService();
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
}
long timeout = nacosConfigProperties.getTimeout();
//configService放入nacosPropertySourceBuilder变量中,后面的获取文件都是通过这个Builder
nacosPropertySourceBuilder变量中,后面的 = new NacosPropertySourceBuilder(configService,
timeout);
String name = nacosConfigProperties.getName();
//获取spring.cloud.nacos.config.prefix指定的固定文件前缀
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
//未指定则使用应用名称
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
//加载spring.cloud.nacos.config.shared-configs 指定的配置文件
loadSharedConfiguration(composite);
//加载spring.cloud.nacos.config.extension-configs指定的配置文件
loadExtConfiguration(composite);
//加载和application名称,profile相关的默认配置文件
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}
获取配置文件都是通过loadNacosPropertySource()方法
private NacosPropertySource loadNacosPropertySource(final String dataId,
final String group, String fileExtension, boolean isRefreshable) {
if (NacosContextRefresher.getRefreshCount() != 0) {
if (!isRefreshable) {
return NacosPropertySourceRepository.getNacosPropertySource(dataId,
group);
}
}
/**
从配置中心拉取配置
dataId:资源Id,一般是文件名
group:所属分组
fileExtension:文件扩展名
*/
return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
isRefreshable);
}
上面说了nacosPropertySourceBuilder里面保存有configService,最后真正拉取配置的方法是loadNacosData()
NacosPropertySourceBuilder#loadNacosData()
private List<PropertySource<?>> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
try {
//从nacos中读取配置
data = configService.getConfig(dataId, group, timeout);
if (StringUtils.isEmpty(data)) {
log.warn(
"Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
dataId, group);
return Collections.emptyList();
}
if (log.isDebugEnabled()) {
log.debug(String.format(
"Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
group, data));
}
//转换成PropertySource
return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
fileExtension);
}
catch (NacosException e) {
log.error("get data from Nacos error,dataId:{} ", dataId, e);
}
catch (Exception e) {
log.error("parse data from Nacos error,dataId:{},data:{}", dataId, data, e);
}
return Collections.emptyList();
}
这里看到是通过调用 NacosConfigService.getConfig 去拉取 Nacos 里的配置。返回是一个字符串类型,然后使用NacosDataParserHandler转换成PropertySource。
自动刷新
应用可以通过配置spring.cloud.nacos.config.refresh-enable:true属性来控制应用配置自动刷新,也就是配置信息在配置中心修改后,应用程序可以自动刷新拉取最新的配置,在nacos config starter中另一个自动装配类NacosConfigAutoConfiguration中,会初始化一个NacosContextRefresher自动注册配置监听器。NacosContextRefresher实现了ApplicationListener<ApplicationReadyEvent> 接口,当容器初始化完成后会调用其onApplicationEvent(),通过其registerNacosListenersForApplications()方法来注册配置监听listener。
NacosContextRefresher#registerNacosListenersForApplications()
private void registerNacosListenersForApplications() {
if (isRefreshEnabled()) {//配置为自动刷新
for (NacosPropertySource propertySource : NacosPropertySourceRepository
.getAll()) {
if (!propertySource.isRefreshable()) {
continue;
}
String dataId = propertySource.getDataId();
//加载过的配置(dataId, group)订阅事件
registerNacosListener(propertySource.getGroup(), dataId);
}
}
}
事件订阅在registerNacosListener(),底层还是走的 NacosConfigService.addListener → ClientWorker → Listener.receiveConfigInfo。
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
refreshCountIncrement();
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
//发布RefreshEvent事件
applicationContext.publishEvent(
new RefreshEvent事件(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug(String.format(
"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
group, dataId, configInfo));
}
}
});
try {
//添加Listener
configService.addListener(dataKey, groupKey, listener);
log.info("[Nacos Config] Listening config: dataId={}, group={}", dataKey,
groupKey);
}
catch (NacosException e) {
log.warn(String.format(
"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
groupKey), e);
}
}
当 Nacos 配置发生变化时,它会触发 Spring Cloud 的 RefreshEvent。配合 @RefreshScope,Bean 会被销毁重建,从而达到动态刷新效果。
本地快照
nacos每次获取到配置后,ClientWorker 会调用工具类LocalConfigInfoProcessor把数据写入 ${user.home}/nacos/config/ 下的文件。获取配置时,先尝试远程,如果失败就读这个快照。本地快照类似于缓存,保证即使 Nacos Server 挂了,应用也能继续运行。
ClientWorker
ClientWorker是在NacosConfigService中的一个内部属性。ClientWorker 是 Nacos 客户端架构中一个至关重要的后台工作线程。所有客户端和服务端数据同步都是通过ClientWorker来完成。
其构造方法会初始化一个ConfigRpcTransportClient实例作为数据同步的rpc请求客户端,另外初始化一个ScheduledExecutor用来定期执行数据同步任务。
public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager,
final NacosClientProperties properties) throws NacosException {
this.configFilterChainManager = configFilterChainManager;
init(properties);
agent = new ConfigRpcTransportClient(properties, serverListManager);
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(initWorkerThreadCount(properties),
new NameThreadFactory("com.alibaba.nacos.client.Worker"));
agent.setExecutor(executorService);
agent.start();
}
ConfigRpcTransportClient在start()方法通过startInternal()来完成线程任务启动
public void startInternal() {
executor.schedule(() -> {
while (!executor.isShutdown() && !executor.isTerminated()) {
try {
listenExecutebell.poll(5L, TimeUnit.SECONDS);
if (executor.isShutdown() || executor.isTerminated()) {
continue;
}
executeConfigListen();
} catch (Throwable e) {
LOGGER.error("[rpc listen execute] [rpc listen] exception", e);
try {
Thread.sleep(50L);
} catch (InterruptedException interruptedException) {
//ignore
}
notifyListenConfig();
}
}
}, 0L, TimeUnit.MILLISECONDS);
}
这里schedule执行的任务是基于基于BlockingQueue 的事件驱动模型长轮询:
这里的listenExecutebell 是一个BlockingQueue(阻塞队列),它充当了事件信号的角色。
整个逻辑可以分解为以下几个步骤:
- 启动任务
executor.schedule(...)负责在后台启动一个任务。这个任务是一个while循环,它会持续运行,直到线程池被关闭。 - 阻塞等待信号
listenExecutebell.poll(5L, TimeUnit.SECONDS)是这里的关键。这个方法会让当前线程进入阻塞状态,它会做两件事:- 等待:线程会在这里“休眠”,等待
listenExecutebell队列中出现一个新元素。只要有新元素被放入队列,线程就会立即被唤醒。 - 超时唤醒:如果队列在 5 秒内都没有新元素,
poll()方法会返回null,线程也会被唤醒继续执行。
- 等待:线程会在这里“休眠”,等待
- 执行核心逻辑
executeConfigListen()方法是线程被唤醒后执行的真正逻辑。它负责:收集需要监听的配置:遍历所有已注册的监听器,收集dataId、group和tenant等信息。发送 gRPC 请求:将这些信息打包成一个 gRPC 请求,发送给 Nacos 服务器,并等待服务器响应。完成后listenExecutebell.offer(bellItem)往阻塞队列放入一个元素,这样下次while循环可以poll获取继续执行。
这种基于 BlockingQueue 的设计,比简单的 while (true) 循环更加高效和灵活,因为它实现了生产者-消费者模型:
- 生产者:当一个新的配置监听器被添加(例如,调用
addListener()方法)时,或者当 Nacos 服务器推送了配置变更的响应时,某个线程会将一个信号(如一个空对象)放入listenExecutebell队列中。 - 消费者:
startInternal()启动的线程就是消费者。它会一直等待队列中的信号。
这种模式带来了几个核心优势:
- 高效利用 CPU:线程在
poll()处等待时,不会占用 CPU 资源(不像while循环中带sleep()的方式)。它只在有新事件发生时才被唤醒,极大地节省了系统开销。 - 响应速度更快:一旦有新的配置需要监听,或者收到了来自服务器的推送通知,线程可以立即被唤醒,而无需等待上一个轮询周期结束。
- 优雅地处理并发:
BlockingQueue是线程安全的,它能够可靠地处理多个线程同时添加事件信号的情况。
更多推荐

所有评论(0)