共计 8495 个字符,预计需要花费 22 分钟才能阅读完成。
学习不必那么功利,二师兄带你从更高维度轻松浏览源码~
本篇文章咱们来通过源码剖析一下 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/failover
this.failoverDir = cacheDir + FAILOVER_DIR;
// 初始化 executorService
this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
@Override
public 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 {
@Override
public 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