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

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

最新文章

  1. C#排序算法小结
  2. xml dtd 内部dtd 外部DTD 公共DTD
  3. Connection broken for id 62, my id = 70, error =
  4. 悲惨记忆。。QImage之 pixel() &amp;&amp; setPixel()参数不要给反了。。。
  5. SpringHttpInvoker解析1-使用示例
  6. 创建一个叫做People的类: 属性:姓名、年龄、性别、身高 行为:说话、计算加法、改名 编写能为所有属性赋值的构造方法; (2)创建主类: 创建一个对象:名叫“张三”,性别“男”,年龄18岁,身高1.80; 让该对象调用成员方法: 说出“你好!” 计算23+45的值 将名字改为“李四”
  7. 第7章 使用RAID与LVM磁盘阵列技术
  8. less-3-混合
  9. POJ 3208-Apocalypse Someday(数位dp)
  10. Qt 操作Excel
  11. Windows7 64位系统搭建Cocos2d-x 2.2.1最新版以及Android交叉编译环境(具体教程)
  12. AndroidAnnotations框架配置
  13. 【hihocoder 1249 Xiongnu&#39;s Land】线性扫描
  14. hdu1443 Joseph---约瑟夫环
  15. UVALive - 3026:Period
  16. JMeter测试工具的使用
  17. selenium常用操作
  18. SecureCRT通过拷贝配置文件登陆
  19. 125 open source Big Data architecture papers for data professionals
  20. parity 钱包

热门文章

  1. 模拟javaWeb责任链的设计
  2. android Manifest.xml 文件详解
  3. LeetCode Array Easy 219. Contains Duplicate II
  4. Redis 设置权限密码,以及如何开启关闭设置
  5. shells - 有效登录 shell 的路径名
  6. SSH工具
  7. linux ---pgbouncer的安装和配置
  8. js使用childnodes获取子节点时多了text节点
  9. js unshift()
  10. 简单的51单片机多任务操作系统(C51)