Eureka 系列(05)消息广播(上):消息广播原理分析

0. Spring Cloud 系列目录 - Eureka 篇

首先回顾一下客户端服务发现的流程,在上一篇 Eureka 系列(04)客户端源码分析 中对 Eureka Client 的源码进行了分析,DiscoverClient 负载服务发现,会将 Eureka Server 的服务全量同步到客户端。客户端同步的方式有两种:一是全量同步,二是增量同步,如果增量同步失败,则回滚到全量同步。

Eureka Client 服务发现的具体方式是启动了几个定时任务:

  1. CacheRefreshThread 本地缓存更新线程,采用轮询的方式,默认每 30s 从服务器同步注册服务信息。
  2. HeartbeatThread 心跳检测线程,默认每 30s 发送一次心跳到服务端。
  3. InstanceInfoReplicator 线程,默认每 30s 检测一次实例信息是否发生变更,如果发生变化就重新注册一次。这个好像是 Eureka 独有的吧!

接下来,我们分析一下服务器消息广播机制,如何保障数据的最终一致性?相关的核心实现在 com.netflix.eureka.cluster 内。

Eureka 消息广播主要分三部分讲解:

  1. 服务器列表管理:PeerEurekaNodes 管理了所有的 PeerEurekaNode 节点。
  2. 消息广播机制分析:PeerAwareInstanceRegistryImpl 收到客户端的消息后,第一步:先更新本地注册信息;第二步:遍历所有的 PeerEurekaNode,转发给其它节点。
  3. TaskDispacher 消息处理: Acceptor - Worker 模式分析。

本文重点分析前两部分的消息广播原理,下一章则分析 TaskDispacher 的 Acceptor - Worker 模式。

1. 服务器列表管理

Eureka 中负责服务器列表管理的是 PeerEurekaNodes,在 Nacos Naming 中也有一个类似功能的类 ServerListManager。这个类还是要关注一下,涉及到 Eureka 的动态扩容。

PeerEurekaNodes 构建时会初始化 "Eureka-PeerNodesUpdater" 定时器,默认每 10min 调用 updatePeerEurekaNodes(resolvePeerUrls()) 方法更新一次服务列表。

图1:Eureka 服务器列表更新

sequenceDiagram
participant Scheduler
participant PeerEurekaNodes
participant EndpointUtils
participant PeerEurekaNode
Scheduler ->> PeerEurekaNodes : updatePeerEurekaNodes
PeerEurekaNodes ->> PeerEurekaNodes : 1. 查找最新的服务器列表:resolvePeerUrls
PeerEurekaNodes ->> EndpointUtils : getDiscoveryServiceUrls
PeerEurekaNodes ->> PeerEurekaNode : 2.1 废弃的Server: shutDown
PeerEurekaNodes ->> PeerEurekaNode : 2.2 新增的Server: createPeerEurekaNode

总结: EndpointUtils.getDiscoveryServiceUrls 默认调用 getServiceUrlsFromConfig,即读取配置文件的 serviceUrl 配置。当服务器列表发生变化时会将废弃的 PeerEurekaNode 节点关闭,同时将新增的节点添加到 List<PeerEurekaNode> peerEurekaNodes 服务器列表中。

注意:peerEurekaNodes 服务器列表中并不包含当前 Server 的服务器,在 resolvePeerUrls 时会将当前服务器排除。

1.1 创建 PeerEurekaNode

  1. protected PeerEurekaNode createPeerEurekaNode(String peerEurekaNodeUrl) {
  2. HttpReplicationClient replicationClient = JerseyReplicationClient.createReplicationClient(serverConfig, serverCodecs, peerEurekaNodeUrl);
  3. String targetHost = hostFromUrl(peerEurekaNodeUrl);
  4. if (targetHost == null) {
  5. targetHost = "host";
  6. }
  7. return new PeerEurekaNode(registry, targetHost, peerEurekaNodeUrl, replicationClient, serverConfig);
  8. }

