目录

nacos集群启动

配置数据库

配置集群文件

nacos启动配置

nacos加载节点信息

集群心跳健康检查机制

节点状态同步

数据新增及变更同步

总结 


nacos集群启动

配置数据库

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

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

#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
spring.datasource.platform=mysql

### Count of DB:
db.num=1

### Connect URL of DB:
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
db.user.0=root
db.password.0=123456

配置集群文件

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

nacos配置文件MySQL文件怎么使用 nacos配置数据源_加载

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

nacos配置文件MySQL文件怎么使用 nacos配置数据源_List_02

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

192.168.1.88:8846
192.168.1.88:8847
192.168.1.88:8848

nacos启动配置

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

nacos配置文件MySQL文件怎么使用 nacos配置数据源_数据库_03

nacos加载节点信息

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

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

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

集群心跳健康检查机制

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

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

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

public boolean responsible(String serviceName) {
        final List<String> servers = healthyList;
        
        if (!switchDomain.isDistroEnabled() || EnvUtil.getStandaloneMode()) {
            return true;
        }
        
        if (CollectionUtils.isEmpty(servers)) {
            // means distro config is not ready yet
            return false;
        }
        
        int index = servers.indexOf(EnvUtil.getLocalAddress());
        int lastIndex = servers.lastIndexOf(EnvUtil.getLocalAddress());
        if (lastIndex < 0 || index < 0) {
            return true;
        }
        
        int target = distroHash(serviceName) % servers.size();
        return target >= index && target <= lastIndex;
    }

    private int distroHash(String serviceName) {
        return Math.abs(serviceName.hashCode() % Integer.MAX_VALUE);
    }

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

nacos配置文件MySQL文件怎么使用 nacos配置数据源_java_04

节点状态同步

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

@PostConstruct
    public void init() {
        GlobalExecutor.registerServerStatusReporter(new ServerStatusReporter(), 2000);
        GlobalExecutor.registerServerInfoUpdater(new ServerInfoUpdater());
    }

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

@Override
        public void run() {
            try {
                ...
                int weight = Runtime.getRuntime().availableProcessors() / 2;
                if (weight <= 0) {
                    weight = 1;
                }
                long curTime = System.currentTimeMillis();
                String status = LOCALHOST_SITE + "#" + EnvUtil.getLocalAddress() + "#" + curTime + "#" + weight
                        + "\r\n";
                // 获取所有的节点信息
                List<Member> allServers = getServers();
                ...
                //遍历
                if (allServers.size() > 0 && !EnvUtil.getLocalAddress()
                        .contains(IPUtil.localHostIP())) {
                    for (Member server : allServers) {
                        //排除自己
                        if (Objects.equals(server.getAddress(), EnvUtil.getLocalAddress())) {
                            continue;
                        }
                        ...
                        Message msg = new Message();
                        msg.setData(status);
                        //向接口/operator/server/status发送数据
                        synchronizer.send(server.getAddress(), msg);
                    }
                }
            } catch (Exception e) {
                Loggers.SRV_LOG.error("[SERVER-STATUS] Exception while sending server status", e);
            } finally {
                GlobalExecutor
                        .registerServerStatusReporter(this, switchDomain.getServerStatusSynchronizationPeriodMillis());
            }
            
        }

数据新增及变更同步

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

//从serviceMap中获取所有的serviceName,key:namespaceId,value:set<serviceName>
                Map<String, Set<String>> allServiceNames = getAllServiceNames();
                
                if (allServiceNames.size() <= 0) {
                    //ignore
                    return;
                }
                
                for (String namespaceId : allServiceNames.keySet()) {
                    //创建需要同步的数据对象,它封装了namespaceId对应的service所有的实例信息
                    ServiceChecksum checksum = new ServiceChecksum(namespaceId);
                    //遍历serviceName集合,获取每个service所对应的全部实例信息,
                    for (String serviceName : allServiceNames.get(namespaceId)) {
                        //只有维持心跳的节点才会向checksum中添加数据
                        if (!distroMapper.responsible(serviceName)) {
                            continue;
                        }
                        Service service = getService(namespaceId, serviceName);
                        if (service == null || service.isEmpty()) {
                            continue;
                        }
                        //拼接所有实例信息,解析为md5赋值给checksum属性
                        service.recalculateChecksum();
                        //添加到checksum中
                        checksum.addItem(serviceName, service.getChecksum());
                    }
                    //封装消息
                    Message msg = new Message();
                    msg.setData(JacksonUtils.toJson(checksum));
                    //拿到所有nacos节点地址
                    Collection<Member> sameSiteServers = memberManager.allMembers();
                    if (sameSiteServers == null || sameSiteServers.size() <= 0) {
                        return;
                    }
                    //将消息发送给除自身意外的所有nacos节点
                    for (Member server : sameSiteServers) {
                        if (server.getAddress().equals(NetUtils.localServer())) {
                            continue;
                        }
                        synchronizer.send(server.getAddress(), msg);
                    }
                }

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

总结 

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

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