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

Nacos源码学习系列第10篇服务端消息接收类PushReceiver 前面的文章中我们着重讲了服务的注册和主动查询服务列表的功能。实际上在客户端发起服务查询(订阅)后,服务端会定时推送服务的结果给客户端,在1.1.4版本中是通过udp的方式推动的 初始化 public PushReceiver(HostReactor hostReactor) { try { this.hostReactor = hostReactor; udpSocket = new DatagramSocket(); . 阅读详情

目录

 

序言:

1:客户端核心类:

2:服务端核心代码:

2.1:数据模型有关

2.2:数据一致性有关

2.3:数据管理相关:

3:服务段与客户端交互详细描述

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

3.2:上线

3.2.1客户端:

3.2.2 服务端:

3.3:下线

3.1:客户端

3.4:心跳检测

4.1:客户端

3.4.2:服务端

3.5:节点订阅

3.5.1:客户端轮询

3.5.2:服务端推送

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

3.6.1:ephemeral类型

3.6.2:persistent类型

总结:


序言:

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

核心对外提供API接口类:

​

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

上述的每一个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:客户端核心类:

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

  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进行校验)。

2:服务端核心代码:

2.1:数据模型有关

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

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

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

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

2.2:数据一致性有关

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

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

具体算法实现: 

  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中会存在一份缓存数据用于读,避免频繁读写磁盘,写的动作在写入磁盘时刷新缓存)

2.3:数据管理相关:

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

  2. ServiceManage:nacos服务端用于管理Service信息,实现 RecordListener<Service>接口,当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接口:

  4.  

3:服务段与客户端交互详细描述

3.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服务端的自动发现,目前nacos还未做到,还需要依赖cluster.conf指定获取集群集群 然后通过raft进行leader选举的。

如下所示:

 

​

​

​

3.2:上线

3.2.1客户端:

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

​

​

​

 

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

 

 

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

3.2.2 服务端:

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

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

​

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

​

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

 

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

​

​详细源码:

/**
     * 若节点为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<String, String> 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<Integer>() {
                    @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<Datum<Instances>>() {
            });
        } else if (KeyBuilder.matchSwitchKey(datumJson.get(key).asText())) {
            datum = JacksonUtils.toObj(jsonObject.get("datum").toString(), new TypeReference<Datum<SwitchDomain>>() {
            });
        } else if (KeyBuilder.matchServiceMetaKey(datumJson.get(key).asText())) {
            datum = JacksonUtils.toObj(jsonObject.get("datum").toString(), new TypeReference<Datum<Service>>() {
            });
        }
        //上面都是处理请求数据 转化为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<Instance> 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:下线

3.1:客户端

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

​

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

​

​

​

 

​

 

​

3.2:服务端

通过InstaceController来接受信息。

​

 

3.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

​executorService中执行的BeatTask详解:

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

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

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

3.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<Instance> 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延时调度一次

3.5:节点订阅

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

3.5.1:客户端轮询

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

3.5.2:服务端推送

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

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

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

3.6.1:ephemeral类型

  1. 每一个服务节点通过ServiceReporter来主动定时的发送本服务器的健康报告,其它服务器收到client节点变更通过ServiceStatusSynchronizer send发送->/service/status接口(变更服务器中数据)->addUpdatedService2Queue,最终触发ServiceUpdater ,push订阅的client信息。

  2. 每一个服务节点通过UpdatedServiceProcessor来定期从其他服务器异步获取更改的服务。

  3. 每一个服务节点收到实例的注册或下线请求时,最终通过TaskDispatcher中的TaskScheduler来将上下线同步其它节点中

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

3.6.2:persistent类型

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

总结:

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

Spring Cloud Alibaba组件之深入分析Nacos服务调用源码实现 文章目录前言核心组件总结 前言 提示:这里可以添加本文要记录的大概内容: 例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。 核心组件 @Configuration(proxyBeanMethods=false) @EnableConfigurationProperties @Import({HttpClientConfiguration.class, OkHttpRibbonConfiguration.class, RestClien 阅读详情

相关推荐

Nacos注册中心16-临时实例集群间同步

我们知道nacos是支持CP与AP模型的,如果有小伙伴不知道AP CP代表啥含义,可以百度搜下CAP理论学习下。像我们常见的注册中心Eureka是AP模型,保证服务可用性的,Zookeeper 是CP模型,保证数据的一致性。

BASK2311的博客 602

5. nacos之服务心跳

(1)client发送心跳 在2. nacos之服务注册_ZXH240651200的博客-优快云博客里面分析过,如果服务注册类型是临时节点(默认就是临时的),会启动心跳任务。 com.alibaba.nacos.client.naming.NacosNamingService#registerInstance(java.lang.String, java.lang.String, com.alibaba.nacos.api.naming.pojo.Instance) public vo..

ZXH240651200的博客 5522

Nacos 源码分析 (三) 服务注册与发现

服务如何注册 服务注册最重要的就是将服务注册到哪里,在注册中心服务端,肯定有一个用来管理服务的容器,他保存着所有服务的实例。 我们暂时不需要知道该容器具体的实现细节,只需要知道有这样一个概念。 nacos-registry-princple-1.jpg nacos-registry-princple-2.jpg nacos-registry-princple-3.jpg ...

郭传志的博客 1336

com.alibaba.nacos.client.naming nacos心跳以及侦听一直打印问题。

com.alibaba.nacos.client.naming 项目引用了公司自己的核心服务包,里面日志配置级别都是debug的,调整项目的日志级别后,发现nacos不停的打印心跳链接和侦听的请求日志,有点影响代码调试 处理方法 ...

VC520530的博客 1万+

Nacos心跳机制解读(含简单源码分析)

心跳机制是一种用于监测和管理微服务可用性的机制,它用来维护注册中心和服务提供者之间的连接状态,并及时更新服务实例的状态信息。心跳机制包括两个主要组件:心跳发送方(客户端)和心跳接收方(服务端)。每隔几分钟发送一个固定信息给服务端,服务端收到后回复一个固定信息如果服务端几分钟内没有收到客户端信息则视客户端断开。发包方可以是客户也可以是服务端心跳发送方(Heartbeat Sender):每个微服务都会定期发送称为心跳消息的请求到一个中央位置(例如注册中心或负载均衡器)。

一个菜鸡的博客 3802

Nacos介绍与实战教程

欢迎来到 Nacos 的世界!Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式

llaxw1989的博客 2213

nacos服务com.alibaba.nacos.client.naming - out of date data received, old-t......

问题原因:Nacos集群,时钟不一致,查看本地和Nacos集群时a

whatzhang007的博客 1万+

【仿真】Carla Python API实战:从零搭建自动驾驶测试环境

本文详细介绍了如何使用Carla Python API从零搭建自动驾驶测试环境。通过Carla仿真平台,开发者可以构建高度逼真的城市环境和车辆物理模型,利用Python API实现传感器配置、数据采集、场景构建及自动驾驶算法集成。文章还分享了实际项目中的调试技巧和性能优化经验,帮助读者快速掌握自动驾驶仿真测试的核心技术。

weixin_29050513的博客 212

【分布式】一文搞懂SpringCloudAlibaba - (二)注册中心 - Nacos

分布式开发过程中要管理众多的服务也就是服务治理,管理服务与服务之间的依赖关系,实现服务调用、负载均衡、服务容错、以及服务的注册与发现。如果微服务之间存在调用依赖,就需要得到目标服务的服务地址,也就是微服务治理的服务发现。要完成服务发现,就需要将服务信息存储到某个载体,载体本身即是微服务治理的服务注册中心,而存储到载体的动作即是服务注册。在 RPC/HTTP 远程调用过程中,服务与服务之间依赖关系非常大,服务 URL 地址管理非常复杂,所以这时候需要对我们服务的 URL 实现治理。

之杭 1137

机器学习——笔记整理(原理、python代码)——反反复复,出精活!

回顾知识,检查自我,开拓新域。 机器学习1.监督学习 机器学习简介: 机器学习是实现人工智能的手段,其主要研究内容是如何利用数据或经验进行学习,改善具体算法的性能。 多领域交叉,涉及概率论、统计学,算法复杂度理论等多门学科 广泛应用于网络搜索、垃圾邮件过滤、推荐系统、广告投放、信用评价、欺诈检测、股票交易和医疗诊断等应用。 机器学习分类: 机器学习一般分为下面几种类别 监督学习 ( Supervised Learning) 无监督学习 ( Unsupervised Learning) 强化学习(

weixin_45666566的博客 6485

使用DeepSeek、MCP和AKShare实现智能金融问答系统技术方案

DeepSeek是由深度求索公司开发的大规模语言模型,最新版本DeepSeek-V3采用混合专家(MoE)架构,拥有671B参数,激活37B参数,在多项基准测试中表现优异3。要使用DeepSeek的API服务:访问DeepSeek平台注册账号获取API Key(形如AKShare是基于Python的金融数据接口库,提供股票、基金、期货、宏观经济等领域的海量数据。主要特点包括:免费开源,数据源丰富支持多种数据格式(DataFrame/JSON)持续更新维护安装命令。

Kin 3170

Nacos架构篇 - 服务注册

本文基于 Nacos 的 1.3.2 版本展开服务注册的分析。

The Secret 5172

RuoYi项目适配达梦数据库(二)

RuoYi项目,代码生成模块的达梦数据库适配。

邢慧鹏的博客 1万+

nacos注册中心

nacos 概念 命名空间:用于进行租户粒度的配置隔离,常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。 配置:需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。通常以 param-key=param-value 的形式存在。 配置集:一组相关的配置项的集合称。一个配置文件通常就是一个配置集,一个系统或者应用可以包含多个配置集。常用于不同的应用或组件使用了相同的配置类型,如 database_url 配置和 MQ_topic

weixin_40663800的博客 756

ego-planner启动参数深度解析:从配置到实战避障应用

本文深入解析EGO-Planner启动参数的配置与实战应用,重点探讨无人机自主导航中的避障性能优化。通过环境感知、运动约束和算法控制三类参数的详细说明,结合ROS和RViz工具,提供从基础配置到高级调优的完整指南,帮助开发者提升无人机在复杂场景下的规划效率和安全性。

rum55的博客 245

Telegram Bot Api使用教程

其中token为你的机器人token,method为telegram给定的方法,在获取群组chat_id那一步就使用了telegram的其中一个方法(getUpdates),其他方法后面会介绍。telegram发送消息的方式类似与钉钉机器人,都是向一个api发送http请求,而且对于同一个APItelegram支持GET和POST两种请求方式.请求的api格式如下。通常来说我们都需要让机器人在一个群组里工作,所以首先我们需要将机器人添加到我们指定的群组,在群组里发送随意消息并@这个机器人,比如...

豫迷糊的码上生活 1万+

Nacos服务注册原理分析

从依赖上找到需要启动的是要加载的服务注册类,主要配置三个对象类的register方法,封装实例和心跳信息通过http请求,定时发送发送心跳包,默认时间间隔是5秒。通过http请求,发送实例信息。服务端接收到心跳请求,更新心跳包最新时间。服务端在15秒没有收到心跳包会将实例设为不健康,在30秒没有收到心跳包会将临时实例移除掉。接收到服务注册接口,通过ephemeral判断是否走AP还是走CPAP模式使用Distro协议。通过调用接口实现,持久化实例信息。

qq_42700121的博客 500

攻克+86手机号Discord验证墙:从“无效”到“成功”的实战指南

本文提供了一份针对+86手机号在Discord注册时频繁遭遇“手机号无效”问题的实战解决指南。文章系统分析了问题根源,并分步详解了从打造纯净网络环境、尝试多种手机号格式(如+86前加0),到更换iOS等客户端设备的有效方法,旨在帮助用户成功完成Discord手机号验证。

mac99的博客 1100

深度解析nacos源码之注册中心(服务主动下线)

原创不易,转载请注明出处 本文基于nacos1.4.0 文章目录前言1.客户端请求服务下线2.服务端处理服务主动下线 前言 nacos作为注册中心,支持两种服务下线,一种是客户端主动调用api向服务端发送服务下线,然后实现服务下线,第二种就是服务故障,然后服务端很长时间没有收到某个实例的心跳信息,服务端就会将这个服务健康状态设置成false,也就是标志不健康状态(这个时间默认是15s,也就是15s服务端没有收到某个服务的心跳信息),如果更长时间没有收到心跳信息,直接就会将这个服务摘除(默认是30s)。.

猿上生活 6880

基于AC算法的图像显著性检测及Matlab代码实现

以上是基于AC算法的图像显著性检测的Matlab代码实现。图像显著性检测是计算机视觉领域的一个重要任务,其目标是确定图像中最具有显著性的区域,即吸引人眼注意力的区域。图像显著性检测是计算机视觉领域的一个重要任务,其目标是确定图像中最具有显著性的区域,即吸引人眼注意力的区域。AC算法的原理基于人眼视觉系统的特性,人眼在观察图像时通常会先关注图像中的显著性区域,然后才会注意到其他细节。AC算法的原理基于人眼视觉系统的特性,人眼在观察图像时通常会先关注图像中的显著性区域,然后才会注意到其他细节。

TechPulseZ的博客 438

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

Nacos注册中心AP模式核心源码分析(单机模式)

Nacos注册表架构:Nacos中注册表是一个重要的概念,也是Nacos存储服务实例的数据结构,源码中体现在的serviceMap属性中。例:public = {第一层 Map:Map<String, Map<String, Service>>键(String):public → 代表命名空间(namespaceId)。值(Map<String, Service>) → 该命名空间下的所有服务。第二层 Map:Map<String, Service>

西岭千秋雪的博客 901

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

【微服务架构】SpringCloud Alibaba(七):Nacos 2.1.0 作为注册中心(源码分析之GRPC)

Spring Cloud Alibaba是阿里巴巴提供的一站式微服务解决方案,是Spring Cloud体系中的一个重要分支,它将阿里巴巴在微服务领域的实践经验和开源技术进行了整合,为开发者提供了一系列便捷的工具和组件,用于构建分布式微服务应用。我们想我们Springmvc处理请求的时候,我们是根据路径判断对应的controller,这里我们的请求应该是那个具体的handler呢?总结:我们处理实例就是用的监听器来获取对应的所有实例,然后以map处理,所有请求从这map中拿取对应的对象。

专注上位机开发、机器视觉、机器学习、嵌入式软硬件开发 5万+

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

【微服务架构】SpringCloud Alibaba(七):Nacos 2.1.0 作为注册中心(源码分析之服务发现、服务订阅、服务变更推送)

服务启动时候读取磁盘中数据放到缓存读取磁盘数据 如果没有则发送请求获取数据,然后写到缓存读取磁盘数据,如果有则判断时间是否过期,过期则发送请求,写到缓存读取数据是一个定时任务每6秒执行一次,如果失败就会延长,但最大时长是1分钟这里主要是从注册表中获取数据,所以理解这里需要看一下我们的注册表中,各个数据之间的关系。

专注上位机开发、机器视觉、机器学习、嵌入式软硬件开发 5万+

SpringCloud Alibaba实战和源码分析——Nacos2.1.0作为注册中心源码分析(8)

找入口的方式:自动装配类 spring.factories事件驱动:NacosAutoServiceRegistration实现了applicationListener接口判断变量 1、 debug 2、 全文搜索 定位赋值位置通过ephemeral的值判断是grpc通信,还是http通信,通过这我们能判断ap模式是用的grpc模式,cp模式是用http通信判断服务端处理类的方式,我们可以根据请求参数,找对应服务端的处理类(由于开源框架都是规范的,一般都是根据请求参数来命名,所以可以采用这种方式)

qq_45546075的博客 941

nacos源码分析-服务注册

Nacos 是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台目前主要关注 Nacos 服务注册与发现相关的内容服务提供者在启动时会向Nacos注册中心发送注册请求,包括服务名称、IP地址、端口号等信息。Nacos服务端接收到注册请求后,将服务实例信息存储在注册中心的数据库中,并缓存到内存中以便快速查询。注册成功后,服务提供者会定期向Nacos发送心跳请求,以表明服务实例仍在运行中。服务消费者从服务注册中心发现并调用服务。

海贼王路飞的博客 1010

详解nacos注册中心服务注册流程

详解nacos注册中心服务注册流程 说起注册中心,我们首先要知道注册中心是用来做什么的,注册中心一般都是用在微服务架构中,而微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信通常采用HTTP的方式,这些服务共用一个最小型的集中式的管理。这个最小型的集中式管理的组件就是服务注册中心。 一、nacos 简介 本文的目的在于详解 nacos 注册中心的服务注册流程,所以首先需要对 nacos 有个基本的了解。nacos 提供了一组简单易用的特性集,帮助您快速实现

wangjimmy1994的博客 3900
上一篇: Mysql数据数据库引擎InnoDB与MyISAM
下一篇: 分布式一致性算法与开源分布式应用具体实现
F_Hello_World
博客等级 码龄11年 38粉丝 58原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值