大家好,这篇文章跟大家聊下 SpringCloudAlibaba 中的微服务组件 Nacos。Nacos 既能做注册中心,又能做配置中心,这篇文章主要来聊下做配置中心时 client 端的一些设计,主要从源码层面进行分析,相信看完这篇文章你对 Nacos client 端的工作原理应该有比较深刻的了解。

SpringCloud 应用启动拉去配置

我们之前写过一篇文章,介绍了一些 Spring 提供的扩展机制。其中说到了 ApplicationContextInitializer,该扩展是在上下文准备阶段(prepareContext),容器刷新之前做一些初始化工作,比如我们常用的配置中心 client 基本都是继承该初始化器,在容器刷新前将配置从远程拉到本地,然后封装成 PropertySource 放到 Environment 中供使用。

在 SpringCloud 场景下,SpringCloud 规范中提供了 PropertySourceBootstrapConfiguration 继承 ApplicationContextInitializer,另外还提供了个 PropertySourceLocator,二者配合完成配置中心的接入。

从上述截图可以看出,在 PropertySourceBootstrapConfiguration 这个单例对象初始化的时候会将 Spring 容器中所有的 PropertySourceLocator 实现注入进来。然后在 initialize 方法中循环所有的 PropertySourceLocator 进行配置的获取,从这儿可以看出 SpringCloud 应用是支持我们引入多个配置中心实现的,获取到配置后调用 insertPropertySources 方法将所有的 PropertySource(封装的一个个配置文件)添加到 Spring 的环境变量 environment 中。

上图展示了在 spring-cloud-starter-alibaba-nacos-config 包提供的自动装配类中进行了 NacosPropertySourceLocator 的定义,该类继承自上述说的 PropertySourceLocator,重写了 locate 方法进行配置的读取。

我们来分析下 NacosPropertySourceLocator,locate 方法只提取了主要流程代码,可以看到 Nacos 启动会加载以下三种配置文件,也就是我们在 bootstrap.yml 文件里配置的扩展配置 extension-configs、共享配置 shared-configs 以及应用自己的配置,加载到配置文件后会封装成 NacosPropertySource 返回。

    public PropertySource<?> locate(Environment env) {
// 生成 NacosConfigService 实例,后续配置操作都是围绕该类进行
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 = new NacosPropertySourceBuilder(configService,
timeout);
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
loadSharedConfiguration(composite);
loadExtConfiguration(composite);
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}

loadApplicationConfiguration 加载应用配置时,同时会加载以下三种配置,分别是

  1. 不带扩展名后缀,application

  2. 带扩展名后缀,application.yml

  3. 带环境,带扩展名后缀,application-prod.yml

并且从上到下,优先级依次增高

加载的核心方法是 loadNacosDataIfPresent -> loadNacosPropertySource

build 方法调用 loadNacosData 获取配置,然后封装成 NacosPropertySource,并且将该对象缓存到 NacosPropertySourceRepository 中,后续会用到。

loadNacosData 方法中会将实际配置加载请求委托给 configService 去做,然后解析返回的字符串,解析器实现了 PropertySourceLoader 接口,支持 yml、properties、xml、json 这几种。

getConfig 方法会调用到 getConfigInner 方法,通过 namespace, dataId, group 唯一定位一个配置文件

  1. 首先获取本地缓存文件的配置内容,如果有直接返回

  2. 如果步骤 1 从本地没找到相应配置文件,开始从远处拉去,Nacos 2.0 以上版本使用 Grpc 协议进行远程通信,1.0 及以下使用 Http 协议进行远程通信,我们这边以 1.x 为例来解读

getServerConfig 方法会构造最终的 http 请求参数进行调用,如果返回 ok,则将返回内容写入到本地缓存文件中,并进行返回。

至此,在项目启动的时候(上下文准备阶段)我们就拉到了远程 Nacos 中的配置,并且封装成 NacosPropertySource 放到了 Spring 的环境变量里。

监听器注册

