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

Spring Cloud 系列目录 - Eureka 篇

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

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

首先回顾一下消息广播的流程,在上一篇 Eureka 系列(05)消息广播(上):消息广播原理分析 中对 Eureka 消息广播的源码进行了分析,PeerAwareInstanceRegistryImpl 将消息广播任务委托给 PeerEurekaNode。PeerEurekaNode 内部采用 TaskDispacher 的 Acceptor - Worker 模式进行异步处理。本文则重点分析这种异步处理机制。

1. Acceptor - Worker 模式原理

图1:Acceptor - Worker 模式原理

sequenceDiagram
participant TaskDispatcher
participant AcceptorExecutor
participant WorkerRunnable
note left of TaskDispatcher : 接收消息广播任务<br/>process
TaskDispatcher ->> AcceptorExecutor : process
WorkerRunnable ->> AcceptorExecutor : requestWorkItem
WorkerRunnable ->> AcceptorExecutor : requestWorkItems
note right of WorkerRunnable : run
opt error
WorkerRunnable -->> AcceptorExecutor : reprocess
end

总结: TaskDispatcher 接收消息广播任务,实际由 AcceptorExecutor 线程处理,之后由 WorkerRunnable 线程执行。WorkerRunnable 线程执行逻辑已经分析过了,下面看一下 AcceptorExecutor 的源码。

AcceptorExecutor 主要方法:

  • process/reprocess 接收消息广播任务,存放到 acceptorQueue/reprocessQueue 队列中。
  • requestWorkItem/requestWorkItems 获取要执行的消息广播任务 singleItemWorkQueue/batchWorkQueue

2. AcceptorExecutor

图2:AcceptorRunner 任务处理

graph LR
A(Client)
B(reprocessQueue)
C(acceptorQueue)
D(pendingTasks<br/>processingOrder)
E(batchWorkQueue)
F(singleItemWorkQueue)
A -- reprocess --> B
A -- process --> C
B -- drainReprocessQueue --> D
C -- drainAcceptorQueue --> D
D -- assignBatchWork --> E
D -- assignSingleItemWork --> F

总结: AcceptorRunner 线程每 10s 轮询一次,消息广播任务从 acceptorQueue -> pendingTasks -> batchWorkQueue,WorkerRunner 执行线程直接获取 batchWorkQueue 任务执行。

如果 pendingTasks 任务超载(默认10000)丢弃的原则是:一是丢弃最老的任务和重试的任务,执行最新的任务。二是同taskId的任务只执行最新的任务

  • pendingTasks 队列满后,reprocessQueue 任务会全部丢弃,acceptorQueue 则丢弃最老的任务执行最新的任务。
  • AcceptorExecutor 的初始化是在 PeerEurekaNode 方法中。默认 pendingTasks 的最大任务数为 maxBufferSize=10000个,一次批处理的最大数为 maxBatchingSize=200个,批处理的最大延迟时间为 maxBatchingDelay=500ms。

2.1 属性

AcceptorExecutor 内部有多个队列,维护任务的执行,队列的功能如下:

  1. private final int maxBufferSize; // pendingTasks队列最大值,默认值 10000
  2. private final int maxBatchingSize; // 一次批处理的最大任务数,默认值 250
  3. private final long maxBatchingDelay; // 任务的最大延迟时间,默认值 500ms
  4. // 1. 接收消息广播的任务
  5. private final BlockingQueue<TaskHolder<ID, T>> acceptorQueue;
  6. private final BlockingDeque<TaskHolder<ID, T>> reprocessQueue;
  7. // 2. 默认每 10s 轮询一次,将接收的消息处理一次
  8. // AcceptorRunner 单线程处理,所以是普通队列
  9. private final Map<ID, TaskHolder<ID, T>> pendingTasks;
  10. private final Deque<ID> processingOrder;
  11. // 3. 即将要处理的消息广播任务
  12. private final Semaphore singleItemWorkRequests;
  13. private final BlockingQueue<TaskHolder<ID, T>> singleItemWorkQueue;
  14. private final Semaphore batchWorkRequests;
  15. private final BlockingQueue<List<TaskHolder<ID, T>>> batchWorkQueue;

2.2 源码分析

