这些不知道,别说你熟悉 Nacos,深度源码解析!

贾萍
• 阅读 739

大家好,这篇文章跟大家聊下 SpringCloudAlibaba 中的微服务组件 Nacos。Nacos 既能做注册中心,又能做配置中心,这篇文章主要来聊下做配置中心时 client 端的一些设计,主要从源码层面进行分析,相信看完这篇文章你对 Nacos client 端的工作原理应该有比较深刻的了解。
SpringCloud 应用启动拉去配置
我们之前写过一篇文章,介绍了一些 Spring 提供的扩展机制。其中说到了 ApplicationContextInitializer,该扩展是在上下文准备阶段(prepareContext),容器刷新之前做一些初始化工作,比如我们常用的配置中心 client 基本都是继承该初始化器,在容器刷新前将配置从远程拉到本地,然后封装成 PropertySource 放到 Environment 中供使用。
在 SpringCloud 场景下,SpringCloud 规范中提供了 PropertySourceBootstrapConfiguration 继承 ApplicationContextInitializer,另外还提供了个 PropertySourceLocator,二者配合完成配置中心的接入。

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

上图展示了在 spring-cloud-starter-alibaba-nacos-config 包提供的自动装配类中进行了 NacosPropertySourceLocator 的定义,该类继承自上述说的 PropertySourceLocator,重写了 locate 方法进行配置的读取。
我们来分析下 NacosPropertySourceLocator,locate 方法只提取了主要流程代码,可以看到 Nacos 启动会加载以下三种配置文件,也就是我们在 bootstrap.yml 文件里配置的扩展配置 extension-configs、共享配置 shared-configs 以及应用自己的配置,加载到配置文件后会封装成 NacosPropertySource 返回。

这些不知道,别说你熟悉 Nacos,深度源码解析!

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 加载应用配置时,同时会加载以下三种配置,分别是

不带扩展名后缀,application

带扩展名后缀,application.yml

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

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

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

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

至此,在项目启动的时候(上下文准备阶段)我们就拉到了远程 Nacos 中的配置,并且封装成 NacosPropertySource 放到了 Spring 的环境变量里。
监听器注册
上面章节我们说了服务启动的时候从远程 Nacos 服务端拉到配置,这个章节我们来说下配置变动怎么实时通知到客户端,首先需要注册监听器。
主要看 NacosContextRefresher 类,该类会监听服务启动发布的 ApplicationReadyEvent 事件,然后进行配置监听器的注册。

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

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

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

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

至此,在服务启动后向每一个需要支持热更新的配置都注册了一个监听器,用来监听远程配置的变动,以及做相应的处理
配置热更新
上面章节我们讲了服务启动的时候从远程 Nacos 服务端拉到配置,以及服务启动后对需要支持热更新的配置都注册了一个监听器,这个章节我们来说下配置变动后具体是怎么处理的。
回到上述说过的 NacosPropertySourceLocator 的 locate 方法看看,该方法首先会获取一个 ConfigService。

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

至此,Nacos 的处理流程已经结束了,RefreshEvent 事件主要由 SpringCloud 相关类来处理。
RefreshEvent 事件处理
RefreshEvent 事件会由 RefreshEventListener 来处理,该 listener 含有一个 ContextRefresher 的对象。

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

这些不知道,别说你熟悉 Nacos,深度源码解析!

这里我们说到了会对标有 ConfigurationProperties 注解的配置类进行 rebind,那对于普通组件类里标有 @Value 注解的属性要怎么生效呢?这个其实需要配合 @RefreshScope 注解来生效的。
我们继续回到上述的 refresh() 方法,接着会有一步 refreshAll 的操作,会调用父类的 destroy 方法。

这些不知道,别说你熟悉 Nacos,深度源码解析!

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

这些不知道,别说你熟悉 Nacos,深度源码解析!

这些不知道,别说你熟悉 Nacos,深度源码解析!

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