上面章节我们说了服务启动的时候从远程 Nacos 服务端拉到配置,这个章节我们来说下配置变动怎么实时通知到客户端,首先需要注册监听器。

主要看 NacosContextRefresher 类,该类会监听服务启动发布的 ApplicationReadyEvent 事件,然后进行配置监听器的注册。

registerNacosListenersForApplications 方法里会进行判断,如果自动刷新机制是开启的,则进行监听器注册。上个章节我们说到了会将拉到的配置缓存到 NacosPropertySourceRepository 中, 这儿就从缓存中获取所有的配置,然后循环进行监听器注册(如果配置文件中配置 refresh 字段为 false,则不注册监听器)。

我们可以看到,监听器是以 dataId + groupId + namespace 为维度进行注册的,监听器的主要操作就三步。

  1. REFRESH_COUNT ++,在上述说的 loadNacosPropertySource 方法有用到

  2. 往 NacosRefreshHistory#records 中添加一条刷新记录

  3. 发布一个 RefreshEvent 事件,该事件是 SpringCloud 提供的,主要就是用来做环境变更刷新用的

注册操作经过 ConfigService,在 ClientWorker 中处理,这块会创建一个 CacheData 对象,该对象主要就是用来管理监听器的,也是非常重要的一个类。

CacheData 中字段如下图,ManagerListenerWrap 对 Listener 做层包装,内部会保存 listener、上次变更的 content 以及 md5(用来判断配置有没有变更用)。

并且在 addCacheDataIfAbsent 方法中会将刚才创建的 CacheData 缓存到 ClientWorker 中的一个 Map 中,后续会用到。

至此,在服务启动后向每一个需要支持热更新的配置都注册了一个监听器,用来监听远程配置的变动,以及做相应的处理

配置热更新

上面章节我们讲了服务启动的时候从远程 Nacos 服务端拉到配置,以及服务启动后对需要支持热更新的配置都注册了一个监听器,这个章节我们来说下配置变动后具体是怎么处理的。

回到上述说过的 NacosPropertySourceLocator 的 locate 方法看看,该方法首先会获取一个 ConfigService。

NacosConfigManager 中会进行一个 ConfigService 单例对象的创建,创建流程最终会委托给 ConfigFactory,使用反射方式创建一个 NacosConfigService 的实例对象,NacosConfigService 是一个很核心的类,配置的获取,监听器的注册都需要经此。

我们看下 NacosConfigService 的构造函数,会去创建一个 ClientWorker 类的对象,这个类是实现配置热更新的核心类。

ClientWorker 的构造函数里会去创建两个线程池,executor 会每隔 10ms 进行一次配置变更的检查,executorService 主要是用来处理长轮询请求的。

checkConfigInfo 方法中会创建一个长轮询任务丢到 executorService 线程池中去处理。

LongPollingRunnable 的 run 方法代码有点多,主要流程如下:

  1. 获取上个章节中说到的缓存 cacheMap,然后遍历,判断如果该配置使用的是本地缓存模式,则调用 checkListenerMd5 去检查读到的本地缓存文件中内容的 Md5 跟上次更新的 Md5 是不是一样,不一样则调用 safeNotifyListener 去通知监听器处理,并且更新 listenerWrap 中的 content、Md5

  1. checkUpdateDataIds 该方法中,会将所有的 dataId 按定义格式拼接出一个字符串,构造一个长轮询请求,发给服务端,Long-Pulling-Timeout 超时时间默认 30s,如果服务端没有配置变更,则会保持该请求直到超时,有配置变更则直接返回有变更的 dataId 列表。

  1. 拿到第二步有变更的 dataId 后会调用 getServerConfig 获取最新的配置内容,然后遍历调用 checkListenerMd5 去检查最新拉取的配置内容的 Md5 跟上次更新的 Md5 是不是一样,不一样则调用 safeNotifyListener 去通知监听器处理,并且更新 listenerWrap 中的 content、Md5

checkListenerMd5 方法如下,主要就是判断两个 md5 是不是相同,不同则调用 safeNotifyListener 处理。