2.2.1 AcceptorRunner 总体流程

  1. class AcceptorRunner implements Runnable {
  2. @Override
  3. public void run() {
  4. long scheduleTime = 0;
  5. while (!isShutdown.get()) {
  6. try {
  7. // 1. 将任务从 reprocessQueue/acceptorQueue -> pendingTasks
  8. drainInputQueues();
  9. int totalItems = processingOrder.size();
  10. long now = System.currentTimeMillis();
  11. // 2. trafficShaper 流量整行,执行失败后的延迟时间
  12. // congestionRetryDelayMs 100ms 执行一次
  13. // networkFailureRetryMs 1000ms 执行一次
  14. if (scheduleTime < now) {
  15. scheduleTime = now + trafficShaper.transmissionDelay();
  16. }
  17. // 3. pendingTasks -> batchWorkQueue
  18. if (scheduleTime <= now) {
  19. assignBatchWork();
  20. assignSingleItemWork();
  21. }
  22. // 4. 没有可执行的任务了,等待 10s
  23. if (totalItems == processingOrder.size()) {
  24. Thread.sleep(10);
  25. }
  26. } catch (InterruptedException ex) {
  27. } catch (Throwable e) {
  28. }
  29. }
  30. }
  31. }

总结: AcceptorRunner 线程每 10s 轮询一次,消息广播任务从从 acceptorQueue -> pendingTasks -> batchWorkQueue,WorkerRunner 执行线程直接获取 batchWorkQueue 任务执行。

  • drainInputQueues 任务从 acceptorQueue -> pendingTasks
  • assignBatchWork/assignSingleItemWork 任务从 pendingTasks -> batchWorkQueue

2.2.2 drainInputQueues

drainInputQueues 方法处理 reprocessQueue/acceptorQueue 队列中的任务到 pendingTasks,任务只有到 pendingTasks 中才会被处理,否则就丢弃。

  1. // 先处理 reprocessQueue,再处理 acceptorQueue。
  2. // 默认 acceptorQueue 会覆盖 reprocessQueue 中的任务,也就是最新的任务会覆盖重试的任务
  3. private void drainInputQueues() throws InterruptedException {
  4. do {
  5. drainReprocessQueue(); // reprocessQueue
  6. drainAcceptorQueue(); // acceptorQueue
  7. if (!isShutdown.get()) {
  8. if (reprocessQueue.isEmpty() && acceptorQueue.isEmpty() && pendingTasks.isEmpty()) {
  9. TaskHolder<ID, T> taskHolder = acceptorQueue.poll(10, TimeUnit.MILLISECONDS);
  10. if (taskHolder != null) {
  11. appendTaskHolder(taskHolder);
  12. }
  13. }
  14. }
  15. } while (!reprocessQueue.isEmpty() || !acceptorQueue.isEmpty() || pendingTasks.isEmpty());
  16. }

总结: drainInputQueues 方法将接收的消息广播任务从 reprocessQueue/acceptorQueue -> pendingTasks。如果 pendingTasks 任务大多执行的原则是: 丢弃最老的任务和重试的任务,执行最新的任务。

  1. drainReprocessQueue 处理 reprocessQueue 队列,也就是通过 repocess 接收的任务。当 pendingTasks 队列中的任务超出阈值,重试的任务直接丢弃。
  2. drainAcceptorQueue 处理 acceptorQueue 队列,也就是通过 process 接收的任务。pendingTasks 中最老的任务直接丢弃,将新的任务添加到队列中。
  1. private void drainReprocessQueue() {
  2. long now = System.currentTimeMillis();
  3. // 1. 只要 pendingTasks 没有超过阈值,maxBufferSize=10000
  4. // 就将重试的任务添加到 pendingTasks 中
  5. while (!reprocessQueue.isEmpty() && !isFull()) {
  6. TaskHolder<ID, T> taskHolder = reprocessQueue.pollLast();
  7. ID id = taskHolder.getId();
  8. if (taskHolder.getExpiryTime() <= now) {
  9. expiredTasks++;
  10. } else if (pendingTasks.containsKey(id)) {
  11. overriddenTasks++;
  12. } else {
  13. pendingTasks.put(id, taskHolder);
  14. processingOrder.addFirst(id);
  15. }
  16. }
  17. // 2. reprocessQueue 队列中剩余的任务全部丢弃。
  18. if (isFull()) {
  19. queueOverflows += reprocessQueue.size();
  20. reprocessQueue.clear();
  21. }
  22. }

