注册中心 Eureka 源码解析 —— 任务批处理

点击上方“芋道源码”,选择“置顶公众号”

技术文章第一时间送达!

源码精品专栏

 
  • 精尽 Dubbo 原理与源码专栏( 已经完成 69+ 篇,预计总共 75+ 篇 )

  • 中文详细注释的开源项目

  • Java 并发源码合集

  • RocketMQ 源码合集

  • Sharding-JDBC 源码解析合集

  • Spring MVC 和 Security 源码合集

  • MyCAT 源码解析合集



本文主要基于 Eureka 1.8.X 版本

  • 1. 概述

  • 2. 整体流程

  • 3. 任务处理器

  • 4. 创建任务分发器

    • 4.1 批量任务执行分发器

    • 4.2 单任务执行分发器

  • 5. 创建任务接收执行器

  • 6. 创建任务执行器

    • 6.1 创建批量任务执行器

    • 6.2 创建单任务执行器

    • 6.3 工作线程抽象类

  • 7. 网络通信整形器

  • 8. 任务接收执行器【处理任务】

  • 9. 任务接收线程【调度任务】

  • 10. 任务执行器【执行任务】

    • 10.1 批量任务工作线程

    • 10.2 单任务工作线程

  • 666. 彩蛋


1. 概述

本文主要分享 任务批处理。Eureka-Server 集群通过任务批处理同步应用实例注册实例,所以本文也是为 Eureka-Server 集群同步的分享做铺垫。

本文涉及类在 com.netflix.eureka.util.batcher 包下,涉及到主体类的类图如下( 打开大图 ):

注册中心 Eureka 源码解析 —— 任务批处理_第1张图片
  • 紫色部分 —— 任务分发器

  • 蓝色部分 —— 任务接收器

  • 红色部分 —— 任务执行器

  • 绿色部分 —— 任务处理器

  • 黄色部分 —— 任务持有者( 任务 )

推荐 Spring Cloud 书籍

  • 请支持正版。下载盗版,等于主动编写低级 BUG 。

  • 程序猿DD —— 《Spring Cloud微服务实战》

  • 周立 —— 《Spring Cloud与Docker微服务架构实战》

  • 两书齐买,京东包邮。

推荐 Spring Cloud 视频

  • Java 微服务实践 - Spring Boot

  • Java 微服务实践 - Spring Cloud

  • Java 微服务实践 - Spring Boot / Spring Cloud

2. 整体流程

任务执行的整体流程如下( 打开大图 ):

注册中心 Eureka 源码解析 —— 任务批处理_第2张图片
  • 细箭头 —— 任务执行经历的操作

  • 粗箭头 —— 任务队列流转的方向

  • 不同于一般情况下,任务提交了立即同步或异步执行,任务的执行拆分了三层队列

    • 蓝线:分发器在收到任务执行请求后,提交到接收队列,任务实际未执行

    • 黄线:执行器的工作线程处理任务失败,将符合条件( 见 「3. 任务处理器」 )的失败任务提交到重新执行队列。

    • 第一层,接收队列( acceptorQueue ),重新处理队列( reprocessQueue )。

  • 第二层,待执行队列( processingOrder )
    * 粉线:接收线程( Runner )将重新执行队列,接收队列提交到待执行队列。

  • 第三层,工作队列( workQueue )
    * 粉线:接收线程( Runner )将待执行队列的任务根据参数( maxBatchingSize )将任务合并成批量任务,调度( 提交 )到工作队列。
    * 黄线:执行器的工作线程,一个工作线程可以拉取一个批量任务进行执行。

  • 三层队列的好处

    • 接收队列,避免处理任务的阻塞等待。

    • 接收线程( Runner )合并任务,将相同任务编号( 是的,任务是带有编号的 )的任务合并,只执行一次。

    • Eureka-Server 为集群同步提供批量操作多个应用实例的接口,一个批量任务可以一次调度接口完成,避免多次调用的开销。当然,这样做的前提是合并任务,这也导致 Eureka-Server 集群之间对应用实例的注册和下线带来更大的延迟。毕竟,Eureka 是在 CAP 之间,选择了 AP

3. 任务处理器

com.netflix.eureka.util.batcher.TaskProcessor ,任务处理器接口。接口代码如下:

// ... 省略代码,超过微信文章上限
  • ProcessingResult ,处理任务结果。

    • `Success` ,成功。

    • `Congestion` ,拥挤错误,任务将会被重试。例如,请求被限流。

    • `TransientError` ,瞬时错误,任务将会被重试。例如,网络请求超时。

    • `PermanentError` ,永久错误,任务将会被丢弃。例如,执行时发生程序异常。

  • #process(task) 方法,处理单任务。

  • #process(tasks) 方法,处理批量任务。

4. 创建任务分发器

com.netflix.eureka.util.batcher.TaskDispatcher ,任务分发器接口。接口代码如下:

// ... 省略代码,超过微信文章上限
  • #process(…) 方法,提交任务编号,任务,任务过期时间给任务分发器处理。

com.netflix.eureka.util.batcher.TaskDispatchers ,任务分发器工厂类,用于创建任务分发器。其内部提供两种任务分发器的实现:

  • 批量任务执行的分发器,用于 Eureka-Server 集群注册信息的同步任务。

  • 单任务执行的分发器,用于 Eureka-Server 向亚马逊 AWS 的 ASG ( Autoscaling Group ) 同步状态。虽然本系列暂时对 AWS 相关的不做解析,从工具类的角度来说,本文会对该分发器进行分享。

com.netflix.eureka.cluster.ReplicationTaskProcessor ,实现 TaskDispatcher ,Eureka-Server 集群任务处理器。感兴趣的同学,可以点击链接自己研究,我们将在 《Eureka 源码解析 —— Eureka-Server 集群同步》 有详细解析。

4.1 批量任务执行分发器

调用 TaskDispatchers#createBatchingTaskDispatcher(...) 方法,创建批量任务执行的分发器,实现代码如下:

// TaskDispatchers.java
  1/**
  2:  * 创建批量任务执行的分发器
  3:  *
  4:  * @param id 任务执行器编号
  5:  * @param maxBufferSize 待执行队列最大数量
  6:  * @param workloadSize 单个批量任务包含任务最大数量
  7:  * @param workerCount 任务执行器工作线程数
  8:  * @param maxBatchingDelay 批量任务等待最大延迟时长,单位:毫秒
  9:  * @param congestionRetryDelayMs 请求限流延迟重试时间,单位:毫秒
 10:  * @param networkFailureRetryMs 网络失败延迟重试时长,单位:毫秒
 11:  * @param taskProcessor 任务处理器
 12:  * @param  任务编号泛型
 13:  * @param  任务泛型
 14:  * @return 批量任务执行的分发器
 15:  */

 // ... 省略代码,超过微信文章上限
  • 第 1 至 23 行 :方法参数。比较多哈,请耐心理解。

    • `workloadSize` 参数,单个批量任务包含任务最大数量。

    • `taskProcessor` 参数,自定义任务执行器实现

  • 第 24 至 27 行 :创建任务接收执行器。在 「5. 创建任务接收器」 详细解析。

  • 第 28 至 29 行 :创建批量任务执行器。在 「6.1 创建批量任务执行器」 详细解析。

  • 第 30 至 42 行 :创建批量任务分发器。

    • 第 32 至 35 行 :`#process()` 方法的实现,调用 `AcceptorExecutor#process(…)` 方法,提交 [ 任务编号 , 任务 , 任务过期时间 ] 给任务分发器处理。

4.2 单任务执行分发器

调用 TaskDispatchers#createNonBatchingTaskDispatcher(...) 方法,创建单任务执行的分发器,实现代码如下:

  1/**
  2:  * 创建单任务执行的分发器
  3:  *
  4:  * @param id 任务执行器编号
  5:  * @param maxBufferSize 待执行队列最大数量
  6:  * @param workerCount 任务执行器工作线程数
  7:  * @param maxBatchingDelay 批量任务等待最大延迟时长,单位:毫秒
  8:  * @param congestionRetryDelayMs 请求限流延迟重试时间,单位:毫秒
  9:  * @param networkFailureRetryMs 网络失败延迟重试时长,单位:毫秒
 10:  * @param taskProcessor 任务处理器
 11:  * @param  任务编号泛型
 12:  * @param  任务泛型
 13:  * @return 单任务执行的分发器
 14:  */

 15public static  TaskDispatcher createNonBatchingTaskDispatcher(String id,
 16:                                                                             int maxBufferSize,
 17:                                                                             int workerCount,
 18:                                                                             long maxBatchingDelay,
 19:                                                                             long congestionRetryDelayMs,
 20:                                                                             long networkFailureRetryMs,
 21:                                                                             TaskProcessor taskProcessor)
 
