Nacos源码解读05——Client本地缓存和故障转移

Client本地缓存

ServiceInfoHolder功能概述

ServiceInfoHolder是服务信息的拥有者 ,比如服务注册,客户端从注册中心拉取服务新的服务信息时都会调用该类的。
processServiceInfo方法在前面的文章中出现过多次 他主要是来进行本地化的处理,包括更新缓存服务、发布事件、更新本地文件等操作。

ServiceInfo的本地内存缓存

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

public class ServiceInfo {
    
    @JsonIgnore
    private String jsonFromServer = EMPTY;
    
    private static final String EMPTY = "";
    
    private static final String ALL_IPS = "000--00-ALL_IPS--00--000";
    
    public static final String SPLITER = "@@";
    
    private static final String DEFAULT_CHARSET = "UTF-8";
    
    private String name;
    
    private String groupName;
    
    private String clusters;
    
    private long cacheMillis = 1000L;
    
    private List<Instance> hosts = new ArrayList<Instance>();
    
    private long lastRefTime = 0L;
    
    private String checksum = "";
    
    private volatile boolean allIPs = false;
    
    private volatile boolean reachProtectionThreshold = false;
    
    public ServiceInfo() {
    }
    
    public boolean isAllIPs() {
        return allIPs;
    }
    
    public void setAllIPs(boolean allIPs) {
        this.allIPs = allIPs;
    }
    }

ServiceInfoHolder 通过一个ConcurrentMap来维护着ServiceInfo信息在ServiceInfoHolder 构造方法初始化的时候会创建serviceInfoMap

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

当有服务发生变更时会调用processServiceInfo方法 在processServiceInfo中参考如下代码会往serviceInfoMap中添加数据缓存服务信息

        ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
        if (isEmptyOrErrorPush(serviceInfo)) {
            //empty or error push, just ignore
            return oldService;
        }
        serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);

本地缓存目录

当服务变更时会使用DiskCache往本地缓存目录中写入ServiceInfo信息

        if (changed) {
            NAMING_LOGGER.info("current ips:({}) service: {} -> {}", serviceInfo.ipCount(), serviceInfo.getKey(),
                    JacksonUtils.toJson(serviceInfo.getHosts()));
            NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),
                    serviceInfo.getClusters(), serviceInfo.getHosts()));
            DiskCache.write(serviceInfo, cacheDir);
        }

在ServiceInfoHolder构造方法初始化的时候会执行initCacheDir方法去构建一个本地缓存目录

    public ServiceInfoHolder(String namespace, Properties properties) {
        initCacheDir(namespace, properties);
         ......
    }

默认缓存目录为${user.home}/nacos/naming/public,可以通过System.setProperty(“JM.SNAPSHOT.PATH”)自定义根目录。当这个目录初始化完成之后 故障转移信息也存储在该目录下

    private String cacheDir;
    
    private void initCacheDir(String namespace, Properties properties) {
        String jmSnapshotPath = System.getProperty(JM_SNAPSHOT_PATH_PROPERTY);
    
        String namingCacheRegistryDir = "";
        if (properties.getProperty(PropertyKeyConst.NAMING_CACHE_REGISTRY_DIR) != null) {
            namingCacheRegistryDir = File.separator + properties.getProperty(PropertyKeyConst.NAMING_CACHE_REGISTRY_DIR);
        }
        
        if (!StringUtils.isBlank(jmSnapshotPath)) {
            cacheDir = jmSnapshotPath + File.separator + FILE_PATH_NACOS + namingCacheRegistryDir
                    + File.separator + FILE_PATH_NAMING + File.separator + namespace;
        } else {
            cacheDir = System.getProperty(USER_HOME_PROPERTY) + File.separator + FILE_PATH_NACOS + namingCacheRegistryDir
                    + File.separator + FILE_PATH_NAMING + File.separator + namespace;
        }
    }

故障转移

作用

当开启故障转移后当发生故障时,可以从故障转移定时备份的文件中来获得服务实例信息

实现

