学习不必那么功利,二师兄带你从更高维度轻松浏览源码~

本篇文章咱们来通过源码剖析一下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$ lsDEFAULT_GROUP%40%40nacos.test.1DEFAULT_GROUP%40%40user-provider@@DEFAULTDEFAULT_GROUP%40%40user-service-consumer@@DEFAULTDEFAULT_GROUP%40%40user-service-providerDEFAULT_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