{
 22:     // 创建 任务接收执行器
 23:     final AcceptorExecutor acceptorExecutor = new AcceptorExecutor<>(
 24:             id, maxBufferSize, /* workloadSize = 1 */1, maxBatchingDelay, congestionRetryDelayMs, networkFailureRetryMs
 25:     );
 26:     final TaskExecutors taskExecutor = TaskExecutors.singleItemExecutors(id, workerCount, taskProcessor, acceptorExecutor);
 27:     return new TaskDispatcher() {
 28:         @Override
 29:         public void process(ID id, T task, long expiryTime) {
 30:             acceptorExecutor.process(id, task, expiryTime);
 31:         }
 32
 33:         @Override
 34:         public void shutdown() {
 35:             acceptorExecutor.shutdown();
 36:             taskExecutor.shutdown();
 37:         }
 38:     };
 39: }
  • 第 1 至 21 行 :方法参数。比较多哈,请耐心理解。

    • `workloadSize` 参数,相比 `#createBatchingTaskDispatcher(…)` 少这个参数。在第 24 行,你会发现该参数传递给 AcceptorExecutor 使用 1 噢

    • `taskProcessor` 参数,自定义任务执行器实现

  • 第 21 至 25 行 :创建任务接收执行器。和 #createBatchingTaskDispatcher(…) 只差 workloadSize = 1 参数。在 「5. 创建任务接收器」 详细解析。

  • 第 28 至 29 行 :创建任务执行器。和 `#createBatchingTaskDispatcher(…)` 差别很大。「6.2 创建单任务执行器」 详细解析。

  • 第 30 至 42 行 :创建任务分发器。和 #createBatchingTaskDispatcher(…) 一样。

5. 创建任务接收执行器

com.netflix.eureka.util.batcher.AcceptorExecutor ,任务接收执行器。创建构造方法代码如下:

// ... 省略代码,超过微信文章上限
  • 第 5 至 61 行 :属性。比较多哈,请耐心理解。

    • 眼尖如你,会发现 AcceptorExecutor 即存在单任务工作队列( `singleItemWorkQueue` ),又存在批量任务工作队列( `batchWorkQueue` ) ,在 「9. 任务接收线程【调度任务】」 会解答这个疑惑。

  • 第 78 至 79 行 :创建网络通信整形器。在 「7. 网络通信整形器」 详细解析。

  • 第 81 至 85 行 :创建接收任务线程

6. 创建任务执行器

com.netflix.eureka.util.batcher.TaskExecutors ,任务执行器。其内部提供创建单任务和批量任务执行器的两种方法。TaskExecutors 构造方法如下:

// ... 省略代码,超过微信文章上限
  • workerThreads 属性,工作线程工作任务队列会被工作线程池并发拉取,并发执行

  • com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnableFactory ,创建工作线程工厂接口。单任务和批量任务执行器的工作线程实现不同,通过自定义工厂实现类创建。

6.1 创建批量任务执行器

调用 TaskExecutors#batchExecutors(...) 方法,创建批量任务执行器。实现代码如下:

/**
* 创建批量任务执行器
*
@param name 任务执行器名
@param workerCount 任务执行器工作线程数
@param processor 任务处理器
@param acceptorExecutor 接收任务执行器
@param  任务编号泛型
@param  任务泛型
@return 批量任务执行器
*/

// ... 省略代码,超过微信文章上限
  • com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnable.BatchWorkerRunnable ,批量任务工作线程。

6.2 创建单任务执行器

调用 TaskExecutors#singleItemExecutors(...) 方法,创建批量任务执行器。实现代码如下:

/**
* 创建单任务执行器
*
@param name 任务执行器名
@param workerCount 任务执行器工作线程数
@param processor 任务处理器
@param acceptorExecutor 接收任务执行器
@param  任务编号泛型
@param  任务泛型
@return 单任务执行器
*/