ServiceInfoHolder的构造方法初始化的时候会创建一个FailoverReactor这个类是专门去处理故障转移的

 public ServiceInfoHolder(String namespace, Properties properties) {
  ......
   this.failoverReactor = new FailoverReactor(this, cacheDir);
  ......
  }
    public FailoverReactor(ServiceInfoHolder serviceInfoHolder, String cacheDir) {
        this.serviceInfoHolder = serviceInfoHolder;
          // 拼接故障根目录:${user.home}/nacos/naming/public/failover
        this.failoverDir = cacheDir + FAILOVER_DIR;
        // init 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();
    }
    public void init() {
        //执行间隔5秒,执行任务为SwitchRefresher
        executorService.scheduleWithFixedDelay(new SwitchRefresher(), 0L, 5000L, TimeUnit.MILLISECONDS);
        //初始化延迟30分钟执行,执行间隔24小时,执行任务为DiskFileWriter;
        executorService.scheduleWithFixedDelay(new DiskFileWriter(), 30, DAY_PERIOD_MINUTES, TimeUnit.MINUTES);
        
        // backup file on startup if failover directory is empty.
        //初始化立即执行,执行间隔10秒,执行核心操作为DiskFileWriter;
        executorService.schedule(new Runnable() {
            @Override
            public void run() {
                try {
                    File cacheDir = new File(failoverDir);
                    
                    if (!cacheDir.exists() && !cacheDir.mkdirs()) {
                        throw new IllegalStateException("failed to create cache dir: " + failoverDir);
                    }
                    
                    File[] files = cacheDir.listFiles();
                    if (files == null || files.length <= 0) {
                        new DiskFileWriter().run();
                    }
                } catch (Throwable e) {
                    NAMING_LOGGER.error("[NA] failed to backup file on startup.", e);
                }
                
            }
        }, 10000L, TimeUnit.MILLISECONDS);
    }

SwitchRefresher执行

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

    @Override
        public void run() {
            try {
                File switchFile = new File(failoverDir + UtilAndComs.FAILOVER_SWITCH);
                //判断文件是否存在 不存在直接return 
                if (!switchFile.exists()) {
                    switchParams.put(FAILOVER_MODE_PARAM, Boolean.FALSE.toString());
                    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());
                    }
                }
                
            } catch (Throwable e) {
                NAMING_LOGGER.error("[NA] failed to read failover switch.", e);
            }
        }
    }

DiskFileWriter执行

这里就是回判断是否满足写入磁盘文件如果满足则将服务信息写入前面拼接的故障转移目录:${user.home}/nacos/naming/public/failover。只不过第二个定时任务和第三个定时任务的区别时,第三个定时任务有前置判断,只有当文件不存在时才执行。

    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);
            }
        }
    }

FailoverFileReader

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

    class FailoverFileReader implements Runnable {
        
        @Override
        public void run() {
            Map<String, ServiceInfo> domMap = new HashMap<String, ServiceInfo>(16);
            
            BufferedReader reader = null;
            try {
                //拿故障文件
                File cacheDir = new File(failoverDir);
                if (!cacheDir.exists() && !cacheDir.mkdirs()) {
                    throw new IllegalStateException("failed to create cache dir: " + failoverDir);
                }
                
                File[] files = cacheDir.listFiles();
                if (files == null) {
                    return;
                }
                //遍历
                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 {
                               //构建SeriviceInfo
                                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
                        }
                    }
                    //将dom放到临时存储的map中
                    if (!CollectionUtils.isEmpty(dom.getHosts())) {
                        domMap.put(dom.getKey(), dom);
                    }
                }
            } catch (Exception e) {
                NAMING_LOGGER.error("[NA] failed to read cache file", e);
            }
            //当domMap 的Size 大于0 恢复serviceMap 缓存
            if (domMap.size() > 0) {
                serviceMap = domMap;
            }
        }
    }

代码基本流程如下:

读取failover目录下的所有文件,进行遍历处理;
如果文件不存在,跳过;
如果文件是故障转移标志文件,跳过;
读取文件中的json内容,转化为ServiceInfo对象;
将ServiceInfo对象放入domMap当中;
当domMap的Size大于0将domMap赋值给serviceMap达到服务的缓存恢复

参考链接

https://zhuanlan.zhihu.com/p/400891365

你可能感兴趣的:(Nacos,java)