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

protected PeerEurekaNode createPeerEurekaNode(String peerEurekaNodeUrl) {
HttpReplicationClient replicationClient = JerseyReplicationClient.createReplicationClient(serverConfig, serverCodecs, peerEurekaNodeUrl);
String targetHost = hostFromUrl(peerEurekaNodeUrl);
if (targetHost == null) {
targetHost = "host";
}
return new PeerEurekaNode(registry, targetHost, peerEurekaNodeUrl, replicationClient, serverConfig);
}

总结: 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 处理,代码如下:

// 消息广播给 PeerEurekaNode 处理
private void replicateInstanceActionsToPeers(Action action, String appName,
String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node) {
try {
InstanceInfo infoFromRegistry = null;
CurrentRequestVersion.set(Version.V2);
switch (action) {
case Cancel:
node.cancel(appName, id);
break;
case Heartbeat:
InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
break;
case Register:
node.register(info);
break;
case StatusUpdate:
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.statusUpdate(appName, id, newStatus, infoFromRegistry);
break;
case DeleteStatusOverride:
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.deleteStatusOverride(appName, id, infoFromRegistry);
break;
}
} catch (Throwable t) {
}
}

总结: 这个代码就不细说了,接下来就要重点分析 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 单独处理器。这二个任务派发器都是异步处理的。

PeerEurekaNode(PeerAwareInstanceRegistry registry, String targetHost,
String serviceUrl, HttpReplicationClient replicationClient,
EurekaServerConfig config, int batchSize, long maxBatchingDelayMs,
long retrySleepTimeMs, long serverUnavailableSleepTimeMs) {
this.registry = registry;
this.targetHost = targetHost;
this.replicationClient = replicationClient; // HTTP客户端 this.serviceUrl = serviceUrl;
this.config = config;
this.maxProcessingDelayMs = config.getMaxTimeForReplication(); // 任务处理器,真正进行消息转发
ReplicationTaskProcessor taskProcessor = new ReplicationTaskProcessor(targetHost, replicationClient);
// 批处理
String batcherName = getBatcherName();
this.batchingDispatcher = TaskDispatchers.createBatchingTaskDispatcher(
batcherName,
config.getMaxElementsInPeerReplicationPool(),
batchSize,
config.getMaxThreadsForPeerReplication(),
maxBatchingDelayMs,
serverUnavailableSleepTimeMs,
retrySleepTimeMs,
taskProcessor
);
// 单独处理
this.nonBatchingDispatcher = TaskDispatchers.createNonBatchingTaskDispatcher(
targetHost,
config.getMaxElementsInStatusReplicationPool(),
config.getMaxThreadsForStatusReplication(),
maxBatchingDelayMs,
serverUnavailableSleepTimeMs,
retrySleepTimeMs,
taskProcessor
);
}

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

2.2.3 任务接收

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

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

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

2.2.4 任务处理

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

(1) TaskDispatcher 任务调度

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

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

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

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

(2) ReplicationTaskProcessor 任务处理

public ProcessingResult process(List<ReplicationTask> tasks) {
// 1. 合并请求
ReplicationList list = createReplicationListOf(tasks);
try {
// 2. 发送请求: POST /peerreplication/batch
EurekaHttpResponse<ReplicationListResponse> response = replicationClient.submitBatchUpdates(list);
int statusCode = response.getStatusCode();
if (!isSuccess(statusCode)) {
// 3.1 服务器忙,重试
if (statusCode == 503) {
return ProcessingResult.Congestion;
} else { // 其它异常,取消任务
return ProcessingResult.PermanentError;
}
} else {
handleBatchResponse(tasks, response.getEntity().getResponseList());
}
} catch (Throwable e) {
// 3.2 读超时,重试
if (maybeReadTimeOut(e)) {
return ProcessingResult.Congestion;
// 3.3 网络IO异常,重试
} else if (isNetworkConnectException(e)) {
logNetworkErrorSample(null, e);
return ProcessingResult.TransientError;
} else { // 其它异常,取消任务
return ProcessingResult.PermanentError;
}
}
return ProcessingResult.Success;
}

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

// 任务合并:List<ReplicationTask> -> ReplicationList
private ReplicationList createReplicationListOf(List<ReplicationTask> tasks) {
ReplicationList list = new ReplicationList();
for (ReplicationTask task : tasks) {
list.addReplicationInstance(
createReplicationInstanceOf((InstanceReplicationTask) task));
}
return list;
}

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. How many groups(DP)

    题意: 定义:设M为数组a的子集(元素可以重复),将M中的元素排序,若排序后的相邻两元素相差不超过2,则M为a中的一个块,块的大小为块中的元素个数 给出长度为n的数组a,1<=n<=200 ...

  2. latex 查找缺失的库文件

    app-portage/pfl contains a program to search in an online database for a Gentoo package containing a ...

  3. APP测试功能点大全

    APP测试要点 APP测试的时候,建议让开发打好包APK和IPA安装包,测试人员自己安装应用,进行测试.在测试过程中需要注意的测试点如下:   1.安装和卸载   ●应用是否可以在IOS不同系统版本或 ...

  4. WebSocket 网页聊天室

    先给大家开一个原始的websocket的连接使用范例 <?php /* * recv是从套接口接收数据,也就是拿过来,但是不知道是什么 * read是读取拿过来的数据,就是要知道recv过来的是 ...

  5. GeneXus笔记本—获取当月的最后一天

    首先获取当前日期 然后赋值为当前年月的第一天  然后加一个月 减去一天 就是当月最后一天 多用于筛选数据时的条件或者区间 我们先随便拉个页面  简单点就好 放入两个textblock 然后点击Even ...

  6. linux 系统磁盘管理体系

    目录 linux 系统磁盘管理体系 一.磁盘的基本概念 二.磁盘的内部结构 三.磁盘的外部结构 四.磁盘的接口及类型 五.fdisk磁盘分区实践 六.gdisk 分区 七.parted 高级分区工具. ...

  7. Codeforces 1195E. OpenStreetMap (单调队列)

    题意:给出一个n*m的矩形.询问矩形上所有的a*b的小矩形的最小值之和. 解法:我们先对每一行用单调栈维护c[i][j]代表从原数组的mp[i][j]到mp[i][j+b-1]的最小值(具体维护方法是 ...

  8. Android Paint类介绍以及浮雕和阴影效果的设置(转)

    转自:https://blog.csdn.net/lpjishu/article/details/45558375 Paint类介绍 Paint即画笔,在绘制文本和图形用它来设置图形颜色, 样式等绘制 ...

  9. Windows 命令提示符

    命令提示符(cmd): 启动:Win+R ,输入cmd回车 切换盘符:盘符名称: 进入文件夹:cd 文件夹名称 进入多级文件夹:cd 文件夹1\文件夹2\文件夹3 返回上一级:cd .. 直接回根路径 ...

  10. vue 组件之间互相传值:兄弟组件通信

    vue 组件之间互相传值:兄弟组件通信我们在项目中经常会遇到兄弟组件通信的情况.在大型项目中我们可以通过引入 vuex 轻松管理各组件之间通信问题,但在一些小型的项目中,我们就没有必要去引入 vuex ...