// ... 省略代码,超过微信文章上限
  • com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnable.SingleTaskWorkerRunnable ,单任务工作线程。

6.3 工作线程抽象类

com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnable ,任务工作线程抽象类。BatchWorkerRunnable 和 SingleTaskWorkerRunnable 都实现该类,差异在 #run() 的自定义实现。WorkerRunnable 实现代码如下:

// ... 省略代码,超过微信文章上限

7. 网络通信整形器

com.netflix.eureka.util.batcher.TrafficShaper ,网络通信整形器。当任务执行发生请求限流,或是请求网络失败的情况,则延时 AcceptorRunner 将任务提交到工作任务队列,从而避免任务很快去执行,再次发生上述情况。TrafficShaper 实现代码如下:

// ... 省略代码,超过微信文章上限
  • #registerFailure(…) ,在任务执行失败时,提交任务结果给 TrafficShaper ,记录发生时间。在 「10. 任务执行器【执行任务】」 会看到调用该方法。

  • #transmissionDelay(…) ,计算提交延迟,单位:毫秒。「9. 任务接收线程【调度任务】」 会看到调用该方法。

8. 任务接收执行器【处理任务】

调用 AcceptorExecutor#process(...) 方法,添加任务到接收任务队列。实现代码如下:

// AcceptorExecutor.java
// ... 省略代码,超过微信文章上限
  • com.netflix.eureka.util.batcher.TaskHolder ,任务持有者,实现代码如下:

    // ... 省略代码,超过微信文章上限

9. 任务接收线程【调度任务】

后台线程执行 AcceptorRunner#run(...) 方法,调度任务。实现代码如下:

// ... 省略代码,超过微信文章上限
  • 第 4 行 :无限循环执行调度,直到关闭。

  • 第 6 至 7 行 :调用 #drainInputQueues() 方法,循环处理完输入队列( 接收队列 + 重新执行队列 ),直到有待执行的任务。实现代码如下:

    // ... 省略代码,超过微信文章上限
    • 第 4 行 :优先从重新执行任务的队尾拿较新的任务,从而实现保留更新的任务在待执行任务映射( pendingTasks ) 里。

    • 第 12 行 :添加任务编号到待执行队列( processingOrder ) 的头部。效果如下图:

      注册中心 Eureka 源码解析 —— 任务批处理_第3张图片
    • 第 15 至 18 行 :如果待执行队列( pendingTasks )已满,清空重新执行队列( processingOrder ),放弃较早的任务。

    • 重新执行队列( reprocessQueue ) 和接收队列( acceptorQueue )为空

    • 待执行任务映射( pendingTasks )不为空

    • 第 2 行 && 第 18 行 :循环,直到同时满足如下全部条件:

    • 第 3 至 4 行 :处理完重新执行队列( reprocessQueue )。实现代码如下:

      // ... 省略代码,超过微信文章上限
    • 第 5 至 6 行 :处理完接收队列( acceptorQueue ),实现代码如下:

      // ... 省略代码,超过微信文章上限
    • 第 8 至 17 行 :当所有队列为空,阻塞从接收队列( acceptorQueue ) 拉取任务 10 ms。若拉取到,添加到待执行队列( processingOrder )。

  • 第 12 至 16 行 :计算可调度任务的最小时间( scheduleTime )。

    • 当 scheduleTime 小于当前时间,不重新计算,即此时需要延迟等待调度。

    • 当 scheduleTime 大于等于当前时间,配合 TrafficShaper#transmissionDelay(…) 重新计算。

  • 第 19 行 :当 scheduleTime 小于当前时间,执行任务的调度。

  • 第 21 行 :调用 #assignBatchWork() 方法,调度批量任务。实现代码如下:

    // ... 省略代码,超过微信文章上限
    • x

    • 第 2 行 :调用 #hasEnoughTasksForNextBatch() 方法,判断是否有足够任务进行下一次批量任务调度:1)待执行任务( processingOrder )映射已满;或者 2)到达批量任务处理最大等待延迟。实现代码如下:

      // ... 省略代码,超过微信文章上限
    • 第 5 至 17 行 :获取批量任务( holders )。

你可能感兴趣的:(注册中心 Eureka 源码解析 —— 任务批处理)