总结: PeerEurekaNode 代表一个 Eureka Server 节点,包含节点的 url 和配置信息 serverConfig,其中最重要的两个属性是 registry 和 replicationClient:

  • targetHost/serverConfig 当前 Eureka Server 的 url 信息。
  • registry 管理所有的注册信息。
  • replicationClient HTTP Client,用于网络传输。

注意: Discovery Client 默认是 JerseyApplicationClient,这两者的区别是 JerseyReplicationClient 的请求头是 PeerEurekaNode.HEADER_REPLICATION=true,而 JerseyApplicationClient 请求头的默认参数为 false。isReplication 这个参数的意思是是否是其它服务器转发的请求。

为什么要有这个参数呢?大家想一下,EurekaA 向 EurekaB 转发请求,如果 EurekaB 又向 EurekaA 转发请求,这样就会造成死循环,所以就在请求头中加上这个参数 isReplication=true。当然如果是客户端发起的请求,则需要同步给其它服务器,所以客户端 isReplication=false。

2. 消息广播分析

Eureka Server 接收客户端的请求后,会将请求转发给 PeerAwareInstanceRegistryImpl 处理。这个 registry 会做两件事:一是本地注册信息更新(同步);二是将消息广播给其它服务器(异步)。

由此也可以看出 Eureka 是 AP 模型的,优先保障了可用性,事实上大多数注册中心的实现方案都是 AP 模型,只有 ZK 是 CP 模型。事实上,ZK 是分布式协调服务,并不是专门用来进行服务治理的。

本文重点关注第二步:消息广播机制。

2.1 Eureka 消息广播流程

PeerAwareInstanceRegistryImpl 处理完本地注册信息更新后,会将请求转发给 PeerEurekaNode 处理,这个过程是异步的。也就是说本地注册信息更新后请求就返回了,而消息广播都是由 TaskDispatcher 异步处理,当然数据也就可能会短时间内不一致。

图2:Eureka 消息广播流程

sequenceDiagram
participant PeerAwareInstanceRegistryImpl
participant AbstractInstanceRegistry
participant PeerEurekaNodes
participant PeerEurekaNode
note over PeerAwareInstanceRegistryImpl,PeerEurekaNode : 接收EurekaClient请求:<br/>register/cancel/heartbeat/statusUpdate/deleteStatusOverride
PeerAwareInstanceRegistryImpl ->> AbstractInstanceRegistry : 1. 更新本地注册信息:register/cancel/heartbeat/...
loop 2. 消息广播给其它Server
PeerAwareInstanceRegistryImpl ->>+ PeerAwareInstanceRegistryImpl : replicateToPeers
PeerAwareInstanceRegistryImpl ->> PeerEurekaNodes : getPeerEurekaNodes
PeerAwareInstanceRegistryImpl ->> PeerEurekaNodes : continue: isThisMyUrl
PeerAwareInstanceRegistryImpl ->> PeerAwareInstanceRegistryImpl : replicateInstanceActionsToPeers
loop 消息广播
PeerAwareInstanceRegistryImpl ->>- PeerEurekaNode : register/cancel/heartbeat/...
end
end

总结: PeerAwareInstanceRegistryImpl 是 Eureka 的核心类,服务的注册、下线、心跳检测都是由这个类完成的,服务的本地注册信息都是由这个其父类 AbstractInstanceRegistry 进行维护的。

  1. 本地注册信息更新(同步):首先由 AbstractInstanceRegistry 完成本地缓存的服务信息更新。

  2. 消息广播(异步):replicateToPeers 方法先从 PeerEurekaNodes 获取所有的服务器节点,通过 isThisMyUrl 排除自身后,给其余的所有服务器进行消息广播。消息广播的处理是由 PeerEurekaNode 类完成的,这个类的处理都是异步的。

    注意:即使 Eureka Server 宕机,也会进行消息广播,直到任务过期为至。这中间可能会出现数据不同步,但一旦网络恢复后,接收到其它服务器广播的心跳信息,此时会进行数据同步。

