Nacos配置中心
环境准备: 下载nacos 1.4.1 版本(源码) 地址: github.com/alibaba/nac… (具体可以参考Nacos源码启动源码服务作为配置中心且成功连接数据库,我有文章介绍)
扩展: nacos也可以直接下载server包(非源码)直接使用,好处是简单方便,坏处是看不到具体执行过程 下载Nacos server 1.4.1 版本 地址: github.com/alibaba/nac…
修改为单机启动(默认是集群启动方式):
1.直接修改为启动脚本,将MODE = "cluster" 修改为 standalone
2.在bin目录下cmd 启动 startup.cmd -m standalone
代码准备:
从资料库: D:\BaiduNetdiskDownload\12-Alibaba微服务流控组件Sentinel实战下-fox\vip-spring-cloud-alibaba
主要用到mall-user 和 nacos-config
在nacos-config服务中打印了姓名name 和年龄age
nacos-config 的配置application.properties中配置:
nacos-config 项目中添加Nacos配置中心依赖
nacos-config的配置bootstarp.properties中添加nacos的配置
nacos控制台添加对应的配置文件nacos-config.yaml,默认先加载public命名空间下 配置文件的名对应服务名nacos-config, 配置文件后缀对应配置中yaml
启动nacos-config项目,输出结果: shuwei , 18
修改nacos-config.yaml配置: 将年龄改成19
此时服务会动态的感知到配置的变化,控制台会打印:
Nacos配置文件优先级
1.如果不指定配置文件后缀名:默认会读取nacos配置中心properties后缀的文件即: nacos-config.properties, 如果没有nacos-config.properties,会读取application.properties的原始值
假如不配置nacos-config.properties 会优先读取application.properties中的属性值
添加配置nacos-config.properties ,可以看到控制台马上感知到了配置变化
2.指定环境,环境隔离 如果配置文件bootstarp.properties中配置环境名,就会优先读取{profile}.${file-extension:properties}
现在配置spring.profiles.active = prod, 此时就会读取: nacos-config-prod.yaml
添加nacos控制台配置:
输出:
注意:此时控制台也有配置文件nacos-config.yaml , 但是优先读取nacos-config.prod.yaml 所以得出结论: nacos-config.prod.yaml优先级(最高) > nacos-config.yaml(次之)
3.指定namesapce命名空间 & group
nacos控制台新建一个prod的命名空间,并且命名空间id和配置文件相同
可以直接将public(默认空间)的配置克隆到新的prod空间里
修改prod空间下的nacos-config-prod.yaml 文件
输出:
此时使用的就是指定命名空间下的配置文件
4.共享配置(优先级最低)
像一些公用的属性配置,例如redis等等可以用通用配置
5.额外配置
额外配置可以同时支持多个,用[0],[1]区分, 数字越大,优先级越高,额外配置优先级高于共享配置(通用配置)
6.总结优先级 优先级从高到低 1)nacos-config-product.yaml精确配置 2)nacos-config.yaml 同工程不同环境的通用配置 3)ext-config: 不同工程扩展配置 4)shared-dataids 不同工程通用配置: common2.yml>common1.yml
Nacos配置中心服务案例: 以mall-user项目为例 1.先引入nacos配置中心依赖:
2.创建bootstrap.yml文件:
指定了配置中心地址,以及文件后缀名yml, 以及使用了额外的配置文件nacos.yml 和mybatis-plus.yml 指定了自动刷新开启 注意:
1).额外的配置文件是可以同时指定多个文件的,[0]不可少,以及逗号分隔配置文件
2).使用额外的两个配置文件以后,该项目会先读取mall-user.yml & nacos.yml & mybatis-plus.yml
3).早期版本yaml文件的配置只能写yaml, 现在的版本都支持yml, 对应nacos中的配置类型就是yaml
3.原本的application.yml 中的配置就可以提取出来放到mall-user.yml & nacos.yml & myabtis-plus.yml中
mall-user.yml中放一些基础配置,服务名,数据库配置等
nacos.yml中放nacos配置中心相关的配置
mybatis-plus.yml中放mybatis-plus的相关配置
启动mall-user服务,控制台可以打印出读取的配置文件:
可以看到分别读取了nacos.yml, mybatis.yml , mall-user.yml
(加载顺序代表了优先级,越后加载优先级越高)
这样就做到了配置拆分,根据不同的功能拆分,读取的时候合并读取,其他服务,例如订单服务再使用配置的时候,就不用写到自己的application.yml中,也可以使用nacos配置中心的相关配置
==========================================================================
@RefreshScope的使用
当我们从nacos配置中心只通过@Value来获取属性值的时候,是无法动态感知Nacos配置变化的,之前的例子我们是通过while死循环来感知的,所以才可以动态变化的,这种方式不可取
所以可以使用注解@RefreshScope来实现动态感知
一开始的值是age = 29
配置中心修改成30,再调用接口:
========================================================================= @NacosPropertySource + @NacosValue
以nacos-spring-boot-config-example项目为例
在Nacos上配置对应的配置文件,dataId = example
调用接口:结果是true,配置生效
改成false:
再调用接口: 值动态变化了,接口也感知到了
=========================================================================
配置中心架构
===========================================================================
SpringBoot和Nacos整合机制
在Springboot的SPI机制中加载了一系列监听器,其中涉及nacos的配置文件加载的就是:ConfigFileApplicationListener
在Springboot启动时:
一开始Spirngboot启动,调用run()==>
prepareEnviroment() ==> 涉及到spirngboot加载配置
==>
==> 其中就包括监听器: ConfigFileApplicationListener的调用(也就是早就发布了该事件,Springboot或者说Spirng就是在这里调用了监听器,触发了监听逻辑)
通过该监听器,一直会调用到PropertySourceLoader.load()
可以看到,是Springboot的run() ==> prepareEnvironment==> 来加载配置的(流程太麻烦省略)
===========================================================================
对于Nacos,借助了Spring提供的PropertySource类& Springboot提供的PropertySourceLoader资源加载器来加载配置
PropertySourceLoader.load() 有各种实现,有PropertiesPropertySourceLoader用来加载Properties配置文件,也有YamlPropertySourceLoader来加载yaml配置文件
以PropertiesPropertySourceLoader作为入口(nacos-config项目debug模式调试):
优先被加载的是bootstrap.properties,然后才是application.properties
==============================================================================
prepareEnvironment中会加载所有的nacos配置文件
==> 调用初始化器
==> 调用所有的初始化器
其中关于nacos的配置文件加载的初始化器: PropertySourceBootstrapConfiguration 初始化器需要实现ApplicationContextInitializer, 重写initialize()方法 Spring扩展点之一: ApplicationContextInitializer 在ConfigurableApplicationContext#refresh() 之前调用,通常用于需要对应用上下文做初始化的web应用中,比如根据上下文环境注册属性源或激活配置文件等 所以如果你想整合Springboot来加载自己的配置文件,就可以实现这个接口,重写initialize(),加载自己的配置
PropertySourceBootstrapConfiguration 是在spring-cloud-context的包中SPI机制加载进来的
调用它的initialize() ==>
==> 收集环境配置,最终 要的就是这个PropertySource
==> locate()方法只有一个实现类:NacosPropertySourceLocator
通过源码可以看到Nacos加载配置文件的顺序了 先读取Shared共享配置,然后读取Ext额外配置,最后读取应用配置(越晚读取,就会覆盖,优先级越高) 而应用配置中加载顺序如下:
文件名(微服务名称) ==> 文件名.扩展名 ==> 文件名.profile.文件扩展名
所以优先级: 文件名.profile.文件扩展名 > 文件名.扩展名> 文件名(微服务名)
例如:nacos-config-prod.yaml > nacos-config.yaml > nacos-config
=============================================================
获取配置的方法一定是调用了Nacos服务端的接口来获取配置的
==>
==>
==>
==>
==>
==>
==> 实现类NacosConfigService : 实现了ConfigService接口 并且提供了各种接口: getConfigAndSignListener 获取配置并且注册监听器 removeConfig 删除配置 publishConfig 发布配置 removeListener 删除监听器 addListener 注册监听器 getConfig 获取配置信息
==>
优先使用本地配置,保证哪怕断网了,也可以正常使用Nacos,用于容错,可以从本地去拉取本地配置 使用ClientWorker配置中心的工作线程,worker.getServiceConfig() 也就是发送http请求调用接口获取配置中心的配置 get请求接口url : v1/cs/configs
=============================================================
在Springboot启动时还会发布事件
==>
==> 在EventPublishingRunListener 中发布了ApplicationReadyEvent事件
发布事件就有对应的监听器: NacosContextRefresher 对应监听方法onApplicationEvent
==> 如果需要刷新配置,拿到所有的配置文件以后,给每个配置文件注册一个监听器,监听每一个配置文件的变化
==> 当每个配置发生变化的时候都会回调innerReceive()方法
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo); ==> 当配置发生变化的时候,添加历史版本记录
applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config")); ==> 发布了一个刷新事件: 将容器中的bean做一个替换 对应的监听器是RefreshEventListener ==>
==>
==>
1.refrehEnvironment() ==> (非重点)
extract(this.context.getEnvironment().getPropertySources()); ==> 抽取出除了SYSTEM, JNDI,SERVLET 之外所有的参数变量
addConfigFilesToEnvironment() ==> 把原来的enviroment里面的参数放到一个新建的Spring Context容器下重新加载,完事之后关闭容器,这里就是获取参数的新值了
changes(before,extract(this.context.getEnvironment().getPropertySources())) ==> 获取新的参数值,并和之前的进行比较,找出改变的参数值
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys)); ==> 发布环境变更事件,并带上改变的参数值
2.this.scope.refreshAll()==> 这里的scope 代表的是RefreshScope scope,就是我们使用的注解,这样容器刷新的时候,这个注解配置的类就能拿到最新的配置
==>
super.destory() ==> 先删除之前Scope里面的缓存,下次就会重新从BeanFactory获取一个新的实例(该实例使用新的配置)
this.context.publishEvent() ==> 后面就不跟了
=============================================================
服务端:
源码需要先打开nacos服务端代码(启动运行起来,使用源码项目来作为配置中心,连接上mysql数据库)
就先看之前获取配置的接口: get请求 v1/cs/configs 对应的就是这个 ConfigController#getConfig()接口
doGetConfig()==>
md5 = cacheItem.getMd5(); ==> 判断文件发生变化: Nacos是获取文件的MD5(或者hash)来比较文件是否发生变化,Eukera也是通过md5的方式来比较的
PropertyUtil.isDirectRead() ==> 如果是单机部署且使用derby数据源(内置),返回true,调用findConfigInfo() 查询实时配置, 如果是集群部署或者使用mysql,则返回false,读取本地文件系统中的配置,调用targetFile()
targetFile()==> 走到该步骤的前提是Nacos服务端成功连接数据库
也就是会从本地文件系统的data目录中去获取配置信息(当nacos服务端配置发生变化时,本地文件系统data下的配置数据也会同时发生变化) 注意: 这里不是去查mysql,而是去查本地磁盘缓存,所以直接修改mysql的配置是不行的,修改配置需要发布ConfigDataChangeEvent事件,触发本地文件和内存的更新
=============================================================
数据库里存储配置数据的作用: 客户端服务启动的时候将数据的配置读取出来加载到服务器的磁盘中 DumpService 中实现
EmbeddedDumpServcei 对应的是内置数据库derby ExternalDumpService 对应的外置数据库 mysql
所以看一下mysql的实现:
==> 全量dump配置信息
==>
全量与增量 如果最后一次的心跳间隔时间超过6个小时,全量的将配置信息从数据库复制到磁盘,如果小于6个小时,则进行增量复制
dumpAllProcessor.process(new DumpAllTask()); ==>
persistService.findConfigMaxId() ==> 查询mysql,获取最大的主键id
select max(id) from config_info
Page< ConfigInfoWrapper> page = persistService.findAllConfigInfoFragment(lastMaxId, PAGE_SIZE); ==> 根据最大id,来进行分页,每次捞取1000条配置刷新进磁盘和内存
select id, data_id , gorup_id ,teant_id , app_name, content, md5, gmt_modified,type from config_info where id> ? order by id asc limit ? ?
ConfigCacheService.dump() ==> 写入磁盘 ==>
DiskUtil.saveToDisk(dataId, group, tenant, content); ==> 将配置保存到磁盘文件中,当客户端调用nacos配置中心通过接口getConfig获取配置的时候优先获取磁盘文件中的配置,就是这里保存到磁盘的
updateMd5(groupKey, md5, lastModifiedTs); ==> 缓存配置信息的MD5 到内存中,并发布localDateChangeEvent事件 ==>
如果MD5不相同,则更新cacheItem中的MD5属性和lastModifiedTs属性,并发布事件LocalDateChangeEvent事件来通知配置数据有变化
===========================================================================
** 用户修改nacos控制台配置的配置文件**
用户通过nacos-console控制界面修改了配置,点击发布 ConfigController # publishConfig() ==>
- persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, true); ==> 选择外部数据库msyql对应的实现类: ExternalStoragePersistServiceImpl
假设现在是新增配置: addConfigInfo(srcIp, srcUser, configInfo, time, configAdvanceInfo, notify); ==>
做的事无非就是查询旧配置,更新配置,保存旧配置到历史
2.ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime())); ==> 触发ConfigDataChangeEvent事件==>
==>
==>
==>
==>循环的去通知==>
这里拿到的subscirber有两种订阅者: LongPollingService 以及 AsyncNotifyService subscirber.onEvent(event) 传入的event参数是: ConfigDataChangeEvent ==>
==> 1)调用LongPollingService.onEvent() ==>
其实也就是调用LongPollingService#LongPollingService()构造方法里面里面的onEvent(), LongPollingService是一个长轮询的服务,而这个LongPollingService在作为bean初始化的时候就已经调用了构造方法,然后已经注册好了订阅者服务,并且声明好了订阅方法onEvent(),这是一种发布订阅模式
event instanceof LocalDataChangeEvent ==> 判断了是否是LocalDataChangeEvnet事件
ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps)); ==>如果是LocalDataChangeEvnet事件,会调用该方法
所以这里的事件ConfigDataChangeEvent 是不匹配LocalDataChangeEvnet 的,不会走 但是这里会底层调用了两次(两个事件实际都触发了,我debug跟了很久,也没看出来LocalDataChangeEvnet是怎么调用的,但是确实调用了,至于为啥最终还会调用LocalDataChangeEvent,可以不用深究,知道这里确实会触发即可)
==>线程池执行任务,即执行LongPollingService.DataChangeTask # run() ==>
迭代allSubs队列,得到ClientLongPolling, 这是客户端的长轮询对象,里面保存了一些例如ip, clientMd5Map, asyncContext等等数据
asyncContext是Servlet3.0新增的异步处理, 这个对象持有客户端的request和response, 就是通过这个对象,在服务端有了数据变更的情况下,可以将变更的数据响应给客户端, 然后响应配置发生变化的key
allSubs队列中的数据是什么时候加进去的呢? 后面会分析
2)调用AsyncNotifyService.onEvent()==>
这里判断事件就是LocalDataChangeEvnet, 就是让集群其他节点来同步配置(集群模式下才需要看,暂时不看)
===========================================================
NacosConfigBootstrapConfiguration
先查一下Nacos-config的SPI配置文件spring.factories
可以看到NacosConfigBootstrapConfiguration里面@Bean初始化了三个类:
1.NacosConfigProperties 配置中心的属性配置类,对应bootstrap.properties中的配置信息
2.NacosPropertySourceLocator 实现了PropertySourceLocator接口,该接口是Spring cloud提供的接口,Springboot启动的时候调用PropertySourceLocator.locate(env)来加载配置信息
3.NacosConfigManager 持有NacosConfigProperties 以及ConfigService ===> new NacosConfigManager(nacosConfigProperties) ==>
==>
==>
又得来到NacosConfigService的构造方法==>
1.初始化了一个MetricsHttpAgent agent , 用来向nacos server发起请求的代理
2.初始化了一个ClientWorker 客户端工作类 ==>
1.首先创建了一个线程数 = CPU核数的周期性线程池 放入executorService, 并用线程工厂重写newThread方法设置了线程类型为守护线程
2.启动了一个延迟定时类线程池scheuleWithFixedDelay,四个参数分别是线程,初始延迟时间,每次执行任务的间隔时间,时间单位
3.new ThreadFactory() 是new 接口, 重写了创建线程的方法newThread,线程池调用线程的时候调用的工厂方法,通过这个方法可以进行设置线程的优先级,线程命名规则以及线程类型,是守护线程还是普通线程,适合给线程池的线程做统一的初始化工作
知识点: ThreadFactory是线程池的重要参数之一,在Executor中提供了一个默认的线程工厂
checkConfigInfo(); = >
这里的含义是利用创建好的线程池executorService 来执行 LongPollingRunable cacheMap中缓存着需要刷新的配置,将cacheMap中的数量以3000分一个组 ==>
1.checkLocalConfig(cacheData); ==> 检查本地配置
2.checkUpdateDataIds(cacheDatas, inInitializingCacheList); ==> 向nacos server发出一个长连接30s超时,返回nacos server有更新过的dataIds
也就是发送http POST请求调用接口: http://127.0.0.1:8848/nacos/v1/cs/configs/listener
携带参数 readTimeoutMs = 45s
header参数:
Long-Pulling-Timeout = 30s
Long-Pulling-Timeout-No-Hangup = true
后面会具体分析这个接口
3.getServerConfig(dataId, group, tenant, 3000L); ==> 根据变化的dataId调用服务端获取配置信息 /nacos/v1/cs/configs,并更新本地快照
- cacheData.checkListenerMd5(); ==> 对有变化的配置调用对应的监听器处理
==>
==>
listener.receiveConfigInfo(contentTmp);==> AbstarctSharedListener
==>
还记得在前面说过SpringBoot容器启动的时候给每个配置文件按照dataId纬度添加了一个监听器嘛,就是刷新配置和应用的监听器,里面的回调方法就是innerReceive,就是在这里调用的,所以有配置发生变化的时候会调用这个方法
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo); ==> 添加刷新纪录
applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config")); ==> 发布RefreshEvent事件,对应的监听器为RefreshEventListener
在RefreshEventListener的onApplicationEvent()方法中可以看到这个监听器监听的父类型是ApplicationEvent
过来的监听器可以是ApplicationReadyEvent 或者是 RefreshEvent事件
现在我们过来的是RefreshEvent ==> handle((RefreshEvent) event)) ==>
this.refresh.refresh(); ==> this.refresh其实是ContextRefresher.refresh() 用来刷新容器中标记了@RefreshScope Bean的类
1.refreshEnvironment() ==> 刷新环境,把原来环境里的参数放到一个新建的SpringContext容器下重新加载,完事之后关闭容器,获取新参数值和之前的进行比较,找出改变的参数,然后发布环境变更事件 EnvironmentChangeEvent ,带上改变的参数值
2.this.scope.refreshAll(); ==> 对应就是RefreshScope.refreshAll 刷新refreshScope里的bean实例
super.destory()==> 清除Scope里面的缓存,下次就会重新从BeanFactory获取一个新的实例(该实例使用新的配置)
this.context.publishEvent(new RefreshScopeRefreshedEvent()); ==> 发布一个RefreshScope的刷新事件
===========================================================
来看服务端: /v1/cs/configs/listener接口
前面的分析我们知道: 该接口作为客户端pull拉取配置变化结果的接口,10ms执行一次轮询这个接口, 30s超时
==>
LongPollingService.isSupportLongPolling(request) ==> 判断是否支持长轮询,判断的依旧就是请求的header中是否携带参数Long-Pulling-Timeout ,如果携带了参数就代表是长轮询的逻辑,如果没有携带参数,就不支持长轮询,则直接与当前的配置进行比较,返回有变更的配置
longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize); ==> 支持长轮询的处理==>
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500); ==> 服务端这边最多处理时长29.5s, 需要预留个0.5s来返回,以免客户端那边超时
long timeout = Math.max(10000, Long.parseLong(str) - delayTime); ==> 拿30-0.5 = 29.5s
MD5Util.compareMd5(req, rsp, clientMd5Map); ==> 比较客户端的md5与当前Server端的是否一致, 不一致的返回到changedGroups
generateResponse(req, rsp, changedGroups); ==> md5比较不一致,生成响应信息直接返回
ConfigExecutor.executeLongPolling(new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag)); ==> 执行长连接任务 ==>
无论配置有没有更新,延迟29.5s执行,然后把自己添加到一个allSubs中
ClientLongPolling被提交给scheduler执行,可以拆分成4个步骤: 1.创建一个调度的任务,调度的延时时间为29.5s 2.将该clientLongPolling自身的实例添加到一个allSubs中去 3.延时时间到了之后,首先将该ClientLongPolling 自身的实例从allSubs 中移除 4.获取服务端中保存的对应客户端请求的groupKeys,检查是否发生变更,将结果写入response返回给客户端
=======================================================
Nacos配置中心总源码图