safeNotifyListener 方法主要就是调用监听器的 receiveConfigInfo 方法,然后更新监听器包装器中的 lastContent、lastCallMd5 字段。

监听器要执行的方法我们上面也已经讲过了,这边再贴下截图,主要就是发布 RefreshEvent 事件。

至此,Nacos 的处理流程已经结束了,RefreshEvent 事件主要由 SpringCloud 相关类来处理。

RefreshEvent 事件处理

RefreshEvent 事件会由 RefreshEventListener 来处理,该 listener 含有一个 ContextRefresher 的对象。

如下图所示,refreshEnvironment 会去刷新 Spring 环境变量,实际上是交给 updateEnvironment 方法去做的刷新,具体刷新思想就是重新创建一个 Spring 容器,然后将这个新容器中的环境信息设置到原有的 Spring 环境中。拿到所有变化的配置项后,发布一个环境变化的 EnvironmentChangeEvent 事件。

ConfigurationPropertiesRebinder 会监听 EnvironmentChangeEvent 事件,监听到事件后会对所有的标注有 ConfigurationProperties 注解的配置类进行销毁后重新初始化的操作,完之后我们的配置类中的属性就是最新的了。

这里我们说到了会对标有 ConfigurationProperties 注解的配置类进行 rebind,那对于普通组件类里标有 @Value 注解的属性要怎么生效呢?这个其实需要配合 @RefreshScope 注解来生效的。

我们继续回到上述的 refresh() 方法,接着会有一步 refreshAll 的操作,会调用父类的 destroy 方法。

父类就是 GenericScope,我们知道 Spring 中的 Bean 是有Scope 的概念的,Spring 默认 Scope 有单例和原型两种,同时提供了 Scope 扩展接口,通过实现该接口我们可以定义自己的 Scope。

通过doGetBean 方法可以看出,这些自定义 Scope 类型对象的管理会交给相应的 Scope 实现去管理。

SpringCloud 实现的 RefreshScope 就是用来在运行时动态刷新 Bean 用的,RefreshScope 继承 GenericScope,提供 get 和 destroy 方法。

GenericScope 内部有一个 cache,用来保存所有该 Scope 类型的对象。

回到主线,所以在 refreshAll 中调用 super.destroy 方法时会将该 scope 的这些 Bean 都销毁掉,在下次 get 的时候在重新创建 Bean,新创建的 Bean 就有了我们最新的配置。

至此,我们就实现了配置热更新的效果了。

总结

文章从服务启动时的配置拉取,服务启动后的配置监听器注册,以及配置变动后的热更新实现三个方面从源码层面解析了整个的原理,希望对大家有所帮助。

个人开源项目

DynamicTp 是一个基于配置中心实现的轻量级动态线程池管理工具,主要功能可以总结为动态调参、通知报警、运行监控、三方包线程池管理等几大类。

目前累计 2.2k star,欢迎大家试用,感谢你的 star,欢迎 pr,业务之余一起给开源贡献一份力量

官网https://dynamictp.cn

gitee 地址https://gitee.com/dromara/dynamic-tp

github 地址https://github.com/dromara/dynamic-tp