最终所有的消息广播都由 PeerEurekaNode 处理,代码如下:

  1. // 消息广播给 PeerEurekaNode 处理
  2. private void replicateInstanceActionsToPeers(Action action, String appName,
  3. String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node) {
  4. try {
  5. InstanceInfo infoFromRegistry = null;
  6. CurrentRequestVersion.set(Version.V2);
  7. switch (action) {
  8. case Cancel:
  9. node.cancel(appName, id);
  10. break;
  11. case Heartbeat:
  12. InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
  13. infoFromRegistry = getInstanceByAppAndId(appName, id, false);
  14. node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
  15. break;
  16. case Register:
  17. node.register(info);
  18. break;
  19. case StatusUpdate:
  20. infoFromRegistry = getInstanceByAppAndId(appName, id, false);
  21. node.statusUpdate(appName, id, newStatus, infoFromRegistry);
  22. break;
  23. case DeleteStatusOverride:
  24. infoFromRegistry = getInstanceByAppAndId(appName, id, false);
  25. node.deleteStatusOverride(appName, id, infoFromRegistry);
  26. break;
  27. }
  28. } catch (Throwable t) {
  29. }
  30. }

总结: 这个代码就不细说了,接下来就要重点分析 PeerEurekaNode 是如何进行消息转发的。

2.2 PeerEurekaNode 消息处理

2.2.1 消息处理整体流程分析

图3:Eureka 消息批处理时序图

sequenceDiagram
participant PeerEurekaNode
participant batchingTaskDispatcher
participant BatchWorkerRunnable
participant AcceptorExecutor
participant ReplicationTaskProcessor
participant JerseyReplicationClient
PeerEurekaNode ->> batchingTaskDispatcher : process -> (register/cancel/heartbeat/...)
batchingTaskDispatcher ->> AcceptorExecutor : process
batchingTaskDispatcher ->>+ BatchWorkerRunnable : run
BatchWorkerRunnable ->> AcceptorExecutor : requestWorkItems
BatchWorkerRunnable ->>- ReplicationTaskProcessor : process(List<ReplicationTask> tasks)
ReplicationTaskProcessor ->> JerseyReplicationClient : submitBatchUpdates -> `POST: peerreplication/batch`
opt 处理失败
ReplicationTaskProcessor -->> AcceptorExecutor : reprocess
end

总结: PeerEurekaNode 收到请求后,将请求转发给 TaskDispatcher,TaskDispatcher 内部维护一个阻塞队列。即然是阻塞队列那就肯定有消费线程了,这个线程就是 WorkerRunnable。WorkerRunnable 不断轮询,只要有任务是调用 ReplicationTaskProcessor 进行数据同步。如果同步失败进行重试,直到任务失效。这样再配合周期性的心跳检测,就能保证数据的最终一致性了。

nonBatchingDispatcher 和 batchingTaskDispatcher 类似,就不多介绍了。

思考: 如果同时有大量的数据需要同步给其它服务器,此时会发起多个网络请求,有什么好办法?

Eureka 考虑到了这个问题,具体措施就是将多个请求合并成一个请求进行处理,这就是 batchingTaskDispatcher 和 nonBatchingDispatcher 的区别。

消息广播核心类功能分析

PeerEurekaNode 接收消息广播任务后,统一由 TaskDispatcher 进行异步处理。TaskDispatcher 将任务的接收和处理分别交由不同的线程完成,即典型的 Acceptor - Worker 模式。WorkerRunnable 通过 AcceptorExecutor#requestWorkItems 获取即将执行的任务后,调用 ReplicationTaskProcessor 执行消息广播任务。

  • 数据同步(PeerEurekaNode):接收消息广播任务。
  • 任务分发(TaskDispatcher):统一调度 PeerEurekaNode 接收的消息广播任务。实际接收消息广播由线程 AcceptorExecutor 处理,执行由 WorkerRunnable 处理。
  • 任务管理(AcceptorExecutor):统一管理所有的任务。
  • 执行线程(WorkerRunnable):消息广播任务执行线程。
  • 任务处理(ReplicationTaskProcessor):执行数据同步。

2.2.2 初始化