drainReprocessQueue 方法当任务过多时直接丢弃了重试的任务,drainAcceptorQueue 则不同,丢弃最老的任务,执行最新的任务。目的是保证新任务肯定能执行,而旧的任务根据实际情况丢弃。

  1. private void drainAcceptorQueue() {
  2. while (!acceptorQueue.isEmpty()) {
  3. appendTaskHolder(acceptorQueue.poll());
  4. }
  5. }
  6. private void appendTaskHolder(TaskHolder<ID, T> taskHolder) {
  7. // 1. 如果任务超出阈值,丢弃最老的任务
  8. if (isFull()) {
  9. pendingTasks.remove(processingOrder.poll());
  10. queueOverflows++;
  11. }
  12. // 2. 将最新的任务添加到队列中
  13. TaskHolder<ID, T> previousTask = pendingTasks.put(taskHolder.getId(), taskHolder);
  14. if (previousTask == null) {
  15. processingOrder.add(taskHolder.getId());
  16. } else {
  17. overriddenTasks++;
  18. }
  19. }

总结: 还是那句话,如果 pendingTasks 超出阈值时执行的原则是:

  1. 丢弃最老的任务和重试的任务,执行最新的任务。

  2. 同taskId的任务只执行最新的任务

    appendTaskHolder 会覆盖同名的 taskId 任务,taskId 的生成是在 PeerEurekaNode 接收消息广播任务时生成的,生成的原则是:requestType(任务类型)+ appName(应用名称)+ id(实例id)。任务类型包括:register、cancel、heartbeat、statusUpdate、deleteStatusOverride。

    1. // PeerEurekaNode.process 是会生成 taskId
    2. private static String taskId(String requestType, InstanceInfo info) {
    3. return taskId(requestType, info.getAppName(), info.getId());
    4. }
    5. private static String taskId(String requestType, String appName, String id) {
    6. return requestType + '#' + appName + '/' + id;
    7. }

2.2.3 assignBatchWork

将任务从 pendingTasks 从移动到 batchWorkQueue 中,requestWorkItem 直接获取 batchWorkQueue 进行处理。

  1. // pendingTasks -> batchWorkQueue
  2. void assignBatchWork() {
  3. // 1. pendingTasks 为空则 false,pendingTasks 队列满了肯定为 true。
  4. // 任务的延迟时间不超过 maxBatchingDelay=500ms
  5. if (hasEnoughTasksForNextBatch()) {
  6. if (batchWorkRequests.tryAcquire(1)) {
  7. long now = System.currentTimeMillis();
  8. // 2. 批处理任务最大为 maxBatchingSize=250
  9. int len = Math.min(maxBatchingSize, processingOrder.size());
  10. List<TaskHolder<ID, T>> holders = new ArrayList<>(len);
  11. while (holders.size() < len && !processingOrder.isEmpty()) {
  12. ID id = processingOrder.poll();
  13. TaskHolder<ID, T> holder = pendingTasks.remove(id);
  14. // 3. 任务过期,直接丢弃
  15. if (holder.getExpiryTime() > now) {
  16. holders.add(holder);
  17. } else {
  18. expiredTasks++;
  19. }
  20. }
  21. // 4. 添加到 batchWorkQueue 队列中
  22. if (holders.isEmpty()) {
  23. batchWorkRequests.release();
  24. } else {
  25. batchSizeMetric.record(holders.size(), TimeUnit.MILLISECONDS);
  26. batchWorkQueue.add(holders);
  27. }
  28. }
  29. }
  30. }

总结: assignBatchWork 执行的条件:一是 pendingTasks 任务超载了,立即执行;二是任务延迟时间大于 maxBatchingDelay=500ms。目的就是为了控制任务的执行频率:任务太多或延迟时间过长立即执行。

  1. private boolean hasEnoughTasksForNextBatch() {
  2. if (processingOrder.isEmpty()) {
  3. return false;
  4. }
  5. // 队列中任务大多立即执行 maxBufferSize=10000
  6. if (pendingTasks.size() >= maxBufferSize) {
  7. return true;
  8. }
  9. // 任务延迟时间太长立即执行 maxBatchingDelay=500ms
  10. TaskHolder<ID, T> nextHolder = pendingTasks.get(processingOrder.peek());
  11. long delay = System.currentTimeMillis() - nextHolder.getSubmitTimestamp();
  12. return delay >= maxBatchingDelay;
  13. }

3. 问题总结

3.1 消费速度控制

