Nacos配置服务的源码解析
从Nacos的源代码分析Nacos Cofnig的实现
NacosConfigAutoConfiguration
负责Nacos配置服务的自动配置
如果spring.cloud.nacos.config.enabled(默认为true)为false时因为ConditionalOnProperty注解不导入到Spring Factory。
装配的Bean:
- NacosConfigProperties:Nacos配置文件POJO,负责通过Spring的配置功能从配置文件中加载nacos的配置(前缀为spring.cloud.nacos)。创建时会考虑ApplicationContext的层次性。使用方法assembleConfigServiceProperties封装成Properties。
- NacosConfigManager:Nacos配置管理器,接收一个(依赖于)NacosConfigProperties,并且创建(通过NacosFactory.createConfigService)、管理(提供getter)**ConfigServer(配置服务器)**对象。
- NacosRefreshHistory:Nacos刷新历史,存储Nacos的刷新历史。历史记录时间戳、DataId、Group、以及数据的摘要(MD5)。最多存储最新的MAX_SIZE(20)条记录。用于在Endpoint中展示。
- NacosContextRefresher:Nacos上下文刷新器,接收(依赖于)NacosConfigManager、NacosRefreshHistory,负责在程序启动时从ConfigServer拉取配置,以及侦听配置的刷新并执行刷新。通过Nacos配置文件的spring.cloud.nacos.config.refresh-enable(默认开启)属性控制开关。当Spring Application发布ApplicationReadyEvent事件后向ConfigServer添加对于所有开启自动刷新的配置源所指定的DataId、Group的数据监听(Listener)。
Nacos Config实现
NacosPropertySourceLocator
Nacos Config通过使用Spring Cloud提供的PropertySourceLocator进行资源定位。Nacos创建其实现类NacosPropertySourceLocator定位配置服务器上的配置文件。
首先看一下PropertySourceLocator是干什么的。PropertySourceLocator让spring读取我们自定义的配置文件(注册到Spring Environment),然后使用**@Value注解即可读取到配置文件中的属性,这解释了为什么Nacos等配置中心可以直接使用Value注解进行配置的读取。值得一提,这是基于SPI(Service Provider Interface)机制的,需要在META-INF/spring.factories中定义BootstrapConfiguration**,所以Nacos Config也实现了一个BootstrapConfiguration,其中就向Spring容器注册了NacosPropertySourceLocator这个Bean。
PropertySourceLocator中提供了方法locate,方法传入一个Environment对象(当前Spring应用环境),返回一个PropertySource对象(配置源)。在Nacos实现的定位器中实现了从配置服务器加载配置的功能。分析源码得:
- 从NacosConfigManager获取ConfigServer对象,如果为空,返回null。
- 获取超时时间(spring.cloud.nacos.config.timeout)及配置文件名称(spring.cloud.nacos.config.name),如未指定名称则使用前缀(spring.cloud.nacos.config.prefix),如果还是没有指定则使用应用名(spring.application.name)。
- 创建CompositePropertySource(表示一组配置源,继承于PropertySource,Name为NACOS)。
- 加载共享配置(loadSharedConfiguration)
- 从配置文件中获取spring.cloud.nacos.config.shared-configs中的配置文件
- 通过NacosPropertySourceBuilder构建配置源(如果可刷新的配置源不为0个(代表已加载过配置源)且不开启自动刷新,从配置源仓库(NacosPropertySourceRepository)中加载)
- 存入NacosPropertySourceRepository
- 加载拓展配置(loadExtConfiguration),类似于加载共享配置。
- 加载应用配置(loadApplicationConfiguration)
- 获取配置文件格式(spring.cloud.nacos.config.file-extension)
- 获取组(spring.cloud.nacos.config.group)
- 使用步骤2中获取的配置文件名称直接加载一次,存入NacosPropertySourceRepository
- 加上文件类型的后缀名加载一次,存入NacosPropertySourceRepository
- 对于所有活动的配置文件(Environment.getActiveProfiles)使用文件名-配置文件后缀.文件类型的作为配置名称进行加载,存入NacosPropertySourceRepository
- 返回CompositePropertySource
这样就实现了获取并使用配置服务器上的配置文件的功能,而且可以发现Nacos不仅仅加载单个配置文件,它会把所有可能涉及的配置文件都加载进来作为一个配置源。
NacosPropertySourceBuilder
此外关于如何从Config Server获取配置源的流程封装在NacosPropertySourceBuilder,它在NacosPropertySourceLocator进行资源定位(locate)时被创建。
使用这个类的build方法将会从配置服务器加载配置源。
- 使用ConfigServer的getConfig方法加载配置
- 使用NacosDataParserHandler的parseNacosData方法解析数据
- 通过PropertySourceLoader进行加载,这里会有多个实现(JSON、XML以及原有的YML、PROPERTITES)
- 创建NacosByteArrayResource,并且设置文件名(带后缀)
- 调用PropertySourceLoader.load从NacosByteArrayResource读取内容返回一个数据源列表
- 将EnumerablePropertySource转为OriginTrackedMapPropertySource
- 向NacosPropertySourceRepository存入加载的数据源
NacosFactory
这个类封装了Nacos的几个Factory(ConfigFactory、NamingFactory、NamingMaintainFactory)来提供ConfigService、NamingService和NamingMaintainService。
ConfigFactory
用于创建ConfigService,只有两个方法分别通过Porperties和serverAddr来创建配置服务。但是其创建配置服务的具体实现在Porperty中(serverAddr被封装为Porperties)。
- 通过反射获取了**NacosConfigService(ConfigService的具体实现)**这个类
- 调用类的构造方法创建实例(传入Porperties)
- 检查Properties中CONTEXT_PATH属性是不是合法(不为空且没有两个’/'连在一起)
- 从Properties中拿到编码默认为(UTF-8)
- 从Properties中取出并且构造为命名空间放回Properties(是为了整合多租户和云端解析)
- 创建ConfigFilterChainManager对象,这个类实现了IConfigFilterChain接口,管理一组IConfigFilter(按顺序对过滤器排序,内部将执行过滤器链任务委托给一个VirtualFilterChain实现)。创建时使用JDK6引入的ServiceLoader查找IConfigFilter。
- 创建一个agent(MetricsHttpAgent),用于与服务器进行交互(基于HTTP协议),封装了通信细节。
- 创建ClientWorker,提供了getServerConfig的实现,用于从配置服务器获取指定配置。
剩下几个是属于Nacos注册中心的工厂,先不做讨论。
NacosConfigService
Nacos的配置服务,提供获取配置(getConfig)、添加监听器(addListener)、移除监听器(removeListener)、发布配置(publishConfig)和移除配置(removeConfig)功能。
底层都是交由ClientWorker或者Agent实现,只是在其基础上进行封装作为门面。
- 获取配置由getConfigInner封装worker实现,会依次从本地文件(开发者管理)、服务器和快照(在从服务器获取成功配置后存储)尝试获取配置,然后通过ConfigFilterChainManager进行定制操作(从服务器获取失败时抛出异常)。
- 发布配置由publishConfigInner封装agent实现,会先交给ConfigFilterChainManager进行定制操作,将要发布的配置内容和基本信息通过agent发送到配置服务器,返回值为是否发布成功,如果服务器返回状态码HTTP_FORBIDDEN则抛出异常。
- 异常配置由removeConfigInner封装agent实现,将要移除的配置基本信息通过agent发送到配置服务器,返回值为是否删除成功,如果服务器返回状态码HTTP_FORBIDDEN则抛出异常。
- 添加监听、移除监听全部直接交由worker负责。
ClientWorker
客户端工作器。提供添加监听器、移除监听器、添加缓存数据、获取缓存数据、获取配置服务器配置内容以及后台定时拉取更新配置。
监听器相关
ClientWorker中关于监听器的实现是围绕CacheData和Listener展开的。
Listener
用于接收指定配置的更新。
其中,receiveConfigInfo方法负责接收更新内容、getExecutor用于指定执行通知时的线程池。
AbstractSharedListener用于接收所有共享配置,在通知前会使用fillContext来指明来自哪一个配置。
PropertiesListener将接收到的配置转为Properties再交给用户处理。
AbstractConfigChangeListener是一个特殊的监听器,会另外接收一个ConfigChangeEvent用于指定配置文件变化内容。
结构图如下
CacheData
这个类代表了本地的一份缓存数据,是配置服务器上某一份配置的本地缓存。存储DataId、Group、Tenant(Namespace)和Content这几个核心内容。此外还存储了内容的MD5摘要、文件类型和监听器列表。Nacos的监听器就是配置在这个类中的。
基本原理是从服务器拉取最新配置后通过setContent方法设置最新内容并且重新计算md5,调用checkListenerMd5方法来对所有的md5和新内容不一致监听器进行更新通知。
- CacheData中定义了一个ManagerListenerWrap类对Listener进行封装,保存了这个监听器内容的MD5摘要以及最新内容。
具体通知方式是在safeNotifyListener中实现的,对于每一个md5和新内容不一致监听器都使用这个方法进行通知。
视Listener的getExecutor方法是不是为空进行同步/异步执行下列操作。
- 判断监听器是不是共享数据监听器(AbstractSharedListener)的实例,是则调用fillContext设置属性。(因为共享设置监听器不止监听一个配置)
- 构造ConfigResponse,然后通过ConfigFilterChainManager进行定制操作。
- 取出处理过的内容,调用监听器的receiveConfigInfo方法进行通知。
- 判断监听器是不是AbstractConfigChangeListener的实例,是则通过ConfigChangeHandler的parseChangeData方法获取所有更新的内容(用ManagerListenerWrap的监听器之前最新内容为参照),封装为ConfigChangeEvent,然后调用receiveConfigChange方法进行通知。
- 更新ManagerListenerWrap的最新内容。
值得注意的是,创建一个CacheData实例的时候会优先从用户指定配置进行加载,如果不存在则从快照进行加载初始内容与加密秘钥,同时,设置为正在初始化状态,等待LongPollingRunnable进行更新检查完成初始化。
配置刷新
ClientWorker管理了一组CacheData,并且在后台进行10ms一次的checkConfigInfo,这个方法主要检查管理的CacheData是否需要被更新。过程如下:
- 获取管理的CacheData数量。
- 确定长轮询总量(每3000个CacheData对应一个长轮询)。
- 如果长轮询总量大于已有长轮询的数量则向线程池放入一个LongPollingRunnable实例,并且更新已有长轮询的数量。
LongPollingRunnable
这个类实现了Runnable接口,负责向服务器进行长轮询,流程如下:
-
获取这个任务对应的所有CacheData。
-
检查这些CacheData的本地内容。
- 如果不使用本地配置且本地文件存在:使用本地文件内容填充CacheData,使用本地配置标记。
- 如果使用本地配置且本地文件不存在:取消使用本地配置的标记。
- 如果使用本地配置且本地文件存在且本地文件版本(最后修改时间)更高:使用本地文件内容填充CacheData,使用本地配置标记。
【本地文件包括用户配置(高优先级)和快照】
【本地标记配置在CacheData初始化时默认为 false,每次更新后为 true】
-
如果使用本地配置文件填充则校对MD5摘要,如果更新了进行通知。(调用CacheData#checkListenerMd5)
-
检查所有没有使用本地配置的CacheData,组装配置名称,最后汇总成一个名单,如果有初始化的CacheData置isInitializingCacheList标记为true。
- 向服务器发起请求,如果isInitializingCacheList为true则即时返回,否则挂起一段时间(默认3s)直到时间结束或者名单中的配置文件有更新。
- 如果服务器返回不正常则将setHealthServer(false),表示服务器状态差。
- 否则设置为true,并且解析返回内容(需要更新的配置名单)。
-
对于要更新的配置名单,使用getServerConfig方法从服务器获取最新配置。
-
向对应的CacheData填充数据(内容、秘钥和类型)。
-
对于所有更新的CacheData,如果这个CacheData不处于初始化状态或者在更新名单中检查MD5摘要并且设置为已经初始化。
-
清理相关内容后重新在线程池中运行自身(循环)。
HttpAgent
Http代理。负责和服务器进行实际交互,屏蔽HTTP细节。不多讲解。