PeerEurekaNode 内部有两个重要的变量:一是 batchingDispatcher 批处理;二是 nonBatchingDispatcher 单独处理器。这二个任务派发器都是异步处理的。

  1. PeerEurekaNode(PeerAwareInstanceRegistry registry, String targetHost,
  2. String serviceUrl, HttpReplicationClient replicationClient,
  3. EurekaServerConfig config, int batchSize, long maxBatchingDelayMs,
  4. long retrySleepTimeMs, long serverUnavailableSleepTimeMs) {
  5. this.registry = registry;
  6. this.targetHost = targetHost;
  7. this.replicationClient = replicationClient; // HTTP客户端
  8. this.serviceUrl = serviceUrl;
  9. this.config = config;
  10. this.maxProcessingDelayMs = config.getMaxTimeForReplication();
  11. // 任务处理器,真正进行消息转发
  12. ReplicationTaskProcessor taskProcessor = new ReplicationTaskProcessor(targetHost, replicationClient);
  13. // 批处理
  14. String batcherName = getBatcherName();
  15. this.batchingDispatcher = TaskDispatchers.createBatchingTaskDispatcher(
  16. batcherName,
  17. config.getMaxElementsInPeerReplicationPool(),
  18. batchSize,
  19. config.getMaxThreadsForPeerReplication(),
  20. maxBatchingDelayMs,
  21. serverUnavailableSleepTimeMs,
  22. retrySleepTimeMs,
  23. taskProcessor
  24. );
  25. // 单独处理
  26. this.nonBatchingDispatcher = TaskDispatchers.createNonBatchingTaskDispatcher(
  27. targetHost,
  28. config.getMaxElementsInStatusReplicationPool(),
  29. config.getMaxThreadsForStatusReplication(),
  30. maxBatchingDelayMs,
  31. serverUnavailableSleepTimeMs,
  32. retrySleepTimeMs,
  33. taskProcessor
  34. );
  35. }

总结: PeerEurekaNode 所有的消息都是异步处理的,分为 batchingDispatcher 和 nonBatchingDispatcher 两种情况。为什么会有批处理了呢?很显然,如何有大量的消息需要转发给另一台服务器,如何一条条发送会浪费网络,这时可以将多个消息合并成一个消息进行发送,这就是 batchingDispatcher 的功能。

2.2.3 任务接收

我们看一下 PeerEurekaNode 接收任务,以注册为例:

  1. public void register(final InstanceInfo info) throws Exception {
  2. long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
  3. // 任务id、任务内容task、任务过期时间expiryTime
  4. batchingDispatcher.process(
  5. taskId("register", info),
  6. new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
  7. public EurekaHttpResponse<Void> execute() {
  8. return replicationClient.register(info);
  9. }
  10. }, expiryTime);
  11. }

总结: PeerEurekaNode 收到消息广播任务后,会由 TaskDispatcher 完成任务的调度。TaskDispatcher 将任务的接收实际委托给了 AcceptorExecutor 线程完成。TaskDispatcher 将任务的接收和处理分别交由不同的线程完成,这是一种典型的 Acceptor - Worker 模式。相关原理会在第三小节进行详细的分析。

2.2.4 任务处理

TaskDispatcher 是一种典型的 Acceptor - Worker 模式。batchingDispatcher 通过 AcceptorExecutor 线程接收任务后,处理就交给 BatchWorkerRunnable 线程。

(1) TaskDispatcher 任务调度

消息处理是在 TaskDispatcher 中完成的,下面以 BatchWorkerRunnable 为例,分析批处理的原理。

  1. public void run() {
  2. try {
  3. while (!isShutdown.get()) {
  4. // 1. 获取要转发的消息,TaskHolder 持有的都是 InstanceReplicationTask
  5. List<TaskHolder<ID, T>> holders = getWork();
  6. metrics.registerExpiryTimes(holders);
  7. List<T> tasks = getTasksOf(holders);
  8. // 2. 请求转发
  9. ProcessingResult result = processor.process(tasks);
  10. // 3. 结果处理,网络IO失败会调用reprocess重试,其它未知异常则取消任务
  11. switch (result) {
  12. case Success:
  13. break;
  14. case Congestion: // 服务器忙,服务器有竞争
  15. case TransientError:// 网络异常,IOException
  16. taskDispatcher.reprocess(holders, result);
  17. break;
  18. case PermanentError:// 其它未知异常
  19. logger.warn("Discarding {} tasks of {} due to permanent error", holders.size(), workerName);
  20. }
  21. metrics.registerTaskResult(result, tasks.size());
  22. }
  23. } catch (InterruptedException e) {
  24. } catch (Throwable e) {
  25. }
  26. }