(1)服务器忙或网络异常

当出现服务器忙或网络IO异常时就需要等待一段时间再发送请求。默认情况下:

  • congestionRetryDelayMs:服务器忙时至少 100ms
  • networkFailureRetryMs:网络IO异常时至少 1000ms
  1. public void run() {
  2. ...
  3. if (scheduleTime < now) {
  4. scheduleTime = now + trafficShaper.transmissionDelay();
  5. }
  6. if (scheduleTime <= now) {
  7. assignBatchWork();
  8. assignSingleItemWork();
  9. }
  10. }

总结: 如果出现服务器忙(503)或网络 IO 异常时,至少要等待一定的时间,再次发送请求。TrafficShaper 是流量整行的意思,即控制请求发送的频率。

  1. long transmissionDelay() {
  2. // 没有任务异常,不能等待
  3. if (lastCongestionError == -1 && lastNetworkFailure == -1) {
  4. return 0;
  5. }
  6. // 出现对方服务器忙,至少 congestionRetryDelayMs=100ms
  7. long now = System.currentTimeMillis();
  8. if (lastCongestionError != -1) {
  9. long congestionDelay = now - lastCongestionError;
  10. if (congestionDelay >= 0 && congestionDelay < congestionRetryDelayMs) {
  11. return congestionRetryDelayMs - congestionDelay;
  12. }
  13. lastCongestionError = -1;
  14. }
  15. // 出现网IO异常,至少 networkFailureRetryMs=1000ms
  16. if (lastNetworkFailure != -1) {
  17. long failureDelay = now - lastNetworkFailure;
  18. if (failureDelay >= 0 && failureDelay < networkFailureRetryMs) {
  19. return networkFailureRetryMs - failureDelay;
  20. }
  21. lastNetworkFailure = -1;
  22. }
  23. return 0;
  24. }

(2)最大任务数限制

如果消息广播任务超出阈值,丢弃的原则是:一是丢弃最老的任务和重试的任务,执行最新的任务。二是同taskId的任务只执行最新的任务。这在 2.2.2小节有详细的说明,默认 maxBufferSize=10000。

(3)批处理任务延迟时间

在 PeerEurekaNode 接收广播任务,生成 TaskHolder 时,会生成任务的提交时间,如果任务延迟赶时间超过 maxBatchingDelay 则立即执行。这个时间是在 hasEnoughTasksForNextBatch 方法中进行控制的。默认 maxBatchingDelay=500ms。

3.2 Semaphore batchWorkRequests 作用分析

在任务的处理过程中都会使用 Semaphore 这个信号锁,它的作用是什么呢?

  1. private final Semaphore batchWorkRequests = new Semaphore(0);
  2. private final BlockingQueue<List<TaskHolder<ID, T>>> batchWorkQueue = new LinkedBlockingQueue<>();
  3. // AcceptorRunner 分配任务
  4. void assignBatchWork() {
  5. if (hasEnoughTasksForNextBatch()) {
  6. if (batchWorkRequests.tryAcquire(1)) {
  7. List<TaskHolder<ID, T>> holders = new ArrayList<>(len);
  8. ...
  9. if (holders.isEmpty()) {
  10. // 如果没有分配任务,下一次可继续分配任务
  11. batchWorkRequests.release();
  12. } else {
  13. // 如果已经分配任务,则必须等到消费才消费才开始重新分配任务
  14. // 如果任务一直没有被消费,则 AcceptorRunner 轮询时会丢弃老的任务
  15. batchWorkQueue.add(holders);
  16. }
  17. }
  18. }
  19. }
  20. // WorkerRunable 获取任务
  21. BlockingQueue<List<TaskHolder<ID, T>>> requestWorkItems() {
  22. batchWorkRequests.release();
  23. return batchWorkQueue;
  24. }

总结: AcceptorRunner 线程轮询时进行任务分配,如果没有获取 Semaphore 锁,也就是说任务一直没有被消费,当 pendingTasks 任务过多,会按照丢弃老的执行新的任务原则进行处理。如果有 WorkerRunable 线程进行消费则会释放锁,重新进行任务分配。


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

