学习不用那么功利,二师兄带你从更高维度轻松阅读源码~

本篇文章我们来通过源码分析一下Nacos的本地缓存及故障转移功能,涉及到核心类为ServiceInfoHolder和FailoverReactor。

ServiceInfoHolder功能概述

ServiceInfoHolder类,顾名思义,服务信息的持有者。前面文章已经多次涉及到ServiceInfoHolder类,比如每次客户端从注册中心获取新的服务信息时都会调用该类的processServiceInfo方法来进行本地化的处理,包括更新缓存服务、发布事件、更新本地文件等。

除了上述功能,该类在实例化时,还做了包含本地缓存目录初始化、故障转移初始化等操作。下面我们就逐一分析一下。

ServiceInfo的本地内存缓存

ServiceInfo,注册服务的信息,其中包含了服务名称、分组名称、集群信息、实例列表信息、上次更新时间等。也就是说,客户端从注册中心获取到的信息在本地都以ServiceInfo作为承载着。

而ServiceInfoHolder类又持有了ServiceInfo,通过一个ConcurrentMap来存储:

public class ServiceInfoHolder implements Closeable {private final ConcurrentMap<String, ServiceInfo> serviceInfoMap;
}

这就是Nacos客户端对服务注册信息的第一层缓存。前面分析processServiceInfo方法时,我们已经看到,当服务信息变更时会第一时间更新serviceInfoMap中的信息。

public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
// ....// 缓存服务信息serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);// 判断注册的实例信息是否已变更boolean changed = isChangedServiceInfo(oldService, serviceInfo);if (StringUtils.isBlank(serviceInfo.getJsonFromServer())) {serviceInfo.setJsonFromServer(JacksonUtils.toJson(serviceInfo));}// ....
}

关于serviceInfoMap的使用就这么简单,当变动实例向其中put最新数据即可。当使用实例,根据key进行get操作即可。

而serviceInfoMap在ServiceInfoHolder的构造方法中进行初始化,默认创建一个空的ConcurrentMap。但当配置了启动时从缓存文件读取信息时,则会从本地缓存进行加载。

// 启动时是否从缓存目录读取信息,默认false。设置为true会读取缓存文件
if (isLoadCacheAtStart(properties)) {this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(DiskCache.read(this.cacheDir));
} else {this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(16);
}

这里涉及到了本地缓存目录,在processServiceInfo方法中,当服务实例变更时,会看到通过DiskCache#write方法向该目录写入ServiceInfo信息。

// 服务实例已变更
if (changed) {NAMING_LOGGER.info("current ips:(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "+ JacksonUtils.toJson(serviceInfo.getHosts()));// 添加实例变更事件,会被推动到订阅者执行NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),serviceInfo.getClusters(), serviceInfo.getHosts()));// 记录Service本地文件DiskCache.write(serviceInfo, cacheDir);
}

下面就来聊聊本地缓存目录。

本地缓存目录

本地缓存目录作为ServiceInfoHolder的一个属性存在,用于指定本地缓存的根目录和故障转移的根目录。

private String cacheDir;

在ServiceInfoHolder的构造方法中,第一个调用的便是生成缓存目录:

public ServiceInfoHolder(String namespace, Properties properties) {// 生成缓存目录:默认为${user.home}/nacos/naming/public,// 可以通过System.setProperty("JM.SNAPSHOT.PATH")自定义根目录initCacheDir(namespace, properties);//...
}

关于生成目录的源码就不看了,默认缓存目录为${user.home}/nacos/naming/public,可以通过System.setProperty(“JM.SNAPSHOT.PATH”)自定义根目录。

初始化完该目录之后,故障转移信息也存储在该目录下。

故障转移

同样在ServiceInfoHolder的构造方法中,会初始化一个FailoverReactor类,同样是ServiceInfoHolder的成员变量。FailoverReactor的作用便是用来处理故障转移的。

this.failoverReactor = new FailoverReactor(this, cacheDir);

这里的this为ServiceInfoHolder当前的对象,也就是说两者相互持有对方的引用。

来看FailoverReactor构造方法:

public FailoverReactor(ServiceInfoHolder serviceInfoHolder, String cacheDir) {// 持有ServiceInfoHolder引用this.serviceInfoHolder = serviceInfoHolder;// 拼接故障根目录:${user.home}/nacos/naming/public/failoverthis.failoverDir = cacheDir + FAILOVER_DIR;// 初始化executorServicethis.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);// 守护线程模式运行thread.setDaemon(true);thread.setName("com.alibaba.nacos.naming.failover");return thread;}});// 其他初始化操作,通过executorService开启多个定时任务执行this.init();
}