总结: TaskDispatcher#BatchWorkerRunnable 负责调度任务,请求的处理还是由 ReplicationTaskProcessor 完成的。需要关注一下 Eureka 异常的处理:

  1. 对方服务器忙或网络IO异常,则会调用 reprocess 进行重试。
  2. 其它未知异常,则统一取消任务。

(2) ReplicationTaskProcessor 任务处理

  1. public ProcessingResult process(List<ReplicationTask> tasks) {
  2. // 1. 合并请求
  3. ReplicationList list = createReplicationListOf(tasks);
  4. try {
  5. // 2. 发送请求: POST /peerreplication/batch
  6. EurekaHttpResponse<ReplicationListResponse> response = replicationClient.submitBatchUpdates(list);
  7. int statusCode = response.getStatusCode();
  8. if (!isSuccess(statusCode)) {
  9. // 3.1 服务器忙,重试
  10. if (statusCode == 503) {
  11. return ProcessingResult.Congestion;
  12. } else { // 其它异常,取消任务
  13. return ProcessingResult.PermanentError;
  14. }
  15. } else {
  16. handleBatchResponse(tasks, response.getEntity().getResponseList());
  17. }
  18. } catch (Throwable e) {
  19. // 3.2 读超时,重试
  20. if (maybeReadTimeOut(e)) {
  21. return ProcessingResult.Congestion;
  22. // 3.3 网络IO异常,重试
  23. } else if (isNetworkConnectException(e)) {
  24. logNetworkErrorSample(null, e);
  25. return ProcessingResult.TransientError;
  26. } else { // 其它异常,取消任务
  27. return ProcessingResult.PermanentError;
  28. }
  29. }
  30. return ProcessingResult.Success;
  31. }

总结: 异常可以和上面对照一下,再看一下批处理到底是如何实现的。批处理实际是将多个消息任务 ReplicationTask 合并成一个任务 ReplicationList,而且转发的路径也变成 POST /peerreplication/batch

  1. // 任务合并:List<ReplicationTask> -> ReplicationList
  2. private ReplicationList createReplicationListOf(List<ReplicationTask> tasks) {
  3. ReplicationList list = new ReplicationList();
  4. for (ReplicationTask task : tasks) {
  5. list.addReplicationInstance(
  6. createReplicationInstanceOf((InstanceReplicationTask) task));
  7. }
  8. return list;
  9. }

3. 附录

附录1:EurekaServerConfigBean 主要参数

参数 功能 默认值
peerEurekaNodesUpdateIntervalMs 定时刷新服务列表的时间 10min

每天用心记录一点点。内容也许不重要,但习惯很重要!

