Nacos注册中心客户端与服务端源码分析

Nacos作为SpringCloud Alibaba (SCA)中注册中心以及配置中心的组件。这里主要分析Nacos客户端与服务端之间注册,下线,心跳续约源码以及对Ribbon负载均衡的Nacos的具体实现。

核心对外提供API接口类:

Nacos注册中心客户端与服务端源码分析_第1张图片

 

对于客户端的使用其实很简单,只要引入spring-cloud-starter-alibaba-nacos-discovery包即可。这个包是一个springboot start项目,所以查看它的源码在

Nacos注册中心客户端与服务端源码分析_第2张图片

上述的每一个AutoConfiguration都有自己需要初始化的对象例如

  1. NacosDiscoveryAutoConfiguration:用于初始化NacosDiscoveryProperties(存储配置属性类),NacosServiceDiscovery(提供对nacos中service以及Instance实例访问具体实现)
  2. RibbonNacosAutoConfiguration:用于初始化Ribbon中核心ServerList类,以及自己实现NacosServerIntrospector,其中Ribbon依赖组件进行初始化(IRule,IPing等)还是在RibbonAutoConfiguration中被创建
  3. NacosDiscoveryEndpointAutoConfiguration:对外提供端点信息(包含Health健康检查),需要依赖于actuator项目
  4. NacosServiceRegistryAutoConfiguration:这里是对核心类一些类进行初始化,例如NacosServiceRegistry(包含创建后续与nacos服务端交互的核心类NamingService-实现NacosNamingService)负责客户端的注册,下线心跳续约检测,NacosAutoServiceRegistration负责触发上述动作

客户端核心类:

  1. NacosNamingService:client发起的后续的所有任务都是基于此类或者此类中包含的组件类来进行,NacosNamingService(该类由NamingFactory工厂类进行创建)通过读取Properties进行初始化

    Nacos注册中心客户端与服务端源码分析_第3张图片

  2. BeatReactor:客户端与服务端周期心跳检测类,内部定义ScheduledThreadPoolExecutor周期调度器,创建名为com.alibaba.nacos.naming.beat.sender线程周期的执行BeatTask任务(该类为BeatReactor中的一个内部类用于向服务端发送心跳信息,最终通过httpclient发送路径为/instance/beat的http请求),内部维护以serviceName+groupName+ip+host为key,BeatInfo为value的map,当client初始化向服务端注册实例时会创建一个BeatInfo对象,通过BeatReactor中addBeatInfo()函数写入,并在beatInfo属性period(默认5s)后调度一次BeatTask。关于心跳检测可以参考后续对心跳机制描述

  3. HostReactor:客户端周期去拉取服务端代码,内部定义ScheduledThreadPoolExecutor周期调度器,创建名为com.alibaba.nacos.client.naming.updater线程周期的执行UpdateTask任务(该类为HostReactor中的一个内部类用于更新client中缓存的服务注册列表信息,在获取列表的同时,告诉服务度它的udp端口号信息,服务端生成对应的PushClient对象,一旦服务端中对应的Service信息发生来变更,服务端可以通过PushClient进行发送变更信息。UpdateTask以Service-cluster组合为单位来周期更新的,更新频率默认1s可设置。通过updateServiceNow()发送http请求-/instance/list)

  4. EventDispatcher:事件分发器 用于管理EventListener。内部定义ScheduledThreadPoolExecutor周期调度器,创建名为com.alibaba.nacos.naming.client.listener线程周期的执行Notifier任务(通过Notifier向注册的EventListener中发生NamingEvent事件,可用于本地扩展(实现ApplicationListener接口监控NamingEvent事件))

  5. PushReceiver:用于接受服务端发送来的ACK数据并进行与本地信息对比更新,最后返回服务端ack信息,该类初始化时创建一个udpSocket,用于与服务端数据通信,定义ScheduledThreadPoolExecutor创建前缀名为com.alibaba.nacos.naming.push.receiver的调度器,用于执行PushReceiver(该类本身实现了Runnable接口)。

    //执行run源码
     @Override
        public void run() {
            while (true) {
                try {
                    // byte[] is initialized with 0 full filled by default
                    //定义
                    byte[] buffer = new byte[UDP_MSS];
                    DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
    
                    //接受服务端发送的udp请求
                    udpSocket.receive(packet);
    
                    //解析数据
                    String json = new String(IoUtils.tryDecompress(packet.getData()), "UTF-8").trim();
                    NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
                    //转化PushPacket
                    PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
                    String ack;
                    //感觉不同数据类型 返回不同ack信息
                    if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
                        //处理请求 更新本地本地Service信息
                        hostReactor.processServiceJSON(pushPacket.data);
    
                        // send ack to server
                        ack = "{\"type\": \"push-ack\""
                            + ", \"lastRefTime\":\"" + pushPacket.lastRefTime
                            + "\", \"data\":" + "\"\"}";
                    } else if ("dump".equals(pushPacket.type)) {
                        // dump data to server
                        ack = "{\"type\": \"dump-ack\""
                            + ", \"lastRefTime\": \"" + pushPacket.lastRefTime
                            + "\", \"data\":" + "\""
                            + StringUtils.escapeJavaScript(JacksonUtils.toJson(hostReactor.getServiceInfoMap()))
                            + "\"}";
                    } else {
                        // do nothing send ack only
                        ack = "{\"type\": \"unknown-ack\""
                            + ", \"lastRefTime\":\"" + pushPacket.lastRefTime
                            + "\", \"data\":" + "\"\"}";
                    }
    
                    //重新发送给原服务
                    udpSocket.send(new DatagramPacket(ack.getBytes(Charset.forName("UTF-8")),
                        ack.getBytes(Charset.forName("UTF-8")).length, packet.getSocketAddress()));
                } catch (Exception e) {
                    NAMING_LOGGER.error("[NA] error while receiving push data", e);
                }
            }
        }

     

 

服务端通过InstanceController接受到/instance/list路径请求,做了什么事情呢?接受client请求数据,获取存在的Service信息(可能存在访问的服务器上还未同步到Service信息未null,若出现直接返回),如果携带中参数包含udp port并且服务端开启了push机制(默认开启但是可以通过配置关闭),通过PushService中addClient()函数创建该请求与Service连接对象(PushClient信息,),并存储在key为namespaceId, serviceName构建,value为PushClient的缓存中(后续通过心跳保持该PuchClient连接),用于后续通知。组装最新Service信息返回(中间需要对Service进行校验)。

 

 

​4:服务端核心代码:

数据模型有关

  1. Instance:nacos数据模型中最小存储单位,主要由ip+port来确定唯一性,一个client对应一个instance

  2. Cluster:集群由相同的配置的Instance构成,内部存储了该集群下persistentInstances与ephemeralInstances。Cluster实例完成之后通过HealthCheckTask来检测所属Instance活性

  3. Service:代表一个服务实例,由多个Cluster构,实现了RecordListener接口,当服务下的实例集群发生变化(增删该,通过事件驱动来解耦),触发其onChange事件,更新本服务器实例信息之后,同时通过PushService来推送注册到其实例下的client变更的节点信息

  4. namespace:命名空间,可以使用它来管理一个注册中心管理多个环境(开发,测试(不同环境),生产)等

数据一致性有关

Nacos中提供的两种一致性算法的实现:CP的Raft与AP的distro,这两种算法分别针对于临时节点与持久性节点的存储,nacos中默认节点类型为临时节点。

Nacos注册中心客户端与服务端源码分析_第4张图片

 nacos中通过DelegateConsistencyServiceImpl类(静态代理设计模式,包含两种具体策略实现),代理执行具体的数据(按照节点的类型)写入

Nacos注册中心客户端与服务端源码分析_第5张图片

具体算法实现: 

  1. DistroConsistencyServiceImpl:AP模型体现,所有节点都是对等的(nacos这里是参考了Eureka服务的原理),负责管理ephemeral实例信息(数据存储在内存中,由DataStore存储,初始化时会执行一次LoadDataTask任务来同步其它服务上的已存储的ephemeral实例信息,通过定期执行同步任务LoadDataTask,与其它节点做到最终数据统一,一旦实例数据发生变化触发通知机制,不仅通知其它节点也会push发送客户端,也通过TaskDispatcher来添加任务最终与其它节点进行同步) .内部包含一个TaskDispatcher来管理一批TaskScheduler的执行,而TaskScheduler通过BlockingQueue来存储数据变更同步到其它服务节点任务。最终使得集群中临时节点实例信息达到最终一致。

  2. RaftConsistencyServiceImpl:CP模型体现,这里nacos对Raft算法的实现。除了leader写入后,follower过半数统一后再写入,follower也会通过leader心跳定期同步数据。负责保存persistent实例信息(数据存储在本地文件磁盘中,由RaftStore存储,实际上在RaftCore中会存在一份缓存数据用于读,避免频繁读写磁盘,写的动作在写入磁盘时刷新缓存)

数据管理相关:

  1. ServiceStatusSynchronizer:同步器,本质都是通过发送其它节点http请求来获取或发送数据。该同步器就2个方法-send发送(/service/status),get获取(获取目标服务器最新Service信息/instance/statuses)

  2. ServiceManage:nacos服务端用于管理Service信息,实现 RecordListener接口,当Service发生变更(增删改,通过事件驱动来解耦),触发其onChange或onDelete事件(例如DistroConsistencyServiceImpl初始化时,如果是临时节点会触发),初始化时延时执行一次ServiceReporter任务来报告各服务的状态信息,一次UpdatedServiceProcessor任务。根据配置来决定是否开启清除空Service的EmptyServiceAutoClean任务(一个延时60s,周期20s执行),该任务尽量不要频繁触发,以免由于心跳机制而导致服务缓存信息可能被删除然后再次创建Service实例。通过ServiceManage中大量的周期同步任务,来保证服务集群中对等节点的数据最终一致性。

    1. PushService:用于向各订阅的客户端(管理所有注册过的PushClient信息)通过udp通信协议发送Service变更信息,内部包含多个ScheduledExecutorService执行器:名为com.alibaba.nacos.naming.push.retransmitter处理两种任务:并开启周期为20s,延时事件为0s的定期清除无效pushClient任务;名为com.alibaba.nacos.naming.push.udpSender:udp通信协议发送Service变更信息。名为com.alibaba.nacos.naming.push.receiver线程类Receiver用于接受接受client的ACK结果。通过监听ServiceChangeEvent事件触发onApplicationEvent函数最终通过udpSender异步进行状态同步。该类与Client中的PushReceiver相对应.

    2. ServiceReporter:来向其它对等服务报告各服务的信息状态,这个属于服务端之间的心跳机制,通过ServiceStatusSynchronizer来发送,初始化延时60s执行一次后续通过周期(可通过serviceStatusSynchronizationPeriodMillis配置默认5s)来,报告的目标是除本身外的其它对等服务器节点。

    3. UpdatedServiceProcessor:该任务用于从其他服务器异步获取更改的服务-每收到一次ServiceReporter任务都会通过ServiceStatusSynchronizer同步器来获取其发送服务节点下该Service下相同的实例,判断是否发生来health变更,若发生变更则将最新状态通知订阅的client

    4. EmptyServiceAutoClean:该任务用于清除失效的Service数据

  3. nacos对客户端提供的API接口:

    Nacos注册中心客户端与服务端源码分析_第6张图片

  4.  

 

 

1:客户端获取nacos集群服务信息

nacos client拉取Server服务信息由NamingProxy来实现,用它来代理client通过httpclient访问nacos服务。nacos提供了两种方式获取,第一种:通过配置文件中nacos.discovery.server-addr=xxx,xxx,xxx属性来设置,这种方式后续客户端不会再通过定时任务获取来 第二种:通过配置文件nacos.discovery.enpoint设置,服务的域名,通过它可以动态获取服务器地址。增强endpoint的功能。在endpoint端配置网段和环境的映射关系,endpoint在接收到客户端的请求(实际上就是http请求)之后,根据客户端的来源IP所属网段,计算出该客户端所属环境,然后找到对应环境的IP列表返回给客户端。对于endpoint描述可以参考https://nacos.io/zh-cn/blog/address-server.html 官网对于这一块描述, 其中若设置endpoint则server-addr无效。我们说的拉取Nacos集群的任务就是这针对于enpoint。

如下所示:

 

Nacos注册中心客户端与服务端源码分析_第7张图片

Nacos注册中心客户端与服务端源码分析_第8张图片

Nacos注册中心客户端与服务端源码分析_第9张图片

2:上线

2.1客户端:


NacosAutoServiceRegistration该类继承于AbstractAutoServiceRegistration(实现了ApplicationListener接口对WebServerInitializedEvent事件进行监控),当服务初始化完成后,因为NacosAutoServiceRegistration重写register函数先进行系统检测。最终还是通过NacosServiceRegistry(携带当前实例的注册表下线NacosRegistration)进行服务注册。如下所示

Nacos注册中心客户端与服务端源码分析_第10张图片

Nacos注册中心客户端与服务端源码分析_第11张图片

 

Nacos注册中心客户端与服务端源码分析_第12张图片

 

Nacos注册中心客户端与服务端源码分析_第13张图片

注: 由ephemeral决定,该实例类型(默认为临时节点)。

Nacos注册中心客户端与服务端源码分析_第14张图片

 

Nacos注册中心客户端与服务端源码分析_第15张图片

这里为何是随机的原因在于,对数据的分片存储,即任意的Service下的instance随机分片落到集群中(这里该server以service角色对这个instance进行管理,数据信息通过离线同步方式进行集群类同步),这里构建节点的类型有不同的措施,不同的一致性算法有不同的持久化实现,后续代码详细说明。到这里客户端发起注册请求结束。

2.2 服务端:

服务端的对外提供API接口类为InstanceController

通过InstanceController找到ServiceManager。这里可以参考Nacos的模型https://nacos.io/zh-cn/docs/architecture.html

Nacos注册中心客户端与服务端源码分析_第16张图片

通用调用createEmptService用于创建一个空service 这时候并不添加instance。这里创建成功后会开启Service下的ClientBeatCheckTask监听任务,监听该Service临时节点(这里临时节点会分片保存)的变更。

Nacos注册中心客户端与服务端源码分析_第17张图片

这里的监听很重要 一旦Service中Instance数据集发送变化,会触发Service下对应的onChange事件,修改服务列表数据

Nacos注册中心客户端与服务端源码分析_第18张图片

因为是持久化节点所以这里通过Raft算法来写入:

 

Nacos注册中心客户端与服务端源码分析_第19张图片

/**
     * 若节点为leader数据写入磁盘 否则转发到leader中由leader写入
     * @param key
     * @param value
     * @throws Exception
     */
    public void signalPublish(String key, Record value) throws Exception {

        if (!isLeader()) {
            //不为leader
            ObjectNode params = JacksonUtils.createEmptyJsonNode();
            params.put("key", key);
            params.replace("value", JacksonUtils.transferToJsonNode(value));
            Map parameters = new HashMap<>(1);
            parameters.put("key", key);

            final RaftPeer leader = getLeader();
            //转发到leader节点 由leader节点统一落盘处理 /raft/datum
            raftProxy.proxyPostLarge(leader.ip, API_PUB, params.toString(), parameters);
            return;
        }

        try {
            //自己是leader
            //加锁 保证写入流正常
            OPERATE_LOCK.lock();
            long start = System.currentTimeMillis();
            //封装新的datum
            final Datum datum = new Datum();
            datum.key = key;
            datum.value = value;
            //是否已存在旧Datum
            if (getDatum(key) == null) {
                datum.timestamp.set(1L);
            } else {
                //数据变更版本号
                datum.timestamp.set(getDatum(key).timestamp.incrementAndGet());
            }

            ObjectNode json = JacksonUtils.createEmptyJsonNode();
            json.replace("datum", JacksonUtils.transferToJsonNode(datum));
            json.replace("source", JacksonUtils.transferToJsonNode(peers.local()));
            //将datum写入磁盘中
            onPublish(datum, peers.local());

            final String content = json.toString();
            //计算发送给各follower时间
            final CountDownLatch latch = new CountDownLatch(peers.majorityCount());
            //包含
            for (final String server : peers.allServersIncludeMyself()) {
                if (isLeader(server)) {
                    //不发送自己 因为本身已经保存
                    latch.countDown();
                    continue;
                }
                //构建请求url 这里调用/raft/datum/commit 各服务落地数据
                final String url = buildURL(server, API_ON_PUB);
                //异步发送数据 最终还是调用RaftCore.onPublish()方法
                HttpClient.asyncHttpPostLarge(url, Arrays.asList("key=" + key), content, new AsyncCompletionHandler() {
                    @Override
                    public Integer onCompleted(Response response) throws Exception {
                        if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
                            Loggers.RAFT.warn("[RAFT] failed to publish data to peer, datumId={}, peer={}, http code={}",
                                datum.key, server, response.getStatusCode());
                            return 1;
                        }
                        latch.countDown();
                        return 0;
                    }

                    @Override
                    public STATE onContentWriteCompleted() {
                        return STATE.CONTINUE;
                    }
                });

            }

            //定义发送各服务器超时时间
            if (!latch.await(UtilsAndCommons.RAFT_PUBLISH_TIMEOUT, TimeUnit.MILLISECONDS)) {
                //只有大多数服务器返回成功,我们才能认为此更新成功
                // only majority servers return success can we consider this update success
                Loggers.RAFT.error("data publish failed, caused failed to notify majority, key={}", key);
                throw new IllegalStateException("data publish failed, caused failed to notify majority, key=" + key);
            }

            long end = System.currentTimeMillis();
            Loggers.RAFT.info("signalPublish cost {} ms, key: {}", (end - start), key);
        } finally {
            OPERATE_LOCK.unlock();
        }
    }

非leader节点会将数据变更请求转发到RaftController(该接口用于leader与follower之间关于Raft的具体实现)对外API中处理

 /**
     * 处理非leader转发的数据put请求
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @PostMapping("/datum")
    public String publish(HttpServletRequest request, HttpServletResponse response) throws Exception {

        response.setHeader("Content-Type", "application/json; charset=" + getAcceptEncoding(request));
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("Content-Encode", "gzip");

        String entity = IoUtils.toString(request.getInputStream(), "UTF-8");
        String value = URLDecoder.decode(entity, "UTF-8");
        JsonNode json = JacksonUtils.toObj(value);

        String key = json.get("key").asText();
        //根据请求类型 落地不同数据 本质还是调用raftCore.signalPublish
        if (KeyBuilder.matchInstanceListKey(key)) {
            raftConsistencyService.put(key, JacksonUtils.toObj(json.get("value").toString(), Instances.class));
            return "ok";
        }

        if (KeyBuilder.matchSwitchKey(key)) {
            raftConsistencyService.put(key, JacksonUtils.toObj(json.get("value").toString(), SwitchDomain.class));
            return "ok";
        }

        if (KeyBuilder.matchServiceMetaKey(key)) {
            raftConsistencyService.put(key, JacksonUtils.toObj(json.get("value").toString(), Service.class));
            return "ok";
        }

        throw new NacosException(NacosException.INVALID_PARAM, "unknown type publish key: " + key);
    }

转发leader成功之后就由leader通过调用/raft/datum/commit接口,使各服务节点同步数据。

 @PostMapping("/datum/commit")
    public String onPublish(HttpServletRequest request, HttpServletResponse response) throws Exception {

        response.setHeader("Content-Type", "application/json; charset=" + getAcceptEncoding(request));
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("Content-Encode", "gzip");

        String entity = IoUtils.toString(request.getInputStream(), "UTF-8");
        String value = URLDecoder.decode(entity, "UTF-8");

        JsonNode jsonObject = JacksonUtils.toObj(value);
        String key = "key";

        RaftPeer source = JacksonUtils.toObj(jsonObject.get("source").toString(), RaftPeer.class);
        JsonNode datumJson = jsonObject.get("datum");

        Datum datum = null;
        if (KeyBuilder.matchInstanceListKey(datumJson.get(key).asText())) {
            datum = JacksonUtils.toObj(jsonObject.get("datum").toString(), new TypeReference>() {
            });
        } else if (KeyBuilder.matchSwitchKey(datumJson.get(key).asText())) {
            datum = JacksonUtils.toObj(jsonObject.get("datum").toString(), new TypeReference>() {
            });
        } else if (KeyBuilder.matchServiceMetaKey(datumJson.get(key).asText())) {
            datum = JacksonUtils.toObj(jsonObject.get("datum").toString(), new TypeReference>() {
            });
        }
        //上面都是处理请求数据 转化为datum
        raftConsistencyService.onPut(datum, source);
        return "ok";
    }

最终核心还是调用Raft中onPublish写入。创建Service成功后会将instace写入到Service中。Instanc根据节点类型决定对应的一致性算法来定。

public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips) throws NacosException {

        //构建instance对应的key 
        String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
        //获取service
        Service service = getService(namespaceId, serviceName);

        //同步加锁 朝service中添加instace
        synchronized (service) {
            //获取添加instace之后的 service列表(以二级缓存为主)
            List instanceList = addIpAddresses(service, ephemeral, ips);

            Instances instances = new Instances();
            instances.setInstanceList(instanceList);
            //根据一致性算法的不同最新的service列表写入到集群中
            consistencyService.put(key, instances);
        }
    }

因为Instance交由Service管理,所以Instance的变化通过Service的onChange来管理包含(change与del),nacos服务端通过Service类添加listener来监听instance数据变更,一旦数据变更触发Service下onChange方法来推送各client节点列表更新(后续的节点订阅详细叙说)。

 

 

3:下线

3.1:客户端

与上线类似,在AbstractAutoServiceRegistration(NacosAutoServiceRegistration继承之)中

Nacos注册中心客户端与服务端源码分析_第20张图片

若NacosAutoServiceRegistration对象被spring容器销毁之后,调用destroy()方法

Nacos注册中心客户端与服务端源码分析_第21张图片

Nacos注册中心客户端与服务端源码分析_第22张图片

Nacos注册中心客户端与服务端源码分析_第23张图片

 

Nacos注册中心客户端与服务端源码分析_第24张图片

 

Nacos注册中心客户端与服务端源码分析_第25张图片

3.2:服务端

通过InstaceController来接受信息。

Nacos注册中心客户端与服务端源码分析_第26张图片

Nacos注册中心客户端与服务端源码分析_第27张图片

4:心跳检测

4.1:客户端

如果是临时实例,则不会在 Nacos 服务端持久化存储,需要通过上报心跳的方式进行包活,如果一段时间内没有上报心跳,则会被 Nacos 服务端摘除。在被摘除后如果又开始上报心跳,则会重新将这个实例注册。持久化实例则会持久化被 Nacos 服务端,此时即使注册实例的客户端进程不在,这个实例也不会从服务端删除,只会将健康状态设为不健康。之前服务的健康检查模式有三种:client、server 和none, 分别代表客户端上报、服务端探测和取消健康检查。在1.0之后由ephemeral指定,若ephemeral为true代表通过client来通过心跳检测,若为false代表由nacos服务进行探测。通过BeatReactor类中的定时任务定时执行。

当一个instance进行注册的时候,通过BeatReactor添加一个心跳检测任务

            beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);

 BeatReactor该类从命名上可以看出采用了反应核设计模式,由他进行BeatTask的分配调度。

添加一个BeatInfo

Nacos注册中心客户端与服务端源码分析_第28张图片

​executorService中执行的BeatTask详解:

Nacos注册中心客户端与服务端源码分析_第29张图片

客户端会携带这个实例的Meat数据通过服务端中InstanceController提供API接口进行通信,服务端中解析的核心代码:

Nacos注册中心客户端与服务端源码分析_第30张图片

Nacos注册中心客户端与服务端源码分析_第31张图片

上述的异步线程为ClientBeatProcessor,如下所示为其核心代码:

Nacos注册中心客户端与服务端源码分析_第32张图片

 客户端收到服务端对心跳影响对请求之后,解析result结果,判断是否需要重新注册后,通过executorService进行延时调度(这个延时的时间以客户端设定为主HEART_BEAT_INTERVAL配置默认5s,若此次请求有问题 以默认的switchDomain中时间为主默认5s)

 

4.2:服务端

服务端健康检测由多个地方触发:

1:Service中通过ClientBeatCheckTask(每5s执行一次)检测该Service下所有非持久化实例信息的活性,剔除那些僵尸节点(这里只会处理非持久化节点,对于非持久化节点对应的Service是分片处理的,每一个非持久化实例信息在由注册的时候选定的nacos节点进行心跳检测,也存储在这个nacos Service中,但实例数据会通过leader转发到RaftCore中缓存中存储,不会落盘),若当前时间距离上一次心跳检测时间超过15s(默认)标为非健康并触发消息通知机制,告知各服务端节点列表变更(这属于后续的节点订阅内容),这里在nacos1.3版本为何在发送为使用nio来发送,这里可以进行优化。检测是否超过30s来自动剔除该实例(由全局设置nacos.naming.expireInstance 默认为true,这里的剔除通过http调用本地的InstanceController中删除接口(先整理本地service对应新实例列表,交由具体的一致性算法来重新写入)

源码:


/**
 * Check and update statues of ephemeral instances, remove them if they have been expired.
 * 检查并更新临时实例的状态,如果它们已过期则将其删除。
 * @author nkorange
 */
public class ClientBeatCheckTask implements Runnable {

    private Service service;

    public ClientBeatCheckTask(Service service) {
        this.service = service;
    }

    @JsonIgnore
    public PushService getPushService() {
        return ApplicationUtils.getBean(PushService.class);
    }

    @JsonIgnore
    public DistroMapper getDistroMapper() {
        return ApplicationUtils.getBean(DistroMapper.class);
    }

    //全局配置 在application设置
    public GlobalConfig getGlobalConfig() {
        return ApplicationUtils.getBean(GlobalConfig.class);
    }

    public SwitchDomain getSwitchDomain() {
        return ApplicationUtils.getBean(SwitchDomain.class);
    }

    public String taskKey() {
        return KeyBuilder.buildServiceMetaKey(service.getNamespaceId(), service.getName());
    }

    @Override
    public void run() {
        try {
            if (!getDistroMapper().responsible(service.getName())) {
                return;
            }

            //是否开启对service下实例下心跳检测
            if (!getSwitchDomain().isHealthCheckEnabled()) {
                return;
            }
            //获取所有非持久化节点
            List instances = service.allIPs(true);

            // first set health status of instances:
            //第一次判断实例运行情况
            for (Instance instance : instances) {
                //判断实例有没有进行心跳检测默认15s
                if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
                    //没被marked 并且 上一次实例正常 修改healthy
                    if (!instance.isMarked()) {
                        if (instance.isHealthy()) {
                            instance.setHealthy(false);
                            //日志记录
                            Loggers.EVT_LOG.info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client timeout after {}, last beat: {}",
                                instance.getIp(), instance.getPort(), instance.getClusterName(), service.getName(),
                                UtilsAndCommons.LOCALHOST_SITE, instance.getInstanceHeartBeatTimeOut(), instance.getLastBeat());
                            //自身监听该事件(这里通过事件驱动设计模式进行解耦) 可能发生实例变化 推送service下实例信息给各订阅者
                            getPushService().serviceChanged(service);
                            //发布InstanceHeartbeatTimeoutEvent事件
                            ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
                        }
                    }
                }
            }

            //是否自动摘除临时实例	默认为true
            if (!getGlobalConfig().isExpireInstance()) {
                return;
            }

            //自动摘除过时的 临时实例
            // then remove obsolete instances:
            for (Instance instance : instances) {

                //被标记了 不做处理
                if (instance.isMarked()) {
                    continue;
                }
                //判断超时事件是否到了超时时间 默认30s  这里可能存在网络波动导致的第一次检测失败 所以这里剔除时间是healthchek的2倍
                if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {
                    // delete instance
                    Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(), JacksonUtils.toJson(instance));
                    //删除实例
                    deleteIP(instance);
                }
            }

        } catch (Exception e) {
            Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
        }

    }


    /**
     * 删除实例
     * @param instance
     */
    private void deleteIP(Instance instance) {

        try {
            //构建请求参数
            NamingProxy.Request request = NamingProxy.Request.newRequest();
            request.appendParam("ip", instance.getIp())
                .appendParam("port", String.valueOf(instance.getPort()))
                .appendParam("ephemeral", "true")
                .appendParam("clusterName", instance.getClusterName())
                .appendParam("serviceName", service.getName())
                .appendParam("namespaceId", service.getNamespaceId());

            //集群删除instance实例
            String url = "http://127.0.0.1:" + ApplicationUtils.getPort() + ApplicationUtils.getContextPath()
                + UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance?" + request.toUrl();

            // delete instance asynchronously:
            //异步删除实例
            HttpClient.asyncHttpDelete(url, null, null, new AsyncCompletionHandler() {
                @Override
                public Object onCompleted(Response response) throws Exception {
                    if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
                        Loggers.SRV_LOG.error("[IP-DEAD] failed to delete ip automatically, ip: {}, caused {}, resp code: {}",
                            instance.toJSON(), response.getResponseBody(), response.getStatusCode());
                    }
                    return null;
                }
            });

        } catch (Exception e) {
            Loggers.SRV_LOG.error("[IP-DEAD] failed to delete ip automatically, ip: {}, error: {}", instance.toJSON(), e);
        }
    }
}

其它集群通过该类去接受这些ACK信息并做出响应的呢,PushReceiver做到了,

 

2:cluster(cluster是逻辑上的存储方式,可能一个service集群中存在多种不同的应用环境-例如不同的机架这里就会产生不同的配置实例cluster,例如A机架上3台service服务,B机架上有2台service服务,那么这里一个service中就存在5个实例 2个cluster)中。cluster中通过HealthCheckTask任务来进行心跳检测,此对象只会在Cluster对象初始化后执行并通过线程pool延时调度一次

5:节点订阅

对于注册中心中节点的订阅一般分为两种:主动轮训与服务端推送,不同的注册中心的有着不同的实现

5.1:客户端轮训

主要通过client中的HostReactor定时pull服务端数据

5.2:服务端推送

一旦服务端中Service发送变更,通过PushService中的udpSender来push给client数据

6:nacos集群中节点数据同步

根据实例节点的不同集群中数据同步的方式也不同

6.1:ephemeral类型

  1. 每一个服务节点通过ServiceReporter来主动定时的发送本服务器的健康报告,其它服务器收到client节点变更通过ServiceStatusSynchronizer send发送->/service/status接口(变更服务器中数据)->addUpdatedService2Queue,最终触发ServiceUpdater ,push订阅的client信息。
  2. 每一个服务节点通过UpdatedServiceProcessor来定期从其他服务器异步获取更改的服务。
  3. 每一个服务节点收到实例的注册或下线请求时,最终通过TaskDispatcher中的TaskScheduler来将上下线同步其它节点中

通过上述三个步骤,使得集群中数据达到最终一致性

6.2:persistent类型

通过Raft算法的实现来保证,第一数据的变更通过leader来带领follower写入,第二leader与follower之间通过心跳机制来保证数据一致

总结:

从上述描述中,我们可以发现Nacos中内部为了保证集群中数据能快速实现一致性,大量的通过短周期(很多都是5s)的同步任务(包含服务端与服务端,服务端与客户端),服务端的主动push(这也对Eureka中最大的问题的解决,Eureka服务端发现节点被剔除,不会主动的通知client,需要client自己主动pull发现,这样在默认情况下其它服务消费者client会保存30s的脏数据),导致nacos集群会产生大量的网络通信。

你可能感兴趣的:(Nacos,Springcloud,分布式,java)