Eureka 系列(06)消息广播(下):TaskDispacher 之 Acceptor - Worker 模式的更多相关文章

  1. Ubuntu下配置Apache的Worker模式

    其实Apache本身的并发能力是足够强大的,但是Ubuntu默认安装的是Prefork模式下的Apache.所以导致很多人后面盲目的去 安装lighttpd或者nginx一类替代软件.但是这类软件有一 ...

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

    Eureka 系列(05)消息广播(上):消息广播原理分析 [TOC] 0. Spring Cloud 系列目录 - Eureka 篇 首先回顾一下客户端服务发现的流程,在上一篇 Eureka 系列( ...

  3. Consul实现原理系列文章2: 用Gossip来做集群成员管理和消息广播

    工作中用到了Consul来做服务发现,之后一段时间里,我会陆续发一些文章来讲述Consul实现原理.这篇文章会讲述Consul是如何使用Gossip来做集群成员管理和消息广播的. Consul使用Go ...

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

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

  5. Eureka 系列(07)服务注册与主动下线

    Eureka 系列(07)服务注册与主动下线 [TOC] Spring Cloud 系列目录 - Eureka 篇 在上一篇 Eureka 系列(05)消息广播 中对 Eureka 消息广播的源码进行 ...

  6. Eureka 系列(02)Eureka 一致性协议

    目录 Eureka 系列(02)Eureka 一致性协议 0. Spring Cloud 系列目录 - Eureka 篇 1. 服务发现方案对比 1.1 技术选型 1.2 数据模型 2. Eureka ...

  7. Eureka 系列(08)心跳续约与自动过期

    Eureka 系列(08)心跳续约与自动过期 [TOC] Spring Cloud 系列目录 - Eureka 篇 在上一篇 Eureka 系列(07)服务注册与主动下线 中对服务的注册与下线进行了分 ...

  8. Eureka 系列(04)客户端源码分析

    Eureka 系列(04)客户端源码分析 [TOC] 0. Spring Cloud 系列目录 - Eureka 篇 在上一篇 Eureka 系列(01)最简使用姿态 中对 Eureka 的简单用法做 ...

  9. 进程间通信系列 之 消息队列函数(msgget、msgctl、msgsnd、msgrcv)及其范例

    进程间通信系列 之 概述与对比   http://blog.csdn.net/younger_china/article/details/15808685  进程间通信系列 之 共享内存及其实例   ...

随机推荐

  1. 插件化框架解读之Class文件与Dex文件的结构(一)

    阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680 Class文件 Class文件是Java虚拟机定义并被其所识别的 ...

  2. Structured Streaming本地local运行小例子

    package com.lin.spark import org.apache.spark.sql.SparkSession object StructuredStreaming { def main ...

  3. 如何减少代码中的if-else嵌套

    实际项目中,往往有大量的if-else语句进行各种逻辑校验,参数校验等等,大量的if-else,语句使代码变得臃肿且不好维护,本篇文章结合我自己的经验,就减少if-else语句给出以下几种方案,分别适 ...

  4. spring 事物(三)—— 声明式事务管理详解

    spring的事务处理分为两种: 1.编程式事务:在程序中控制事务开始,执行和提交:详情请点此跳转: 2.声明式事务:在Spring配置文件中对事务进行配置,无须在程序中写代码:(建议使用) 我对&q ...

  5. STM8硬件设计注意事项

    1.中断 STM8的外部中断和STM32不一样,每个端口PX只有1个中断 2.ADC 1)Additional AIN12 analog input is not selectable in ADC ...

  6. Linux查询Java进程以及杀掉其进程

    今天公司VPN掉线后,访问项目出错502. 百度了说是nginx代理错误,但入职不久不知道咋搞... 于是乎就想重启一下Java应用. 1.找到Java应用的进程 jps 命令    和   ps - ...

  7. 解决error: Microsoft Visual C++ 14.0 is required 问题

    1.https://964279924.ctfile.com/fs/1445568-239446865 2.重新安装 .Net framework 更高的版本:https://support.micr ...

  8. 第二则java读取excel文件代码

    // 得到上传文件的保存目录,将上传的文件存放于WEB-INF目录下,不允许外界直接访问,保证上传文件的安全 String savePath = this.getServletContext().ge ...

  9. el-table的样式修改

    修改头部样式: .el-table .el-table__header-wrapper tr th{ background-color: rgb(18, 47, 92)!important; colo ...

  10. 设置php的环境变量 php: command not found

    执行远程服务器上的某个脚本,却报错,提示php:command not found 找不到php命令 which php  结果是/usr/local/php/bin/php echo $PATH 结 ...