SpringCloudAlibaba 微服务组件 Nacos 之配置中心源码深度解析的更多相关文章

  1. nacos统一配置中心源码解析

    配置文件想必大家都很熟悉,无论什么架构 都离不开配置,虽然spring boot已经大大简化了配置,但如果服务很多 环境也好几个,管理配置起来还是很麻烦,并且每次改完配置都需要重启服务,nacos c ...

  2. 微服务之Nacos配置中心源码解析(二)

    Nacos配置中心源码解析 源码入口 ConfigFactory.createConfigService ConfigService configService = NacosFactory.crea ...

  3. Apollo配置中心源码分析

    Apollo配置中心源码分析 1. apollo的核心代码分享 SpringApplication启动的关键步骤 在SpringApplication中,会加载所有实现了Init方法的类 protec ...

  4. Nacos配置中心源码分析

    1.使用 compile 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config:2.2.3.RELEASE' spring: app ...

  5. spring cloud config配置中心源码分析之注解@EnableConfigServer

    spring cloud config的主函数是ConfigServerApplication,其定义如下: @Configuration @EnableAutoConfiguration @Enab ...

  6. SpringCloudAlibaba 微服务讲解(三)Nacos Discovery-服务治理

    3.1 服务治理 先来思考一个问题,通过上一章的操作,我们已经实现微服务之间的调用,但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题: 一旦服务提供者地址变化,就需 ...

  7. 第二个视频作品《[SpringCloudAlibaba]微服务之注册中心nacos》上线了

    1.场景描述 第二个视频作品出炉了,<[SpringCloudAlibaba]微服务之注册中心nacos>上线了,有需要的朋友可以直接点击链接观看.(如需购买,请通过本文链接购买) 2. ...

  8. 【微服务】- Nacos - 注册中心

    微服务 - 注册中心 - Nacos 生命不息,写作不止 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 一个有梦有戏的人 @怒放吧德德 分享学习心得,欢迎指正,大家一起学习成长! 上一 ...

  9. java架构之路-(微服务专题)初步认识微服务与nacos初步搭建

    历史演变: 以前我们都是一个war包,包含了很多很多的代码,反正我开始工作的时候做的就是这样的项目,一个金融系统,代码具体多少行记不清楚了,内部功能超多,但是实际能用到的不多,代码冗余超大,每次部署大 ...

随机推荐

  1. KingbaseFlySync ddl变更流程

    关键字: KingbaseFlySync.Linux.x86_64.mips64el.aarch64.Java 一.ddl变更流程 1. 停掉客户业务,保证没有新数据产生 确认Oracle数据库上所有 ...

  2. 本地 maven + scala 跑spark wordcount

    pom.xml 点击查看代码 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http: ...

  3. 【Git进阶】基于文件(夹)拆分大PR

    背景 前段时间为了迁移一个旧服务到新项目,由此产生了一个巨大的PR,为了方便Code Review,最终基于文件夹,将其拆分成了多个较小的PR:现在这里记录下,后面可能还会需要. 演示 为了方便演示, ...

  4. Spring Boot2配置Swagger2生成API接口文档

    一.Swagger2介绍 前后端分离开发模式中,api文档是最好的沟通方式. Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务. 及时性 (接 ...

  5. 使用kubectl管理Kubernetes(k8s)集群:常用命令,查看负载,命名空间namespace管理

    目录 一.系统环境 二.前言 三.kubectl 3.1 kubectl语法 3.2 kubectl格式化输出 四.kubectl常用命令 五.查看kubernetes集群node节点和pod负载 5 ...

  6. C++ "链链"不忘@必有回响之单链表

    1. 前言 数组和链表是数据结构的基石,是逻辑上可描述.物理结构真实存在的具体数据结构.其它的数据结构往往在此基础上赋予不同的数据操作语义,如栈先进后出,队列先进先出-- 数组中的所有数据存储在一片连 ...

  7. 传输层协议(tcp ip和udp 三次握手 四次握手)

    1 TCP/IP协议介绍 TCP/IP协议:Transmission Control Protocol/Internet Protocol 传输控制协议/因特网互联协议. TCP/IP是一个Proto ...

  8. 以TrueType为例谈字形描述

    以TrueType为例谈字形描述 作者:哲思 时间:2022.9.17 邮箱:zhe__si@163.com GitHub:zhe-si (哲思) (github.com) 一.前言 在深入理解&qu ...

  9. 在 Kubernetes 容器集群,微服务项目最佳实践

    转载自:https://mp.weixin.qq.com/s/WYu3gDwKKf06f_FYbO9YRg 本文主要介绍我个人在使用 Kubernetes 的过程中,总结出的一套「Kubernetes ...

  10. Kubernetes容器运行时弃用Docker转型Containerd

    文章转载自:https://i4t.com/5435.html Kubernetes社区在2020年7月份发布的版本中已经开始了dockershim的移除计划,在1.20版本中将内置的dockersh ...