FailoverReactor的构造方法基本上把它的功能都展示出来了:

  • 持有ServiceInfoHolder引用;
  • 拼接故障根目录:${user.home}/nacos/naming/public/failover,其中public也有可能是其他的自定义命名空间;
  • 初始化executorService;
  • init方法:通过executorService开启多个定时任务执行;

init方法执行

init方法中开启了三个定时任务:

  • 初始化立即执行,执行间隔5秒,执行任务为SwitchRefresher;
  • 初始化延迟30分钟执行,执行间隔24小时,执行任务为DiskFileWriter;
  • 初始化立即执行,执行间隔10秒,执行核心操作为DiskFileWriter;

这三个任务都是FailoverReactor的内部类,先看后两个任务DiskFileWriter的实现:

class DiskFileWriter extends TimerTask {@Overridepublic void run() {Map<String, ServiceInfo> map = serviceInfoHolder.getServiceInfoMap();for (Map.Entry<String, ServiceInfo> entry : map.entrySet()) {ServiceInfo serviceInfo = entry.getValue();if (StringUtils.equals(serviceInfo.getKey(), UtilAndComs.ALL_IPS) || StringUtils.equals(serviceInfo.getName(), UtilAndComs.ENV_LIST_KEY) || StringUtils.equals(serviceInfo.getName(), UtilAndComs.ENV_CONFIGS) || StringUtils.equals(serviceInfo.getName(), UtilAndComs.VIP_CLIENT_FILE) || StringUtils.equals(serviceInfo.getName(), UtilAndComs.ALL_HOSTS)) {continue;}// 将缓存内容写入磁盘文件DiskCache.write(serviceInfo, failoverDir);}}
}

逻辑非常简单,就是获取ServiceInfoHolder中缓存的ServiceInfo,判断是否满足写入磁盘文件,如果满足,则将其写入前面拼接的故障转移目录:${user.home}/nacos/naming/public/failover。只不过第二个定时任务和第三个定时任务的区别时,第三个定时任务有前置判断,只有当文件不存在时才执行。

最后再来看一下SwitchRefresher的核心实现如下:

File switchFile = new File(failoverDir + UtilAndComs.FAILOVER_SWITCH);
// 文件不存在退出
if (!switchFile.exists()) {switchParams.put("failover-mode", "false");NAMING_LOGGER.debug("failover switch is not found, " + switchFile.getName());return;
}long modified = switchFile.lastModified();if (lastModifiedMillis < modified) {lastModifiedMillis = modified;// 获取故障转移文件内容String failover = ConcurrentDiskUtil.getFileContent(failoverDir + UtilAndComs.FAILOVER_SWITCH,Charset.defaultCharset().toString());if (!StringUtils.isEmpty(failover)) {String[] lines = failover.split(DiskCache.getLineSeparator());for (String line : lines) {String line1 = line.trim();// 1表示开启故障转移模式if (IS_FAILOVER_MODE.equals(line1)) {switchParams.put(FAILOVER_MODE_PARAM, Boolean.TRUE.toString());NAMING_LOGGER.info("failover-mode is on");new FailoverFileReader().run();} else if (NO_FAILOVER_MODE.equals(line1)) {// 0表示关闭故障转移模式switchParams.put(FAILOVER_MODE_PARAM, Boolean.FALSE.toString());NAMING_LOGGER.info("failover-mode is off");}}} else {switchParams.put(FAILOVER_MODE_PARAM, Boolean.FALSE.toString());}
}

上述代码的逻辑梳理如下:

  • 如果故障转移文件不存在,则直接返回。故障转移【开关】文件为名为“00-00—000-VIPSRV_FAILOVER_SWITCH-000—00-00”。
  • 比较文件修改时间,如果已经修改,则获取故障转移文件中的内容。
  • 故障转移文件中存储了0和1标识。0表示关闭,1表示开启。
  • 当为开启状态时,执行线程FailoverFileReader。

FailoverFileReader,顾名思义,就是故障转移文件读取。基本操作就是读取failover目录存储ServiceInfo的文件内容,然后转换成ServiceInfo,并用将所有的ServiceInfo存储在FailoverReactor的serviceMap属性中。

failover目录文件内容示例如下:

(base) appledeMacBook-Pro-2:failover apple$ ls
DEFAULT_GROUP%40%40nacos.test.1
DEFAULT_GROUP%40%40user-provider@@DEFAULT
DEFAULT_GROUP%40%40user-service-consumer@@DEFAULT
DEFAULT_GROUP%40%40user-service-provider
DEFAULT_GROUP%40%40user-service-provider@@DEFAULT

文件内容格式如下:

{"hosts": [{"ip": "1.1.1.1","port": 800,"valid": true,"healthy": true,"marked": false,"instanceId": "1.1.1.1#800#DEFAULT#DEFAULT_GROUP@@nacos.test.1","metadata": {"netType": "external","version": "2.0"},"enabled": true,"weight": 2,"clusterName": "DEFAULT","serviceName": "DEFAULT_GROUP@@nacos.test.1","ephemeral": true}],"dom": "DEFAULT_GROUP@@nacos.test.1","name": "DEFAULT_GROUP@@nacos.test.1","cacheMillis": 10000,"lastRefTime": 1617001291656,"checksum": "969c531798aedb72f87ac686dfea2569","useSpecifiedURL": false,"clusters": "","env": "","metadata": {}
}

下面看一下其中的核心业务实现:

for (File file : files) {if (!file.isFile()) {continue;}// 如果是故障转移标志文件,则跳过if (file.getName().equals(UtilAndComs.FAILOVER_SWITCH)) {continue;}ServiceInfo dom = new ServiceInfo(file.getName());try {String dataString = ConcurrentDiskUtil.getFileContent(file, Charset.defaultCharset().toString());reader = new BufferedReader(new StringReader(dataString));String json;if ((json = reader.readLine()) != null) {try {dom = JacksonUtils.toObj(json, ServiceInfo.class);} catch (Exception e) {NAMING_LOGGER.error("[NA] error while parsing cached dom : " + json, e);}}} catch (Exception e) {NAMING_LOGGER.error("[NA] failed to read cache for dom: " + file.getName(), e);} finally {try {if (reader != null) {reader.close();}} catch (Exception e) {//ignore}}// ... 读入缓存if (!CollectionUtils.isEmpty(dom.getHosts())) {domMap.put(dom.getKey(), dom);}
}

代码基本流程如下:

  • 读取failover目录下的所有文件,进行遍历处理;
  • 如果文件不存在,跳过;
  • 如果文件是故障转移标志文件,跳过;
  • 读取文件中的json内容,转化为ServiceInfo对象;
  • 将ServiceInfo对象放入domMap当中;

当for循环执行完毕,如果domMap不为空,则将其赋值给serviceMap:

if (domMap.size() > 0) {serviceMap = domMap;
}

那么,有同学会问了,这个serviceMap在哪里用到呢?前面我们讲获取实例的时候,通常会调用一个名为getServiceInfo的方法:

public ServiceInfo getServiceInfo(final String serviceName, final String groupName, final String clusters) {NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);String key = ServiceInfo.getKey(groupedServiceName, clusters);if (failoverReactor.isFailoverSwitch()) {return failoverReactor.getService(key);}return serviceInfoMap.get(key);
}

也就是说,如果开启了故障转移,则会优先调用failoverReactor#getService方法,而这个方法便是从serviceMap中获取ServiceInfo。

public ServiceInfo getService(String key) {ServiceInfo serviceInfo = serviceMap.get(key);if (serviceInfo == null) {serviceInfo = new ServiceInfo();serviceInfo.setName(key);}return serviceInfo;
}

至此,关于Nacos客户端的故障转移流程分析完毕。

小结

本篇文章介绍了Nacos客户端本地缓存及故障转移的实现。所谓的本地缓存有两方面,第一方面是从注册中心获得实例信息会缓存在内存当中,也就是通过Map的形式承载,这样查询操作都方便。第二方法便是通过磁盘文件的形式定时缓存起来,以备不时之需。

而故障转移也分两方面,第一方面是故障转移的开关是通过文件来标记的;第二方面是当开启故障转移之后,当发生故障时,可以从故障转移定时备份的文件中来获得服务实例信息。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

06篇 Nacos Client本地缓存及故障转移相关推荐

  1. Nacos客户端本地缓存和故障转移

    在Nacos客户端从Server获得服务的时候,在某些时候出现了一些故障, 这时候为了保证服务正常,Nacos进行了故障转移,原理就是将之前缓存的服务信息拿出来用,防止服务出现问题,涉及到的核心类为S ...

  2. 删除本地缓存localStorage定义的字段 - 代码篇

