目录
版本说明
nacos官方示例
集成nacos客户端源码分析
nacos服务端源码分析
总结
个人认为分析配置中心的源码要比注册中心更需要spring boot源码的理解,主要体现在更新或者刷新容器内部配置文件内容。
主要分为两部分讲解:1、spring boot启动集成nacos及拉取远端配置信息;2、nacos动态更新配置,客户端如何感知并在未重启应用的情况下如何刷新配置。
spring-boot版本:1.5.21.RELEASE
spring-cloud-alibaba版本:1.5.1.RELEASE
nacos版本:1.x
在分析源码之前我们先来看一个nacos官方example示例:
public class ConfigExample {
public static void main(String[] args) throws NacosException, InterruptedException {
String serverAddr = "127.0.0.1";
String dataId = "test";
String group = "DEFAULT_GROUP";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
//根据dataId获取配置信息
String content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
//添加配置信息变更的监听器
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("receive:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});
//发布变更配置信息
boolean isPublishOk = configService.publishConfig(dataId, group, "content");
System.out.println(isPublishOk);
Thread.sleep(3000);
content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
//根据dataId删除配置信息
boolean isRemoveOk = configService.removeConfig(dataId, group);
System.out.println(isRemoveOk);
Thread.sleep(3000);
content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
Thread.sleep(300000);
}
}
执行结果:
根据示例结果我们先来认识一下nacos核心组件ConfigService,通过这个configService可以向nacos服务端发起获取或发布配置的请求,还可注册监听器,监听发生变化的配置,而nacos发布配置动态更新就是通过监听器实现。注意是一个dataId对应一个监听器。
spring boot集成spring cloud alibaba后,SpringApplication.run()会被执行两次,这是spring cloud利用BootstrapApplicationListener事件监听器另起炉灶,通过再次调用SpringApplication.run()重新创建了一个spring容器。关于这块内容另出一期博客,下面代码注释可以看出spring cloud创建的spring容器是spring boot创建的spring容器的父容器。
spring boot应用在启动过程中,在加载创建spring容器后和refresh()之间,会有调用prepareContext()方法,准备刷新容器之前需要初始化的方法,这其中就包含容器初始化器接口的ApplicationContextInitializer.initialize()方法,在spring boot启动流程分析的博客中提了一嘴,默认会去spring boot中spring.factories文件中读取6个初始化器的实现类,如图。
而在引入spring-cloud-alibaba-nacos后,还会加载其他的初始化器,其中就包含加载nacos配置文件的PropertySourceBootstrapConfiguration,由于第一次创建的是bootstrap容器,所以执行PropertySourceBootstrapConfiguration#initialize()方法是在第二次创建spring boot容器时执行的,这里还有其他的一些初始化器,我们忽略掉。
在spring boot自动装配的过程中,会加载NacosConfigBootstrapConfiguration,它创建了NacosPropertySourceLocator的bean,在执行上面的locator.locate(),就调用到NacosPropertySourceLocator.locate()方法,
在分析NacosPropertySourceLocator.locate()之前我们来看一个示例,就明白他们是干什么的了,在nacos UI中分别创建如下配置:
share.properties:
user.age=20
user.height=180
ext.properties:
user.name=james
user.age=21
nacos-config.properties:
user.name=rick
user.weight=50
实验结构得知:加载配置文件的顺序是application覆盖ext,ext覆盖share。
再回过头来看NacosPropertySourceLocator.locate(),它里面有三个核心方法,以及创建configService对象用来与nacos服务端进行交互。然后创建CompositePropertySource对象,他有一个属性很重要,propertySources set集合,用来缓存所有从nacos获取的配置源。
他们依次从nacos服务中拉取bootstrap配置文件中配置的share.properties,ext.properties,nacos-config.properties。只分析loadSharedConfiguration(),其他两个方法的调用流程是相同的。
先读取所有需要拉取的共享配置,然后遍历所有的dataId,调用loadNacosDataIfPresent()
这里主要是通过loadNacosPropertySource()加载创建NacosPropertySource,然后通过addFirstPropertySource()方法将它添加到set
loadNacosPropertySource()主要是做判断,看是否可以直接从缓存中获取配置,如果需要刷新配置再执行build()方法
build()先执行loadNacosData()方法,返回的properties对象,构建NacosPropertySource对象,然会调用collectNacosPropertySources()进行缓存,缓存的是一个map集合,key:dataId,value:nacosPropertySource。
最后到loadNacosData(),通过configService.getConfig()从Nacos服务端拉取配置信息。到这里configService就是nacos客户端提供的api用来获取远端nacos服务的配置信息。继续深入configService.getConfig()源码进行分析
先调用getFailover()方法从本地获取配置信息,文件所在目录:用户空间\nacos\config\fixed-127.0.0.1_8848_nacos\snapshot\DEFAULT_GROUP\xxx.xxx。这样做主要有两个优点:1、避免每次从服务端拉取配置,减少网络开销;2、如果nacos服务宕机或者网络可不用了,服务还可以继续使用(容灾)。
然后调用worker.getServerConfig(),使用agent.httpGet()实际底层调用HttpURLConnection发起http请求(请求路径为/v1/cs/configs),返回结果通过saveSnapshot()方法,写入磁盘,目录就是:用户空间\nacos\config\fixed-127.0.0.1_8848_nacos\snapshot\DEFAULT_GROUP\xxx.xxx
请求来到nacos服务端的ConfigController.getConfig()
一通校验后,进入inner.doGetConfig(),该方法很长,我截取重要的代码来分析
如果没有配置则直接调用persistService.findConfigInfo(),它直接从java的内存数据Derby获取配置信息。
如果在console子工程中resources/application.properties或者已经打好包的配置文件配置并使用mysql数据库,最终代码会执行DiskUtil.targetTagFile(),从磁盘中读取配置信息。问题来了配置信息明明存储在mysql数据库中,磁盘中的数据是从哪来的呢?
那就有请另一个重要的抽象类DumpService,他有两个实现类,分别是EmbeddedDumpService和ExternalDumpService,其中DumpService有一个抽象方法init(),它的两个子类都实现了init()。没错,init()方法会将数据库中的数据写到磁盘中,那么为什么要这么做呢?
先来看看ExternalDumpService.init(),@PostConstruct就不用解释了吧
在dumpOperate()中会向任务管理器TaskManager的父类NacosDelayTaskExecuteEngine的ConcurrentHashMap中添加任务,稍微注意下这个DumpAllTask.TASK_ID,在dumpService的构造方法中会将这个taskId绑定DumpAllProcessor这个任务处理器
在NacosDelayTaskExecuteEngine中定义定时任务线程池来处理ProcessRunnable,ProcessRunnable的run调用processTasks()处理任务
最终调用DumpAllProcessor.process()方法,使用循环分页的方式查询数据库中的数据,然后调用ConfigCacheService.dump()将数据写到对应的文件中,目录是:用户空间\nacos\data\config-data\DEFAULT_GROUP\xxx.xx,当然在dump()方法中会先通过md5的对比是否需要更新数据。
到这里客户端应用从nacos服务端的请求链路就打通了,还遗留一个问题,为什么要将数据从数据库中写到磁盘中,而不是直接读数据库?可能是为了避免每次都请求mysql的网络开销。
1、nacos客户端主要加载共享配置,拓展配置,应用配置,这三个配置的优先级由低到高(先加载的会被后加载的覆盖掉)。应用配置支持配置应用名称/应用名称.后缀名/应用名称.profile.后缀名
2、nacos客户端从服务端拉取到配置后,会缓存到内存和写入磁盘,防止nacos服务宕机或者网络分区导致服务不可用
3、nocos服务端支持两种模式,分别是基于内嵌内存数据库Derby,请求直接查询数据库,另一种外部数据库(如mysql),DumpService会启动定时任务定期将数据库配置信息写到磁盘中,请求会从磁盘文件中读取数据。
最后画了一张流程图: