SpringCloud Alibaba 深入源码 - Nacos 和 Eureka 的区别(健康检测、服务的拉取和订阅)

news2024/11/20 8:43:39

目录

一、Nacos 和 Eureka 的区别

1.1、以 Nacos 注册流程来解析区别


一、Nacos 和 Eureka 的区别


1.1、以 Nacos 注册流程来解析区别

a)首先,我们的服务启动时。都会把自己的信息提交给注册中心,然后注册中心就会把信息保存下来.

注册的信息实际上就是一个嵌套 Map,结构为 Map<String, Map<String, Service>>,第一层 key 就是 namespace_id,起到环境隔离的作用. value 由是一个嵌套 Map<String, Service>.

第二层的 key 表示 group 分组,key 就是分组名,value 就是分组下的某一个服务,实际上就是一个类,内部又维护了一个  Map<String,Cluster> .

第三层的 key 就是集群的名称,value 就是  Cluster ,也是一个类,包含了集群的具体信息.  

因为一个集群中可能包含多个实例,也就是具体的节点信息(例如实例的IP、Port、健康状态),那么 Cluster 这个类中又维护了 两个 Set<Instance>,分别是临时实例和非临时实例(此处,Eureka 就没有做区分,只有临时实例).

b)那么当服务消费者要去消费时,就可以从注册中心拉取服务信息.  这个过程也被称为“服务发现”.  但是他这个拉去动作不是每次都要做的(压力太大),而是将拉取到的服务信息缓存到一个列表中,这样接下来的一段时间里,就不用去拉去了,而是直接从缓存列表中拿. 

当然这个缓存一直不更新也不行,因此会每隔 30 秒取重新拉取一次(多长时间不用记,都是可以配置的),进行更新.

c)消费者拿到服务列表后,就可以通过 负载均衡(LoadBalancer)从列表中挑选一个发起远程调用就可以了. 

d)截至目前为止,Nacos 和 Eureka 还没什么太大的差别,那差别在哪呢?差别就在于服务提供者的健康检测机制.

e)在 nacos 中,将服务分成了临时实例和非临时实例:

  • 临时实例:当临时实例进行心跳检测的时候,如果心跳不跳了,nacos 就会把它从服务中直接剔除.  (这里的心跳检测机制和 Eureka 是一样的,但非临时实例就不一样了)
  • 非临时实例:nacos 就不会要求你给我发心跳了,而是通过 nacos 主动发请求询问,定时向实例发送请求:“你还活着吗?”,即使真的挂了,nacos 也仅仅只是把它标记为 不健康,不会剔除,而是等待它恢复健康.

而 Eureka 只提供了心跳模式的健康监测,而没有主动检测功能。

主动询问进行健康检测,效率岂不是很低?

对于非临时实例,所有的健康检测任务都不是立即执行的,都会被放入一个阻塞队列中,如下源码

@Override
public void process(HealthCheckTask task) {
    // 获取所有 非临时实例的 集合
    List<Instance> ips = task.getCluster().allIPs(false);

    if (CollectionUtils.isEmpty(ips)) {
        return;
    }

    for (Instance ip : ips) {
		// 封装健康检测信息到 Beat
        Beat beat = new Beat(ip, task);
        // 放入一个阻塞队列中
        taskQueue.add(beat);
        MetricsMonitor.getTcpHealthCheckMonitor().incrementAndGet();
    }
}

可以看出,检测任务不是立即执行,这里也采用了异步指定的策略,会把任务放到线程池中取执行,如下:

public void run() {
    while (true) {
        try {
            // 处理任务
            processTask();
            // ...
        } catch (Throwable e) {
            SRV_LOG.error("[HEALTH-CHECK] error while processing NIO task", e);
        }
    }
}

 通过 processTask 来处理健康检测的任务:

private void processTask() throws Exception {
    // 将任务封装为一个 TaskProcessor,并放入集合
    Collection<Callable<Void>> tasks = new LinkedList<>();
    do {
        Beat beat = taskQueue.poll(CONNECT_TIMEOUT_MS / 2, TimeUnit.MILLISECONDS);
        if (beat == null) {
            return;
        }

        tasks.add(new TaskProcessor(beat));
    } while (taskQueue.size() > 0 && tasks.size() < NIO_THREAD_COUNT * 64);
	// 批量处理集合中的任务
    for (Future<?> f : GlobalExecutor.invokeAllTcpSuperSenseTask(tasks)) {
        f.get();
    }
}

 任务被封装到了TaskProcessor中去执行了,TaskProcessor是一个Callable,其中的call方法:

@Override
public Void call() {
    // 获取检测任务已经等待的时长
    long waited = System.currentTimeMillis() - beat.getStartTime();
    if (waited > MAX_WAIT_TIME_MILLISECONDS) {
        Loggers.SRV_LOG.warn("beat task waited too long: " + waited + "ms");
    }
	
    SocketChannel channel = null;
    try {
        // 获取实例信息
        Instance instance = beat.getIp();
		// 通过NIO建立TCP连接
        channel = SocketChannel.open();
        channel.configureBlocking(false);
        // only by setting this can we make the socket close event asynchronous
        channel.socket().setSoLinger(false, -1);
        channel.socket().setReuseAddress(true);
        channel.socket().setKeepAlive(true);
        channel.socket().setTcpNoDelay(true);

        Cluster cluster = beat.getTask().getCluster();
        int port = cluster.isUseIPPort4Check() ? instance.getPort() : cluster.getDefCkport();
        channel.connect(new InetSocketAddress(instance.getIp(), port));
		// 注册连接、读取事件
        SelectionKey key = channel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
        key.attach(beat);
        keyMap.put(beat.toString(), new BeatKey(key));

        beat.setStartTime(System.currentTimeMillis());

        GlobalExecutor
            .scheduleTcpSuperSenseTask(new TimeOutTask(key), CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
    } catch (Exception e) {
        beat.finishCheck(false, false, switchDomain.getTcpHealthParams().getMax(),
                         "tcp:error:" + e.getMessage());

        if (channel != null) {
            try {
                channel.close();
            } catch (Exception ignore) {
            }
        }
    }

    return null;
}

这差别就像是亲生儿子和非亲生儿子,亲生儿子我还会去主动关怀一下,诶,你还活着么?而非临时实例,就是你不心跳了,就把你扔了~

f)还有一个差别在于服务消费者,Eureka 采用的是定时拉取(每 30 秒一次),那如果在 30 秒内服务提供者挂了,消费肯定是不知道的,因此 Eureka 这里更新的时效性也比较差.

我们的 微服务 定时拉取的基本逻辑就是先从本地缓存读:

  • 如果本地缓存没有,就通过 Nacos 客户端构造请求去 nacos 服务器中读取.
  • 如果本地缓存有,就开启定时更新功能(就是创建一个定时任务,每隔一段时间去拉取一次),并返回缓存结果.

核心源码如下:

public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {

    NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
    // 由 服务名@@集群名拼接 key
    String key = ServiceInfo.getKey(serviceName, clusters);
    if (failoverReactor.isFailoverSwitch()) {
        return failoverReactor.getService(key);
    }
    // 读取本地服务列表的缓存,缓存是一个Map,格式:Map<String, ServiceInfo>
    ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
    // 判断缓存是否存在
    if (null == serviceObj) {
        // 不存在,创建空ServiceInfo
        serviceObj = new ServiceInfo(serviceName, clusters);
        // 放入缓存
        serviceInfoMap.put(serviceObj.getKey(), serviceObj);
        // 放入待更新的服务列表(updatingMap)中
        updatingMap.put(serviceName, new Object());
        // 立即更新服务列表
        updateServiceNow(serviceName, clusters);
        // 从待更新列表中移除
        updatingMap.remove(serviceName);

    } else if (updatingMap.containsKey(serviceName)) {
        // 缓存中有,但是需要更新
        if (UPDATE_HOLD_INTERVAL > 0) {
            // hold a moment waiting for update finish 等待5秒中,待更新完成
            synchronized (serviceObj) {
                try {
                    serviceObj.wait(UPDATE_HOLD_INTERVAL);
                } catch (InterruptedException e) {
                    NAMING_LOGGER
                        .error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
                }
            }
        }
    }
    // 开启定时更新服务列表的功能
    scheduleUpdateIfAbsent(serviceName, clusters);
    // 返回缓存中的服务信息
    return serviceInfoMap.get(serviceObj.getKey());
}

定时更新方法如下:

public void updateService(String serviceName, String clusters) throws NacosException {
    ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
    try {
		// 基于ServerProxy发起远程调用,查询服务列表
        String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);

        if (StringUtils.isNotEmpty(result)) {
            // 处理查询结果
            processServiceJson(result);
        }
    } finally {
        if (oldService != null) {
            synchronized (oldService) {
                oldService.notifyAll();
            }
        }
    }
}


public String queryList(String serviceName, String clusters, int udpPort, boolean healthyOnly)
    throws NacosException {
	// 准备请求参数
    final Map<String, String> params = new HashMap<String, String>(8);
    params.put(CommonParams.NAMESPACE_ID, namespaceId);
    params.put(CommonParams.SERVICE_NAME, serviceName);
    params.put("clusters", clusters);
    params.put("udpPort", String.valueOf(udpPort));
    params.put("clientIP", NetUtils.localIP());
    params.put("healthyOnly", String.valueOf(healthyOnly));
	// 发起请求,地址与API接口一致
    return reqApi(UtilAndComs.nacosUrlBase + "/instance/list", params, HttpMethod.GET);
}

 而 nacos 这里的消费者不光进行服务的定时拉取,nacos 还会主动进行消息的订阅推送,一旦发现有服务挂了,就立刻推送一条消息给服务消费者,告诉你服务要更新了.

Nacos 具体是通过什么实现消息订阅推送机制呢?

a)首先 PushPeceiver 这个类(我们自己的微服务配置的 Nacos 客户端),会以 UDP 的方式与 Nacos 服务端建立连接,监听 Nacos 服务端推送的服务变更数据.

b)一旦 Nacos 服务列表发生变更,就会发送 UDP 广播给所有的微服务订阅者.

c)当订阅者接收到通知以后,就可以将接收到的服务信息缓存到本地缓存列表.

d)那么之后再拉取服务的时候,会优先从缓存里读取,缓存里有就直接返回缓存,如果没有,再去拉取或者订阅.

PushPeceiver 构造函数如下:

public PushReceiver(HostReactor hostReactor) {
    try {
        this.hostReactor = hostReactor;
        // 创建 UDP客户端
        String udpPort = getPushReceiverUdpPort();
        if (StringUtils.isEmpty(udpPort)) {
            this.udpSocket = new DatagramSocket();
        } else {
            this.udpSocket = new DatagramSocket(new InetSocketAddress(Integer.parseInt(udpPort)));
        }
        // 准备线程池
        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.push.receiver");
                return thread;
            }
        });
		// 开启线程任务,准备接收变更数据
        this.executorService.execute(this);
    } catch (Exception e) {
        NAMING_LOGGER.error("[NA] init udp socket failed", e);
    }
}

PushReceiver 构造函数中基于线程池来运行任务。这是因为 PushReceiver 本身也是一个Runnable,其中的run方法业务逻辑就是:

@Override
public void run() {
    while (!closed) {
        try {
            // byte[] is initialized with 0 full filled by default
            byte[] buffer = new byte[UDP_MSS];
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
			// 接收推送数据
            udpSocket.receive(packet);
			// 解析为json字符串
            String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();
            NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
			// 反序列化为对象
            PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
            String ack;
            if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
                // 交给 HostReactor去处理
                hostReactor.processServiceJson(pushPacket.data);

                // send ack to server 发送ACK回执,略。。
        } catch (Exception e) {
            if (closed) {
                return;
            }
            NAMING_LOGGER.error("[NA] error while receiving push data", e);
        }
    }
}

通知数据的处理由交给了 HostReactor 的 processServiceJson 方法:

public ServiceInfo processServiceJson(String json) {
    // 解析出ServiceInfo信息
    ServiceInfo serviceInfo = JacksonUtils.toObj(json, ServiceInfo.class);
    String serviceKey = serviceInfo.getKey();
    if (serviceKey == null) {
        return null;
    }
    // 查询缓存中的 ServiceInfo
    ServiceInfo oldService = serviceInfoMap.get(serviceKey);

    // 如果缓存存在,则需要校验哪些数据要更新
    boolean changed = false;
    if (oldService != null) {
		// 拉取的数据是否已经过期
        if (oldService.getLastRefTime() > serviceInfo.getLastRefTime()) {
            NAMING_LOGGER.warn("out of date data received, old-t: " + oldService.getLastRefTime() + ", new-t: "
                               + serviceInfo.getLastRefTime());
        }
        // 放入缓存
        serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
		
        // 中间是缓存与新数据的对比,得到newHosts:新增的实例;remvHosts:待移除的实例;
        // modHosts:需要修改的实例
        if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
            // 发布实例变更的事件
            NotifyCenter.publishEvent(new InstancesChangeEvent(
                serviceInfo.getName(), serviceInfo.getGroupName(),
                serviceInfo.getClusters(), serviceInfo.getHosts()));
            DiskCache.write(serviceInfo, cacheDir);
        }

    } else {
        // 本地缓存不存在
        changed = true;
        // 放入缓存
        serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
        // 直接发布实例变更的事件
        NotifyCenter.publishEvent(new InstancesChangeEvent(
            serviceInfo.getName(), serviceInfo.getGroupName(),
            serviceInfo.getClusters(), serviceInfo.getHosts()));
        serviceInfo.setJsonFromServer(json);
        DiskCache.write(serviceInfo, cacheDir);
    }
	// 。。。
    return serviceInfo;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1402124.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前后端分离多年,为何服务端渲染(SSR)重回风口浪尖?

前后端分离多年&#xff0c;为何服务端渲染&#xff08;SSR&#xff09;重回风口浪尖&#xff1f; 什么是服务端渲染&#xff1f; 咱们先搞明白个事儿&#xff0c;啥叫服务端渲染&#xff1f;服务端渲染的全称是 Server-Side Rendering&#xff0c;简称SSR。 简单说&#xf…

深入解析 JavaScript 中的 F.prototype

&#x1f9d1;‍&#x1f393; 个人主页&#xff1a;《爱蹦跶的大A阿》 &#x1f525;当前正在更新专栏&#xff1a;《VUE》 、《JavaScript保姆级教程》、《krpano》、《krpano中文文档》 ​ ​ ✨ 前言 JavaScript作为一门原型继承语言,函数的prototype属性在其中发挥着重…

基于CanvasLabel的Leaflet矢量数据免切片属性标注实践

目录 前言 一、Leaflet.CanvasLabel 1、开源地址 2、设置参数说明 二、组件集成 1、新建html文件 2、声明样式 3、定义矢量文本渲染器 4、定义地图 5、添加矢量数据 6、最终效果 总结 前言 在一般的业务场景中&#xff0c;针对小量的矢量数据&#xff0c;比如POI兴…

快速统计文件和文件夹大小

windows上没有方便统计各个层级文件夹文件大小的工具&#xff0c;于是自己做了一个 源码 https://gitee.com/chen227/calc-tree-space

imgaug库图像增强指南(32):塑造【雪景】效果的视觉魔法

引言 在深度学习和计算机视觉的世界里&#xff0c;数据是模型训练的基石&#xff0c;其质量与数量直接影响着模型的性能。然而&#xff0c;获取大量高质量的标注数据往往需要耗费大量的时间和资源。正因如此&#xff0c;数据增强技术应运而生&#xff0c;成为了解决这一问题的…

【项目日记(三)】内存池的整体框架设计

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:项目日记-高并发内存池⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你做项目   &#x1f51d;&#x1f51d; 开发环境: Visual Studio 2022 项目日…

python-分享篇-一箭穿心

一箭穿心&#x1f498; 代码 from turtle import * color(black,red) pensize(5) begin_fill() penup() goto(50,50) pendown() right(45) goto(100,0) left(90) fd(120) circle(50,225) penup() goto(0,0) pendown() left(135) fd(120) circle(50,225) seth(90) circle(50,2…

unity 单例模式(实例详解)

文章目录 在Unity中&#xff0c;单例模式是一种常用的编程设计模式&#xff0c;用于确保在整个应用程序生命周期中&#xff0c;只有一个类的实例存在。这样可以保证数据的全局唯一性和共享性&#xff0c;例如游戏场景中的资源管理器、游戏控制器、事件管理器等。 以下是一个简单…

WorkPlus Meet私有化视频会议软件-构建安全高效的内网会议体验

在企业内部&#xff0c;高效的会议协作是推动团队协同和工作效率的关键。而内网会议系统成为了构建安全高效的内部会议体验的必要工具。作为一家领先的内网会议系统&#xff0c;WorkPlus Meet以其卓越的性能和智能化的功能&#xff0c;助力企业实现高效安全的内部会议体验。 为…

python实现图片式PDF转可搜索word文档[OCR](已打包exe文件)