Eureka 系列(05)消息广播(上):消息广播原理分析的更多相关文章

  1. RocketMQ延迟消息的代码实战及原理分析

    RocketMQ简介 RocketMQ是一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的.高可靠.万亿级容量.灵活可伸缩的消息发布与订阅服务. 它前身是MetaQ,是阿里基于Kafka ...

  2. Java并发包源码学习系列:阻塞队列BlockingQueue及实现原理分析

    目录 本篇要点 什么是阻塞队列 阻塞队列提供的方法 阻塞队列的七种实现 TransferQueue和BlockingQueue的区别 1.ArrayBlockingQueue 2.LinkedBloc ...

  3. jquery插件--ajaxfileupload.js上传文件原理分析

    英文注解应该是原作者写的吧~说实话,有些if判断里的东西我也没太弄明白,但是大致思路还是OK的. jQuery.extend({ createUploadIframe: function (id, u ...

  4. Eureka 系列(06)消息广播(下):TaskDispacher 之 Acceptor - Worker 模式

    Eureka 系列(06)消息广播(下):TaskDispacher 之 Acceptor - Worker 模式 [TOC] Spring Cloud 系列目录 - Eureka 篇 Eureka ...

  5. SpringCloud 2020.0.4 系列之 Stream 消息广播 与 消息分组 的实现

    1. 概述 老话说的好:事情太多,做不过来,就先把事情记在本子上,然后理清思路.排好优先级,一件一件的去完成. 言归正传,今天我们来聊一下 SpringCloud 的 Stream 组件,Spring ...

  6. spring websocket 和socketjs实现单聊群聊,广播的消息推送详解

    spring websocket 和socketjs实现单聊群聊,广播的消息推送详解 WebSocket简单介绍 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了.近年来,随 ...

  7. Unity3D笔记九 发送广播与消息、利用脚本控制游戏

    一.发送广播与消息 游戏对象之间发送的广播与消息分为三种:第一种向子对象发送,将发送至该对象的同辈对象或者子孙对象中:第二种为给自己发送,发送至自己本身对象:第三种为向父对象发送,发送至该对象的同辈或 ...

  8. [Pulsar系列] 10分钟学会Pulsar消息系统概念

    Apache Pulsar Pulsar是一个支持多租户的.高性能的服务与服务之间消息通讯的解决方案,最初由雅虎开发,现在由Apache软件基金会管理. Pulsar的主要特性如下: Pulsar实例 ...

  9. 微信公众号开发C#系列-6、消息管理-普通消息接受处理

    1.概述 通过前面章节的学习,我们已经对微信的开发有了基本的掌握与熟悉,基本可以上手做复杂的应用了.本篇我们将详细讲解微信消息管理中普通消息的接收与处理.当普通微信用户向公众账号发消息时,微信服务器将 ...

随机推荐

  1. Python 与 C 对比

    到目前为止,我接触最多两种语言应该就是python 和 C 语言了. 个人理解 1. 执行速度不同, python为解释性语言,C是编译型语言(需要编译器) 2. python 是基于C的实现,C中很 ...

  2. pytorch与numpy中的通道交换问题

    pytorch网络输入图像的格式为(C, H, W),而numpy中的图像的shape为(H,W,C) 所以一般需要变换通道,将numpy中的shape变换为torch中的shape. 方法如下: # ...

  3. 后端数据推送-EventSource

    服务器发送事件(以下简称SSE)是HTML 5规范的一个组成部分,可以实现服务器到客户端的单向数据通信.通过SSE,客户端可以自动获取数据更新,而不用重复发送HTTP请求.一旦连接建立,“事件”便会自 ...

  4. 解决:python安装mysqldb模块报 EnvironmentError: mysql_config not found

    最近学习python操作mysql需要安装mysqldb模块 出现EnvironmentError: mysql_config not found 经网上查看,需要安装mysql客户端开发库libmy ...

  5. Charles 抓 HTTPS 包

    最新 Charles 破解版下载地址:http://charles.iiilab.com/ 关掉翻墙软件!!!!! 重启 Charles !!!!! 重启浏览器!!!!! 如果是抓手机的HTTPS包, ...

  6. k8s的存储卷

    存储卷查看:kubectl explain pods.spec.volumes 一.简单的存储方式 1)2个容器之间共享存储..(删除则数据消失) apiVersion: v1 kind: Pod m ...

  7. css的9个常用选择器

    1.类选择器(通过类名进行选择) <!DOCTYPE html> <html> <head> <title></title> </he ...

  8. mybatis的缓存机制及用例介绍

    在实际的项目开发中,通常对数据库的查询性能要求很高,而mybatis提供了查询缓存来缓存数据,从而达到提高查询性能的要求. mybatis的查询缓存分为一级缓存和二级缓存,一级缓存是SqlSessio ...

  9. 转载:java集合类数据结构分析

    数组是 最常用的数据结构.数组的特点是长度固定,可以用下标索引,并且所有的元素的类型都是一致的.数组常用的场景有把:从数据库里读取雇员的信息存储为 EmployeeDetail[],把一个字符串转换并 ...

  10. Java多线程的理解和实例

    编写具有多线程程序经常会用到的方法:run(), start(), wait(), notify(), notifyAll(), sleep(), yield(), join() 还有一个关键字:sy ...