06篇 Nacos Client本地缓存及故障转移

简介: 06篇 Nacos Client本地缓存及故障转移

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


ServiceInfoHolder功能概述

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


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


ServiceInfo的本地内存缓存

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


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


public class ServiceInfoHolder implements Closeable {

   private final ConcurrentMap<String, ServiceInfo> serviceInfoMap;

}

1

2

3

这就是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);

}

1

2

3

4

5

6

这里涉及到了本地缓存目录,在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;

1

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


public ServiceInfoHolder(String namespace, Properties properties) {

   // 生成缓存目录:默认为${user.home}/nacos/naming/public,

   // 可以通过System.setProperty("JM.SNAPSHOT.PATH")自定义根目录

   initCacheDir(namespace, properties);

   //...

}

1

2

3

4

5

6

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


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


故障转移

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


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

1

这里的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

1

2

3

4

5

6

文件内容格式如下:


{
    "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;

}

1

2

3

那么,有同学会问了,这个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的形式承载,这样查询操作都方便。第二方法便是通过磁盘文件的形式定时缓存起来,以备不时之需。


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




目录
相关文章
|
7月前
|
缓存 API Nacos
在MSE微服务引擎中为Nacos客户端启用本地缓存
在MSE微服务引擎中为Nacos客户端启用本地缓存
79 1
|
10月前
|
存储 设计模式 缓存
Nacos 客户端本地缓存及故障转移源码分析(四)
Nacos 客户端本地缓存及故障转移源码分析(四)
338 1
|
缓存 Java fastjson
实战监听Eureka client的缓存更新
Spring cloud环境中的应用,如果注册到Eureka server,就会从Eureka server获取所有应用的注册信息(也叫服务列表),然后保存到本地,这个操作是周期性的,默认每三十秒一次,今天咱们来通过实战将其观察得清清楚楚
254 0
实战监听Eureka client的缓存更新
|
Nacos
解决集群环境nacos频繁报错:找不到节点 send request fail,client not connected。
解决集群环境nacos频繁报错:找不到节点 send request fail,client not connected。
1047 0
解决集群环境nacos频繁报错:找不到节点 send request fail,client not connected。
|
存储 缓存 JSON
06篇 Nacos Client本地缓存及故障转移
06篇 Nacos Client本地缓存及故障转移
946 0
06篇 Nacos Client本地缓存及故障转移
|
存储 缓存 Nacos
05篇 Nacos Client服务订阅之事件机制剖析
05篇 Nacos Client服务订阅之事件机制剖析
455 0
05篇 Nacos Client服务订阅之事件机制剖析
|
2小时前
|
消息中间件 缓存 NoSQL
Redis经典问题:缓存雪崩
本文介绍了Redis缓存雪崩问题及其解决方案。缓存雪崩是指大量缓存同一时间失效,导致请求涌入数据库,可能造成系统崩溃。解决方法包括:1) 使用Redis主从复制和哨兵机制提高高可用性;2) 结合本地ehcache缓存和Hystrix限流降级策略;3) 设置随机过期时间避免同一时刻大量缓存失效;4) 使用缓存标记策略,在标记失效时更新数据缓存;5) 实施多级缓存策略,如一级缓存失效时由二级缓存更新;6) 通过第三方插件如RocketMQ自动更新缓存。这些策略有助于保障系统的稳定运行。
138 1
|
2小时前
|
存储 消息中间件 缓存
Redis缓存技术详解
【5月更文挑战第6天】Redis是一款高性能内存数据结构存储系统,常用于缓存、消息队列、分布式锁等场景。其特点包括速度快(全内存存储)、丰富数据类型、持久化、发布/订阅、主从复制和分布式锁。优化策略包括选择合适数据类型、设置过期时间、使用Pipeline、开启持久化、监控调优及使用集群。通过这些手段,Redis能为系统提供高效稳定的服务。
|
2小时前
|
存储 缓存 NoSQL
【Go语言专栏】Go语言中的Redis操作与缓存应用
【4月更文挑战第30天】本文探讨了在Go语言中使用Redis进行操作和缓存应用的方法。文章介绍了Redis作为高性能键值存储系统,用于提升应用性能。推荐使用`go-redis/redis`库,示例代码展示了连接、设置、获取和删除键值对的基本操作。文章还详细阐述了缓存应用的步骤及常见缓存策略,包括缓存穿透、缓存击穿和缓存雪崩的解决方案。利用Redis和合适策略可有效优化应用性能。
|
2小时前
|
缓存 NoSQL 安全
Redis经典问题:缓存击穿
本文探讨了高并发系统中Redis缓存击穿的问题及其解决方案。缓存击穿指大量请求同一未缓存数据,导致数据库压力过大。为解决此问题,可以采取以下策略:1) 热点数据永不过期,启动时加载并定期异步刷新;2) 写操作加互斥锁,保证并发安全并设置查询失败返回默认值;3) 预期热点数据直接加缓存,系统启动时加载并设定合理过期时间;4) 手动操作热点数据上下线,通过界面控制缓存刷新。这些方法能有效增强系统稳定性和响应速度。
60 0