    localStorage :操作篇 重要代码: localStorage.removeItem("字段名") //移除localStorage 某个对应的字段 localStora ...

  3. Caffeine本地缓存详解(一篇就明白)

    结论:Caffeine 是目前性能最好的本地缓存,因此,在考虑使用本地缓存时,直接选择 Caffeine 即可. 先看一个小例子,明白如何创建一个 Caffeine 缓存实例. Caffeine ca ...

  4. 微信小程序入门篇4---新闻网 本地缓存 三个页面

    首先,这个小程序较前三个难度加大 项目目录 首页 index.js */var common = require('../../utils/common.js')Page({data: {swiper ...

  5. 《跟二师兄学Nacos吧》第1篇 Nacos客户端服务注册源码分析

    开篇构想 在此之前,已经写了十多篇Nacos的文章,感觉Nacos还值得更深入的学习一下.于是萌生了写一个Nacos源码系列专栏的文章. 写作的目标呢,有两个:第一,能够系统的学习Nacos知识:第二 ...

  6. 探讨下如何更好的使用缓存 —— 集中式缓存Redis的BitMap存储、管道与事务、以及与本地缓存一起构建多级缓存

    大家好,又见面了. 通过前面的文章,我们一起剖析了Guava Cache.Caffeine.Ehcache等本地缓存框架的原理与使用场景,也一同领略了以Redis为代表的集中式缓存在分布式高并发场景下 ...

  7. android-远程图片获取和本地缓存

    概述 对于客户端--服务器端应用,从远程获取图片算是经常要用的一个功能,而图片资源往往会消耗比较大的流量,对应用来说,如果处理不好这个问题,那会让用户很崩溃,不知不觉手机流量就用完了,等用户发现是你的 ...

  8. 本地缓存之王-Caffeine

    引言 随着业务体量的增长,使用的缓存方案一般会经过:1)无缓存直接查DB:2)数据同步+Redis:3)多级缓存 三个阶段. 第1阶段直接查DB只能用于小流量场景,随着QPS升高,需要引入缓存来减轻D ...

  9. 教程篇(7.0) 07. 诊断和故障排除 ❀ FortiClient EMS ❀ Fortinet 网络安全专家 NSE 5

     在本课中,你将学习如何诊断和排除FortiClient问题和FortiClient EMS问题.  在这节课中,你将学习上图显示的主题.  通过展示处理和排除FortiClient问题的能力,你将能 ...

最新文章

  1. 第四期 SA 分析师认证名单正式公布!
  2. linux 压缩以及归档
  3. 拼接的option会多出空行_Word空格,空行,页眉横线等问题,我只花一分钟就全解决了...
  4. Ext.Net全部Icon图标名称展示
  5. 程序员是如何处理密码的?
  6. 平台如何限制ip流量_社区团购平台如何通过地推获得更多流量?
  7. 计数器verilog代码(quartus II)
  8. 【软件工程】 期末考试 重点复习
  9. java贪吃蛇添加背景音乐_java代码中简单添加背景音乐(亲测有效)
  10. the little schemer 笔记(6)
  11. 算术左移,逻辑左移,算术右移,逻辑右移之间的区别
  12. 电信各种视频免流卡申请地址合集附地址失效解决方法
  13. Python爬虫福利:带你爬取妹子图上的美女图片,学习改变生活
  14. winpe 能否修复服务器系统盘,U盘启动盘winpe修复系统的技巧
  15. HTMLCSS登录界面及讲解
  16. Angular *NgFor - angular 基础教程
  17. vscode 调试php 乱码,vscode调试c解决中文乱码怎么办
  18. 蓝光原版与蓝光Remux格式到底有什么区别?
  19. 【Python深度学习之路】-3.2PR曲线
  20. YUV420存储为BMP和JPG图片

热门文章

  1. 传奇战盟GOM引擎登录器配置教程
  2. ESP32用Arduino编程实现CAN总线通讯
  3. word2vec的经验总结
  4. 固定资产盘点的问题及解决方案,云呐如何做固定资产盘点
  5. AutoCAD 2019,cad设计绘图必备哦
  6. Java中xml转义字符和gt,gte,lt,lte缩写
  7. 风控模型评估方法以及大数据风控模型概念
  8. thinkphp5整合系列之汉字转拼音完美支持多音字
  9. 【迅为iMX6Q】开发板:迅为官方提供好的kernel的编译
  10. 利用Java8新特性stream流给集合中的某个属性赋值