目录 1、介绍 1.1、痛点 1.2、程序介绍 2、安装方式 2.1、&#x1f53a;必要环节 2.2、脚本安装 2.2.1、不太推荐的方式 2.2.2、节约内存的方式 2.3、⭐完整版安装 3、使用 3.1、最终文件目录 3.2、主程序 3.2.1、绝对路径 3.2.2、是否为书籍 3.2.3、⭐截取区域 …

Threejs实现立体3D园区解决方案及代码

一、实现方案 单独贴代码可能容易混乱&#xff0c;所以这里只讲实现思路&#xff0c;代码放在最后汇总了下。 想要实现一个简单的工业园区、主要包含的内容是一个大楼、左右两片停车位、四条道路以及多个可在道路上随机移动的车辆、遇到停车位时随机选择是否要停车&#xff0…

内存地址解码3至8 线解码器(74LS138)

3 至 8 线解码器 &#xff08;74LS138&#xff09;1 内存地址解码 处理器通常可以寻址比单个内存芯片覆盖的内存空间大得多 的内存空间。 为了将存储设备拼接到处理器的地址空间中&#xff0c;解码是必要的。 例如&#xff0c;8088 发出 20 位 地址&#xff0c;总共有 1MB 的…

stable diffuison的安装和使用

stable diffuison的安装和使用 简单介绍 Stable Diffusion是一个深度学习文本到图像的生成模型&#xff0c;它可以根据文本描述生成详细的图像。这个模型主要应用于文本生成图像的场景中&#xff0c;通过给定的文本提示词&#xff0c;模型会输出一张与提示词相匹配的图片。 S…

【spring】代码生成器

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;spring ⛺️稳中求进&#xff0c;晒太阳 代码生成器&#xff08;本质IO流&#xff09; 在mybatis的逆向工程生成model和mapper接口和xml文件后&#xff0c;还需要反复的写Service的接口和…

(2)(2.1) Andruav Android Cellular(二)

文章目录 前言 5 Andruav Web Client 6 Andruav Telemetry 7 Andruav高级功能 8 将Andruav与SITL配合使用 9 FAQ 10 术语表 前言 Andruav 是一个基于安卓的互联系统&#xff0c;它将安卓手机作为公司计算机&#xff0c;为你的无人机和遥控车增添先进功能。 5 Andruav W…

提升网站关键词排名的工具

随着互联网的蓬勃发展&#xff0c;网站的关键词排名成为衡量网站流量和曝光度的重要指标。在这个竞争激烈的数字时代&#xff0c;站在搜索引擎结果的前列变得至关重要。为了实现这一目标&#xff0c;合理利用关键词排名优化工具是必不可少的。本文将重点介绍147SEO软件&#xf…

RT Thread Stdio生成STM32L431RCT6无法启动问题

一、问题现象 使用RT thread Stdio生成STM32L431RCT6工程后&#xff0c;编译下载完成后系统无法启动&#xff0c;无法仿真debug&#xff1b; 二、问题原因 如果当前使用的芯片支持包版本为0.2.3&#xff0c;可能是这个版本问题&#xff0c;目前测试0.2.3存在问题&#xff0c…

ESP32 MicroPython 小车红外自动寻迹与避障⑭

ESP32 MicroPython 小车红外自动寻迹与避障⑭ 1、小车超声波避障2、 小车红外自动寻迹3、 小车摄像头自动寻迹 1、小车超声波避障 实验目的 使用舵机水平扫描和超声波测距功能&#xff0c;实现小车自动避障行走 实验内容 小车控制舵机转动到中间位置&#xff0c;读取前方距离。…

26、江科大stm32视频学习笔记——W25Q64简介

一、电路图 1、软件模拟的SPI&#xff1a;线可以任意接 2、硬件模拟的SPI&#xff1a;要按以下方式连接 3、本次软件模拟和硬件模拟使用同一个电路图&#xff0c;方便切换 CS&#xff08;片选&#xff09;&#xff1a;PA4 DO&#xff08;从…

适合初学者的 机器学习 资料合集(可快速下载)

AI时代已经来临&#xff0c;机器学习成为了当今的热潮。但是&#xff0c;很多人在面对机器学习时却不知道如何开始学习。 今天&#xff0c;我为大家推荐几个适合初学者的机器学习开源项目&#xff0c;帮助大家更好地了解和掌握机器学习的知识。这些项目都是开源的&#xff0c;…