Nacos源码系列整体栏目
【一】nacos服务注册底层源码详解
【二】nacos服务发现底层源码详解
【三】nacos的心跳机制底层源码详解
【四】nacos配置中心的底层源码详解
主要就是通过这个nacos来作为一个配置中心,来统一管理这个配置
nacos客户端所有的这个文件配置实现主要是在这个NacosNamingService的类下面,那么这个配置中心主要是在这个NacosConfigService的这个类下面。该接口下面主要有一些获取配置,发布配置,增加监听器,删除配置,删除监听器等操作。
public interface ConfigService {
//获取配置
String getConfig();
//删除配置
boolean removeConfig(String dataId, String group);
//发布
boolean publishConfig();
//监听
void addListener();
//删除监听器
void removeListener();
}
在加载完所有的context上下文之后,客户端就回去拉取这个注册中心里面的这个全部配置文件
@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}
然后在这个getConfigInner方法里面,就是具体的拉取配置这个实现
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException{
// 优先使用本地配置
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
//如果本地配置不为空,则直接返回
if (content != null) {
return content;
}
//如果本地配置为空,就会去服务端那边拉取这个全部的配置文件
//需要通过这个http请求发起这个远程调用
try{
//拉取这个需要的配置
String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
//保存这个结果到本地
cr.setContent(ct[0]);
}
}
这个读取的本地配置的具体实现如下,主要是通过这个getFailover方法实现
public static String getFailover(String serverName, String dataId, String group, String tenant) {
//获取这个本地文件,
File localPath = getFailoverFile(serverName, dataId, group, tenant);
//如果本地文件为空,则直接return返回
if (!localPath.exists() || !localPath.isFile()) {
return null;
}
//本地文件不为空,则读取
return readFile(localPath);
}
如果本地为空,则需要去向这个服务端的配置中心发起http请求,并且最后会通过这个接口回调来判断这个响应的状态码。主要是在这个getServerConfig的方法里面具体实现
public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout) throws NacosException {
HttpRestResult<String> result = null;
//建立http请求
result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
switch (result.getCode()) {
case HttpURLConnection.HTTP_OK: ...省略
case HttpURLConnection.HTTP_NOT_FOUND:
case HttpURLConnection.HTTP_CONFLICT:
case HttpURLConnection.HTTP_FORBIDDEN:
default:
}
在整个容器启动完成之后,就会去调用这个监听器。nacos主要是在这个NacosContextRefresher类下面来实现这个监听,其实现了这个ApplicationListener这个接口,就是一个nacos的一个上下文的一个刷新流。构造方法如下
public NacosContextRefresher(NacosRefreshProperties refreshProperties,NacosRefreshHistory refreshHistory, ConfigService configService) {
/刷新配置文件
this.refreshProperties = refreshProperties;
//刷新历史文件
this.refreshHistory = refreshHistory;
this.configService = configService;
}
在这个类里面,会调用一个onApplicationEvent的事件方法,里面就会去进行一个nacos的监听的一个注册。
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
if (this.ready.compareAndSet(false, true)) {
//nacos的监听注册
this.registerNacosListenersForApplications();
}
}
其注册的nacos的监听器的具体方法如下,就是会去获取nacos的全部的配置文件,然后在获取id之后,通过这个id对这个服务进行一个监听。
private void registerNacosListenersForApplications() {
if (refreshProperties.isEnabled()) {
for (NacosPropertySource nacosPropertySource : NacosPropertySourceRepository
.getAll()) {
//获取id
String dataId = nacosPropertySource.getDataId();
registerNacosListener(nacosPropertySource.getGroup(), dataId);
}
}
}
其监听这个nacos的主要方法registerNacosListener的具体实现如下,当配置发生变化的时候,这个监听方法就会发起一个调用,就会对立面的这个配置进行一个更新和替换。每一次更新都会有一个历史版本,
private void registerNacosListener(final String group, final String dataId){
Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() {
//当配置发生变化的时候,这个监听方法就会发起一个调用
@Override
public void receiveConfigInfo(String configInfo) {
//记录这个历史版本
refreshHistory.add(dataId, md5);
//发布这个监听事件
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
}
}
最后会去调用一个refresh方法,会进行一个环境的刷新,会将新的参数和原来的参数进行一个比较,通过发布这个环境变更事件,对做出改变的值进行一个更新操作。
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
如果感知到这个对应的配置有改变的操作之后,就会清除当前的配置实例,会将新的实例重新通过这个bean工厂进行一个重新getBean的一个操作。
就是在这个客户端进行启动的时候,就会优先拉取本地的配置,如果本地配置不存在,那么就会和这个服务端建立这个http请求,然后去拉取这个服务端的全部配置,就是配置中心的全部配置。在拉取到全部配置之后,会去获取每一个配置文件的dataId,然后通过这个id对服务端的每一个配置文件进行一个监听的操作。每当服务端这边的配置文件出现修改的时候,就可以通过这个监听器进行到一个感知,然后这个客户端也会对对应的配置文件进行修改,每一份修改的配置都会存储在这个nacos配置文件里面,会作为一个历史文件保留。
这个主要是在这个ConfigController这个类下面,在服务端的nacos-config的模块下面。
里面有一个重要的方法,就是getConfig的这个方法,
@GetMapping
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
public void getConfig(HttpServletRequest request, HttpServletResponse response,
@RequestParam("dataId") String dataId, @RequestParam("group") String group,
@RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
@RequestParam(value = "tag", required = false) String tag)
throws IOException, ServletException, NacosException {
// check tenant
ParamUtils.checkTenant(tenant);
tenant = NamespaceUtil.processNamespaceParameter(tenant);
// check params
ParamUtils.checkParam(dataId, group, "datumId", "content");
ParamUtils.checkParam(tag);
final String clientIp = RequestUtil.getRemoteIp(request);
//这里的方法就是记性具体的获取配置信息
inner.doGetConfig(request, response, dataId, group, tenant, tag, clientIp);
}
接下来就想看看这个doGetConfig方法。主要是从这个本地文件读取这个配置,而不是读取这个数据库的配置。这个文件主要存储在这个nacos的data的文件目录下。
public String doGetConfig(HttpServletRequest request, HttpServletResponse response, String dataId, String group, String tenant, String tag, String clientIp) throws IOException, ServletException{
File file = null;
//md5对这些文件里面的数据进行一个加密的操作
md5 = cacheItem.getMd54Beta();
//从磁盘里面获取这个文件的信息
file = DiskUtil.targetBetaFile(dataId, group, tenant);
}
主要在这个DumpService抽象类下面,有从内存中将全部配置文件存入到这个磁盘里面。
通过快捷键ctrl + alt + b可以查看这个抽象类下面的全部实现,主要有如下两个类,分别是EmbeddedDumpService和这个ExternalDumpService类。
然后这个实现类里面会有一个初始化方法,会通过这个bean的前置处理器去初始化这个实例。然后通过这个dumpOperate方法来实现这个具体的配置文件的存储。
@PostConstruct
@Override
protected void init() throws Throwable {
//存储这个配置文件
dumpOperate(processor, dumpAllProcessor, dumpAllBetaProcessor, dumpAllTagProcessor);
}
在这个dumpOperate方法里面,来实现这个存储的具体实现。在这里面有大量的代码,其主要是一些全量加载和一些增量加载。
protected void dumpOperate(){
TimerContext.start(dumpFileContext);
try{
Runnable dumpAll = () -> dumpAllTaskMgr.addTask(DumpAllTask.TASK_ID, new DumpAllTask());
Runnable dumpAllBeta = () -> dumpAllTaskMgr.addTask(DumpAllBetaTask.TASK_ID, new DumpAllBetaTask());
Runnable dumpAllTag = () -> dumpAllTaskMgr.addTask(DumpAllTagTask.TASK_ID, new DumpAllTagTask());
}
Runnable clearConfigHistory = () -> {
LOGGER.warn("clearConfigHistory start");
if (canExecute()) {
try {
Timestamp startTime = getBeforeStamp(TimeUtils.getCurrentTime(), 24 * getRetentionDays());
//用于分页,每次获取磁盘里面的1000行数据
int totalCount = persistService.findConfigHistoryCountByTime(startTime);
if (totalCount > 0) {
int pageSize = 1000;
int removeTime = (totalCount + pageSize - 1) / pageSize;
while (removeTime > 0) {
persistService.removeConfigHistory(startTime, pageSize);
removeTime--;
}
}
} catch (Throwable e) {
}
}
};
//加载配置信息
try {
//判断是增量获取还是全量获取,主要是通过这个时间是否大于6小时
dumpConfigInfo(dumpAllProcessor);
}
}
服务端总结
就是每个配置文件在注册之后,都会现存在这个mysql里面,最后会将这个mysql里面的数据存入到磁盘里面,在客户端来拉取这个配置信息的时候,就会直接去读这个本地磁盘里面的数据。