点赞
收藏
评论区
推荐文章
捉虫大师 捉虫大师
2年前
如何组装一个注册中心
hello,大家好呀,我是小楼。今天不写BUG,来聊一聊注册中心。标题本来想叫《如何设计一个注册中心》,但网上已经有好多类似标题的文章了。所以打算另辟蹊径,换个角度,如何组装一个注册中心。组装意味着不必从0开始造轮子,这也比较符合许多公司对待自研基础组件的态度。知道如何组装一个注册中心有什么用呢?第一可以更深入理解注册中心。以我个人经历来说,注册中心的第一印
捉虫大师 捉虫大师
3年前
我在组内的Nacos分享
本文已收录https://github.com/lkxiaolou/lkxiaolou欢迎star。Nacos简介Nacos:NamingandConfigurationService,可打包部署配置中心和注册中心,也可独立部署其中之一,配置中心、控制台依赖mysql,由阿里巴巴2018年8月开源,github19.1kstar(截止20
从Nacos客户端视角来分析配置中心原理
Hello,大家好,我是麦洛,今天带大家一起从Nacos客户端视角来看看配置中心实现原理;整理这篇文章时候,也参照学习了部分大佬的博客,这里致谢;大家在阅读过程中如果发现错误或者问题,可以私信我交流,一起交流学习,一起进步;在开始阅读文章之前,有些思路我按我的理解先阐述一些,方便大家更快理清思路,不对的地方还请大家批评指正;1.Nacos客户端会在
捉虫大师 捉虫大师
3年前
Nacos注册中心之概要设计
前言在之前的文章中分析了Nacos配置中心,配置中心的核心是配置的创建、读取、推送。注册中心的核心比配置中心多一个服务探活模块,他俩的相似度非常高,甚至阿里内部的注册中心就叫ConfigServer。Nacos注册中心打算分成几个模块来分析,本文重点在于概要设计,基于2.0.0版本。环境搭建用Nacos的源码来搭建源码阅读和调试环境,可参考Nacos调试
Stella981 Stella981
3年前
Nacos跨服务器调用服务报错
利用gateway做springcloud微服务网关路由服务时出现报错情况,发现是和网关不在一个服务器的服务无法使用Nacos服务注册的IPNacos注册中心是:https://github.com/alibaba/nacos各个服务通过Nacos客户端将服务信息注册到Nacos上当Nacos服务注册的IP默认选择出问题时,可以通
Stella981 Stella981
3年前
Nacos配置中心动态获取数组配置
有的时候我们需要动态获取一系列的配置项,假设我们在nacos配置中心的配置如下,nacos配置中心的其他设置请参考Nacos搭建流程(https://my.oschina.net/u/3768341/blog/3138297)skill:name:爆炸冲刺在SpringCloud代
Stella981 Stella981
3年前
Spring Cloud Alibaba系列之Nacos分布式配置中心
SpringCloudAlibaba系列之Nacos分布式配置中心1、前言介绍SpringCloudAlibabaNacosConfig提供用于存储配置和其他元数据的key/value存储,为分布式系统中的外部化配置提供服务器端和客户端支持,nacosconfig是SpringCloudconfigServer和
Stella981 Stella981
3年前
Nacos作为注册中心和配置中心,爱不释手的感觉
在使用SpringCloud做分布式微服务架构时,注册中心是必不可少的一个组件。目前可以用的主要有:Eureka、Consul、Zookeeper。今天,我们就来说一下Alibaba的Nacos怎么样?下载与安装下载地址https://github.com/alibaba/nacos/releases安装:
Easter79 Easter79
3年前
SpringCloud Alibaba微服务实战六
!(https://oscimg.oschina.net/oscnet/f42d2dffa6424a90b9b92653d815be92.gif)导读:本篇作为SpringCloudAlibaba微服务实战系列的第六篇,主要内容是将所有的微服务接入Nacos配置中心并按环境进行隔离。系列文章,欢迎持续关注。!(https:/
Stella981 Stella981
3年前
Spring Cloud Alibaba系列(二)nacos作为服务配置中心
SpringCloudAlibaba系列(二)nacos作为服务配置中心Nacos提供用于存储配置和其他元数据的key/value存储,为分布式系统中的外部化配置提供服务器端和客户端支持。使用SpringCloudAlibabaNacosConfig,您可以在NacosServer集中管理你SpringCloud应用的外部
Stella981 Stella981
3年前
Spring Cloud Alibaba Nacos 服务配置中心和注册中心
学习在SpringCloud中使用Nacos实现服务配置中心和注册中心,类似SpringCloudConfig和SpringCloudNetflixEureka提供的功能。1概述SpringCloudAlibaba是阿里巴巴提供的一套微服务开发一站式解决方案。主要提供的功能:分布式配置中心