• nacos源码级别集群启动及拉取数据同步流程


    目录

    nacos集群启动

    配置数据库

    配置集群文件

    nacos启动配置

    nacos加载节点信息

    集群心跳健康检查机制

    节点状态同步

    数据新增及变更同步

    总结 


    nacos集群启动

    配置数据库

    首先在MySQL中创建数据库nacos(名称随意),在nacos数据库中执行config子工程中的nacos-db.sql文件

    然后在console子工程中,打开数据库的配置,因为nacos集群需要时外部数据库。

    1. #*************** Config Module Related Configurations ***************#
    2. ### If use MySQL as datasource:
    3. spring.datasource.platform=mysql
    4. ### Count of DB:
    5. db.num=1
    6. ### Connect URL of DB:
    7. db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    8. db.user.0=root
    9. db.password.0=123456

    配置集群文件

    在某个目录中创建三个文件

    文件目录及文件名称,注意在8846文件内部还要创建conf目录,不然启动报错:UnknowHostException。

    由于要启动三个节点,所以配置三个节点,文件内容如下:

    1. 192.168.1.88:8846
    2. 192.168.1.88:8847
    3. 192.168.1.88:8848

    nacos启动配置

    配置三个Spring Boot启动项,配置内容如图。配置完成后,就可以直接启动了。

    nacos加载节点信息

    nacos启动过程中,会加载ServerMemberManager这个bean,而他只有一个构造方法,所以会被spring调用,init()方法中主要是3件事:初始化该bean(节点)的属性;注册事件监听器,用来监听其他节点信息的变更;集群模式下读取配置文件信息。

    1. public ServerMemberManager(ServletContext servletContext) throws Exception {
    2. this.serverList = new ConcurrentSkipListMap<>();
    3. EnvUtil.setContextPath(servletContext.getContextPath());
    4. init();
    5. }
    6. protected void init() throws NacosException {
    7. //1、初始化该节点的属性
    8. Loggers.CORE.info("Nacos-related cluster resource initialization");
    9. this.port = EnvUtil.getProperty("server.port", Integer.class, 8848);
    10. this.localAddress = InetUtils.getSelfIP() + ":" + port;
    11. this.self = MemberUtil.singleParse(this.localAddress);
    12. this.self.setExtendVal(MemberMetaDataConstants.VERSION, VersionUtils.version);
    13. serverList.put(self.getAddress(), self);
    14. //2、注册事件监听器到NotifyManager
    15. // register NodeChangeEvent publisher to NotifyManager
    16. registerClusterEvent();
    17. //3、集群模式下读取配置文件信息(这里由几种方式)
    18. // Initializes the lookup mode
    19. initAndStartLookup();
    20. if (serverList.isEmpty()) {
    21. throw new NacosException(NacosException.SERVER_ERROR, "cannot get serverlist, so exit.");
    22. } Loggers.CORE.info("The cluster resource is initialized");
    23. }

    主要分析initAndStartLookup();调用链为lookup.start();—>doStart();—>FileConfigMemberLookup.doStart()—>readClusterConfFromDisk(),到这里就开始读取我们配置的节点内容,加载到内存后,会被解析为Member集合,然后调用ServerMemberManager#memberChange()方法,将信息添加到这个bean的集合属性中,如memberAddressInfos:维护所有"UP"状态的节点信息,serverList:集群所有节点。

    集群心跳健康检查机制

    前面已经分析过单节点nacos心跳逻辑是在服务注册的过程中启动心跳任务ClientBeatCheckTask,如果是nacos集群环境,在执行心跳任务之前会有如下判断,目的就是一个service只会由一个节点执行心跳任务,而不是所有节点

    1. if (!getDistroMapper().responsible(service.getName())) {
    2. return;
    3. }

    三个节点都会执行responsible()方法,但是在调用distroHash(serviceName),对节点个数求余后,只会有一个节点返回true。然后才会执行心跳任务

    1. public boolean responsible(String serviceName) {
    2. final List servers = healthyList;
    3. if (!switchDomain.isDistroEnabled() || EnvUtil.getStandaloneMode()) {
    4. return true;
    5. }
    6. if (CollectionUtils.isEmpty(servers)) {
    7. // means distro config is not ready yet
    8. return false;
    9. }
    10. int index = servers.indexOf(EnvUtil.getLocalAddress());
    11. int lastIndex = servers.lastIndexOf(EnvUtil.getLocalAddress());
    12. if (lastIndex < 0 || index < 0) {
    13. return true;
    14. }
    15. int target = distroHash(serviceName) % servers.size();
    16. return target >= index && target <= lastIndex;
    17. }
    18. private int distroHash(String serviceName) {
    19. return Math.abs(serviceName.hashCode() % Integer.MAX_VALUE);
    20. }

    Debug验证结果:注册两个不同的服务,在不重启的情况下,会分别固定由8846和8848这两个节点保持心跳。 debug断点是在ClientBeatCheckTask#run()中。

    节点状态同步

    在nacos启动中,会加载ServerListManager到spring容器中,它的init()方法被@PostConstruct注解了,所以会被执行。

    1. @PostConstruct
    2. public void init() {
    3. GlobalExecutor.registerServerStatusReporter(new ServerStatusReporter(), 2000);
    4. GlobalExecutor.registerServerInfoUpdater(new ServerInfoUpdater());
    5. }

    创建线程池每隔2s执行ServerStatusReporter任务,代码摘抄了重要部分,主要先从serverMemberManager对象中获取所有节点,遍历排除自己,发送状态数据到各个节点的/operator/server/status接口中。

    1. @Override
    2. public void run() {
    3. try {
    4. ...
    5. int weight = Runtime.getRuntime().availableProcessors() / 2;
    6. if (weight <= 0) {
    7. weight = 1;
    8. }
    9. long curTime = System.currentTimeMillis();
    10. String status = LOCALHOST_SITE + "#" + EnvUtil.getLocalAddress() + "#" + curTime + "#" + weight
    11. + "\r\n";
    12. // 获取所有的节点信息
    13. List allServers = getServers();
    14. ...
    15. //遍历
    16. if (allServers.size() > 0 && !EnvUtil.getLocalAddress()
    17. .contains(IPUtil.localHostIP())) {
    18. for (Member server : allServers) {
    19. //排除自己
    20. if (Objects.equals(server.getAddress(), EnvUtil.getLocalAddress())) {
    21. continue;
    22. }
    23. ...
    24. Message msg = new Message();
    25. msg.setData(status);
    26. //向接口/operator/server/status发送数据
    27. synchronizer.send(server.getAddress(), msg);
    28. }
    29. }
    30. } catch (Exception e) {
    31. Loggers.SRV_LOG.error("[SERVER-STATUS] Exception while sending server status", e);
    32. } finally {
    33. GlobalExecutor
    34. .registerServerStatusReporter(this, switchDomain.getServerStatusSynchronizationPeriodMillis());
    35. }
    36. }

    数据新增及变更同步

    在nacos服务启动中,会加载ServiceManager为spring的bean对象,执行init()方法,其中会创建定时任务线程池每隔1分钟执行ServiceReporter任务,他就是nacos各个节点间同步服务实例元数据的任务。一下是run()所有内容

    1. //从serviceMap中获取所有的serviceName,key:namespaceId,value:set
    2. Map> allServiceNames = getAllServiceNames();
    3. if (allServiceNames.size() <= 0) {
    4. //ignore
    5. return;
    6. }
    7. for (String namespaceId : allServiceNames.keySet()) {
    8. //创建需要同步的数据对象,它封装了namespaceId对应的service所有的实例信息
    9. ServiceChecksum checksum = new ServiceChecksum(namespaceId);
    10. //遍历serviceName集合,获取每个service所对应的全部实例信息,
    11. for (String serviceName : allServiceNames.get(namespaceId)) {
    12. //只有维持心跳的节点才会向checksum中添加数据
    13. if (!distroMapper.responsible(serviceName)) {
    14. continue;
    15. }
    16. Service service = getService(namespaceId, serviceName);
    17. if (service == null || service.isEmpty()) {
    18. continue;
    19. }
    20. //拼接所有实例信息,解析为md5赋值给checksum属性
    21. service.recalculateChecksum();
    22. //添加到checksum中
    23. checksum.addItem(serviceName, service.getChecksum());
    24. }
    25. //封装消息
    26. Message msg = new Message();
    27. msg.setData(JacksonUtils.toJson(checksum));
    28. //拿到所有nacos节点地址
    29. Collection sameSiteServers = memberManager.allMembers();
    30. if (sameSiteServers == null || sameSiteServers.size() <= 0) {
    31. return;
    32. }
    33. //将消息发送给除自身意外的所有nacos节点
    34. for (Member server : sameSiteServers) {
    35. if (server.getAddress().equals(NetUtils.localServer())) {
    36. continue;
    37. }
    38. synchronizer.send(server.getAddress(), msg);
    39. }
    40. }

    大致可以总结为将namespaceId对应的所有实例元数据信息,对于serviceName下所有实例信息,只有维持该serviceName心跳的节点才会对这元数据信息进行处理,将他们都加到一个checksum对象中,然后封装为Message对象中,最后发送给其他所有nacos节点。直到所有namespaceId都遍历结束。

    总结 

    1、集群环境维持每个service心跳的算法,对于一个服务类型会对他的serviceName进行hash,然后对集群节点数量求余,得到一个节点,该节点就是维持该服务类型所对应的所有实例。

    2、节点之间同步服务实例数据就是基于1中选出来的节点,每个节点会向其他节点同步自己维持心跳的服务的所有实例。

  • 相关阅读:
    【C语言】如何理解多级指针?
    排查内存过高的问题systemd-journald
    本地配置免费的https咋做?
    SSL证书也会失效?什么情况下SSL证书会失效呢?
    c 语言stdlib.h介绍
    计算机毕业设计(附源码)python中原图书供应商图书管理系统
    Java HashSet详解
    Maven高级(黑马学习笔记)
    Camunda 7.x 系列【54】管理服务 ManagementService
    对渗透新人的几点建议
  • 原文地址:https://blog.csdn.net/weixin_36279234/article/details/127083522