摘要: 原創出處 http://www.iocoder.cn/Eureka/batch-tasks/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!
本文主要基於 Eureka 1.8.X 版本
- 1. 概述
- 2. 整體流程
- 3. 任務處理器
- 4. 創建任務分發器
- 5. 創建任務接收執行器
- 6. 創建任務執行器
- 7. 網絡通信整形器
- 8. 任務接收執行器【處理任務】
- 9. 任務接收線程【調度任務】
- 10. 任務執行器【執行任務】
- 紫色部分 —— 任務分發器
- 藍色部分 —— 任務接收器
- 紅色部分 —— 任務執行器
- 綠色部分 —— 任務處理器
- 黃色部分 —— 任務持有者( 任務 )
- 請支持正版。下載盜版,等於主動編寫低級 BUG 。
- 程序猿DD —— 《Spring Cloud微服務實戰》
- 周立 —— 《Spring Cloud與Docker微服務架構實戰》
- 兩書齊買,京東包郵。
- 細箭頭 —— 任務執行經歷的操作
- 粗箭頭 —— 任務隊列流轉的方向
不同於一般情況下,任務提交了立即同步或異步執行,任務的執行拆分了三層隊列:
第一層,接收隊列(
acceptorQueue
),重新處理隊列(reprocessQueue
)。- 藍線:分發器在收到任務執行請求後,提交到接收隊列,任務實際未執行。
- 黃線:執行器的工作線程處理任務失敗,將符合條件( 見 「3. 任務處理器」 )的失敗任務提交到重新執行隊列。
第二層,待執行隊列(
processingOrder
)- 粉線:接收線程( Runner )將重新執行隊列,接收隊列提交到待執行隊列。
第三層,工作隊列(
workQueue
)- 粉線:接收線程( Runner )將待執行隊列的任務根據參數(
maxBatchingSize
)將任務合併成批量任務,調度( 提交 )到工作隊列。 - 黃線:執行器的工作線程池,一個工作線程可以拉取一個批量任務進行執行。
- 粉線:接收線程( Runner )將待執行隊列的任務根據參數(
三層隊列的好處:
- 接收隊列,避免處理任務的阻塞等待。
- 接收線程( Runner )合併任務,將相同任務編號( 是的,任務是帶有編號的 )的任務合併,只執行一次。
- Eureka-Server 爲集羣同步提供批量操作多個應用實例的接口,一個批量任務可以一次調度接口完成,避免多次調用的開銷。當然,這樣做的前提是合併任務,這也導致 Eureka-Server 集羣之間對應用實例的註冊和下線帶來更大的延遲。畢竟,Eureka 是在 CAP 之間,選擇了 AP。
- ProcessingResult ,處理任務結果。
Success
,成功。Congestion
,擁擠錯誤,任務將會被重試。例如,請求被限流。TransientError
,瞬時錯誤,任務將會被重試。例如,網絡請求超時。PermanentError
,永久錯誤,任務將會被丟棄。例如,執行時發生程序異常。
#process(task)
方法,處理單任務。#process(tasks)
方法,處理批量任務。#process(...)
方法,提交任務編號,任務,任務過期時間給任務分發器處理。- 批量任務執行的分發器,用於 Eureka-Server 集羣註冊信息的同步任務。
- 單任務執行的分發器,用於 Eureka-Server 向亞馬遜 AWS 的 ASG ( Autoscaling Group ) 同步狀態。雖然本系列暫時對 AWS 相關的不做解析,從工具類的角度來說,本文會對該分發器進行分享。
- 第 1 至 23 行 :方法參數。比較多哈,請耐心理解。
workloadSize
參數,單個批量任務包含任務最大數量。taskProcessor
參數,自定義任務執行器實現。
- 第 24 至 27 行 :創建任務接收執行器。在 「5. 創建任務接收器」 詳細解析。
- 第 28 至 29 行 :創建批量任務執行器。在 「6.1 創建批量任務執行器」 詳細解析。
- 第 30 至 42 行 :創建批量任務分發器。
- 第 32 至 35 行 :
#process()
方法的實現,調用AcceptorExecutor#process(...)
方法,提交 [ 任務編號 , 任務 , 任務過期時間 ] 給任務分發器處理。
- 第 32 至 35 行 :
- 第 1 至 21 行 :方法參數。比較多哈,請耐心理解。
,相比workloadSize
參數#createBatchingTaskDispatcher(...)
少這個參數。在第 24 行,你會發現該參數傳遞給 AcceptorExecutor 使用 1 噢。taskProcessor
參數,自定義任務執行器實現。
- 第 21 至 25 行 :創建任務接收執行器。和
#createBatchingTaskDispatcher(...)
只差workloadSize = 1
參數。在 「5. 創建任務接收器」 詳細解析。 - 第 28 至 29 行 :創建單任務執行器。和
#createBatchingTaskDispatcher(...)
差別很大。「6.2 創建單任務執行器」 詳細解析。 - 第 30 至 42 行 :創建單任務分發器。和
#createBatchingTaskDispatcher(...)
一樣。 - 第 5 至 61 行 :屬性。比較多哈,請耐心理解。
- 眼尖如你,會發現 AcceptorExecutor 即存在單任務工作隊列(
singleItemWorkQueue
),又存在批量任務工作隊列(batchWorkQueue
) ,在 「9. 任務接收線程【調度任務】」 會解答這個疑惑。
- 眼尖如你,會發現 AcceptorExecutor 即存在單任務工作隊列(
- 第 78 至 79 行 :創建網絡通信整形器。在 「7. 網絡通信整形器」 詳細解析。
- 第 81 至 85 行 :創建接收任務線程。
workerThreads
屬性,工作線程池。工作任務隊列會被工作線程池併發拉取,併發執行。com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnableFactory
,創建工作線程工廠接口。單任務和批量任務執行器的工作線程實現不同,通過自定義工廠實現類創建。com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnable.BatchWorkerRunnable
,批量任務工作線程。com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnable.SingleTaskWorkerRunnable
,單任務工作線程。#registerFailure(...)
,在任務執行失敗時,提交任務結果給 TrafficShaper ,記錄發生時間。在 「10. 任務執行器【執行任務】」 會看到調用該方法。#transmissionDelay(...)
,計算提交延遲,單位:毫秒。「9. 任務接收線程【調度任務】」 會看到調用該方法。com.netflix.eureka.util.batcher.TaskHolder
,任務持有者,實現代碼如下:class TaskHolder<ID, T> {/*** 任務編號*/private final ID id;/*** 任務*/private final T task;/*** 任務過期時間戳*/private final long expiryTime;/*** 任務提交時間戳*/private final long submitTimestamp;}- 第 4 行 :無限循環執行調度,直到關閉。
第 6 至 7 行 :調用
#drainInputQueues()
方法,循環處理完輸入隊列( 接收隊列 + 重新執行隊列 ),直到有待執行的任務。實現代碼如下:1: private void drainInputQueues() throws InterruptedException {2: do {3: // 處理完重新執行隊列4: drainReprocessQueue();5: // 處理完接收隊列6: drainAcceptorQueue();7:8: // 所有隊列爲空,等待 10 ms,看接收隊列是否有新任務9: if (!isShutdown.get()) {10: // If all queues are empty, block for a while on the acceptor queue11: if (reprocessQueue.isEmpty() && acceptorQueue.isEmpty() && pendingTasks.isEmpty()) {12: TaskHolder<ID, T> taskHolder = acceptorQueue.poll(10, TimeUnit.MILLISECONDS);13: if (taskHolder != null) {14: appendTaskHolder(taskHolder);15: }16: }17: }18: } while (!reprocessQueue.isEmpty() || !acceptorQueue.isEmpty() || pendingTasks.isEmpty()); // 處理完輸入隊列( 接收隊列 + 重新執行隊列 )19: }- 第 2 行 && 第 18 行 :循環,直到同時滿足如下全部條件:
- 重新執行隊列(
reprocessQueue
) 和接收隊列(acceptorQueue
)爲空 - 待執行任務映射(
pendingTasks
)不爲空
- 重新執行隊列(
第 3 至 4 行 :處理完重新執行隊列(
reprocessQueue
)。實現代碼如下:
1: private void drainReprocessQueue() {2: long now = System.currentTimeMillis();3: while (!reprocessQueue.isEmpty() && !isFull()) {4: TaskHolder<ID, T> taskHolder = reprocessQueue.pollLast(); // 優先拿較新的任務5: ID id = taskHolder.getId();6: if (taskHolder.getExpiryTime() <= now) { // 過期7: expiredTasks++;8: } else if (pendingTasks.containsKey(id)) { // 已存在9: overriddenTasks++;10: } else {11: pendingTasks.put(id, taskHolder);12: processingOrder.addFirst(id); // 提交到隊頭13: }14: }15: // 如果待執行隊列已滿,清空重新執行隊列,放棄較早的任務16: if (isFull()) {17: queueOverflows += reprocessQueue.size();18: reprocessQueue.clear();19: }20: }- 第 4 行 :優先從重新執行任務的隊尾拿較新的任務,從而實現保留更新的任務在待執行任務映射(
pendingTasks
) 裏。 - 第 12 行 :添加任務編號到待執行隊列(
processingOrder
) 的頭部。效果如下圖: - 第 15 至 18 行 :如果待執行隊列(
pendingTasks
)已滿,清空重新執行隊列(processingOrder
),放棄較早的任務。
- 第 4 行 :優先從重新執行任務的隊尾拿較新的任務,從而實現保留更新的任務在待執行任務映射(
第 5 至 6 行 :處理完接收隊列(
acceptorQueue
),實現代碼如下:
private void drainAcceptorQueue() {while (!acceptorQueue.isEmpty()) { // 循環,直到接收隊列爲空appendTaskHolder(acceptorQueue.poll());}}private void appendTaskHolder(TaskHolder<ID, T> taskHolder) {// 如果待執行隊列已滿,移除待處理隊列,放棄較早的任務if (isFull()) {pendingTasks.remove(processingOrder.poll());queueOverflows++;}// 添加到待執行隊列TaskHolder<ID, T> previousTask = pendingTasks.put(taskHolder.getId(), taskHolder);if (previousTask == null) {processingOrder.add(taskHolder.getId());} else {overriddenTasks++;}}
第 8 至 17 行 :當所有隊列爲空,阻塞從接收隊列(
acceptorQueue
) 拉取任務 10 ms。若拉取到,添加到待執行隊列(processingOrder
)。
- 第 2 行 && 第 18 行 :循環,直到同時滿足如下全部條件:
第 12 至 16 行 :計算可調度任務的最小時間(
scheduleTime
)。- 當
scheduleTime
小於當前時間,不重新計算,即此時需要延遲等待調度。 - 當
scheduleTime
大於等於當前時間,配合TrafficShaper#transmissionDelay(...)
重新計算。
- 當
- 第 19 行 :當
scheduleTime
小於當前時間,執行任務的調度。 第 21 行 :調用
#assignBatchWork()
方法,調度批量任務。實現代碼如下:
1: void assignBatchWork() {2: if (hasEnoughTasksForNextBatch()) {3: // 獲取 批量任務工作請求信號量4: if (batchWorkRequests.tryAcquire(1)) {5: // 獲取批量任務6: long now = System.currentTimeMillis();7: int len = Math.min(maxBatchingSize, processingOrder.size());8: List<TaskHolder<ID, T>> holders = new ArrayList<>(len);9: while (holders.size() < len && !processingOrder.isEmpty()) {10: ID id = processingOrder.poll();11: TaskHolder<ID, T> holder = pendingTasks.remove(id);12: if (holder.getExpiryTime() > now) { // 過期13: holders.add(holder);14: } else {15: expiredTasks++;16: }17: }18: //19: if (holders.isEmpty()) { // 未調度到批量任務,釋放請求信號量20: batchWorkRequests.release();21: } else { // 添加批量任務到批量任務工作隊列22: batchSizeMetric.record(holders.size(), TimeUnit.MILLISECONDS);23: batchWorkQueue.add(holders);24: }25: }26: }27: }第 2 行 :調用
#hasEnoughTasksForNextBatch()
方法,判斷是否有足夠任務進行下一次批量任務調度:1)待執行任務(processingOrder
)映射已滿;或者 2)到達批量任務處理最大等待延遲。實現代碼如下:private boolean hasEnoughTasksForNextBatch() {// 待執行隊列爲空if (processingOrder.isEmpty()) {return false;}// 待執行任務映射已滿if (pendingTasks.size() >= maxBufferSize) {return true;}// 到達批量任務處理最大等待延遲( 通過待處理隊列的頭部任務判斷 )TaskHolder<ID, T> nextHolder = pendingTasks.get(processingOrder.peek());long delay = System.currentTimeMillis() - nextHolder.getSubmitTimestamp();return delay >= maxBatchingDelay;}- x
第 5 至 17 行 :獲取批量任務(
holders
)。�� 你會發現,本文說了半天的批量任務,實際是List<TaskHolder<ID, T>>
哈。
- 第 4 行 :獲取批量任務工作請求信號量(
batchWorkRequests
) 。在任務執行器的批量任務執行器,每次執行時,發出batchWorkRequests
。每一個信號量需要保證獲取到一個批量任務。 - 第 19 至 20 行 :未調度到批量任務,釋放請求信號量,代表請求實際未完成,每一個信號量需要保證獲取到一個批量任務。
- 第 21 至 24 行 :添加批量任務到批量任務工作隊列。
- 第 23 行 :調用
#assignSingleItemWork()
方法,調度單任務。
第 23 行 :調用
#assignSingleItemWork()
方法,調度單任務,和#assignBatchWork()
方法類似。實現代碼如下:
void assignSingleItemWork() {if (!processingOrder.isEmpty()) { // 待執行任隊列不爲空// 獲取 單任務工作請求信號量if (singleItemWorkRequests.tryAcquire(1)) {// 【循環】獲取單任務long now = System.currentTimeMillis();while (!processingOrder.isEmpty()) {ID id = processingOrder.poll(); // 一定不爲空TaskHolder<ID, T> holder = pendingTasks.remove(id);if (holder.getExpiryTime() > now) {singleItemWorkQueue.add(holder);return;}expiredTasks++;}// 獲取不到單任務,釋放請求信號量singleItemWorkRequests.release();}}}- x
第 26 至 31 行 :當調度任務前的待執行任務數(
totalItems
)等於當前待執行隊列(processingOrder
)的任務數,意味着:1)任務執行器無任務請求,正在忙碌處理之前的任務;或者 2)任務延遲調度。睡眠 10 秒,避免資源浪費。
- 第 4 行 :無限循環執行調度,直到關閉。
第 6 行 :調用
getWork()
方法,獲取一個批量任務直到成功。實現代碼如下:1: private List<TaskHolder<ID, T>> getWork() throws InterruptedException {2: // 發起請求信號量,並獲得批量任務的工作隊列3: BlockingQueue<List<TaskHolder<ID, T>>> workQueue = taskDispatcher.requestWorkItems();4: // 【循環】獲取批量任務,直到成功5: List<TaskHolder<ID, T>> result;6: do {7: result = workQueue.poll(1, TimeUnit.SECONDS);8: } while (!isShutdown.get() && result == null);9: return result;10: }第 3 行 :調用
TaskDispatcher#requestWorkItems()
方法,發起請求信號量,並獲得批量任務的工作隊列。實現代碼如下:// TaskDispatcher.java/*** 批量任務工作請求信號量*/private final Semaphore batchWorkRequests = new Semaphore(0);/*** 批量任務工作隊列*/private final BlockingQueue<List<TaskHolder<ID, T>>> batchWorkQueue = new LinkedBlockingQueue<>();BlockingQueue<List<TaskHolder<ID, T>>> requestWorkItems() {batchWorkRequests.release();return batchWorkQueue;}- 注意,批量任務工作隊列(
batchWorkQueue
) 和單任務工作隊列(singleItemWorkQueue
) 是不同的隊列。
- 注意,批量任務工作隊列(
第 5 至 8 行 :循環獲取一個批量任務,直到成功。
第 12 行 :調用
#getTasksOf(...)
方法,獲得實際批量任務。實現代碼如下:
private List<T> getTasksOf(List<TaskHolder<ID, T>> holders) {List<T> tasks = new ArrayList<>(holders.size());for (TaskHolder<ID, T> holder : holders) {tasks.add(holder.getTask());}return tasks;}- x
第 14 至 24 行 :調用處理器( TaskProcessor ) 執行任務。當任務執行結果爲
Congestion
或TransientError
,調用AcceptorExecutor#reprocess(...)
提交整個批量任務重新處理,實現代碼如下:
// AcceptorExecutor.javavoid reprocess(List<TaskHolder<ID, T>> holders, ProcessingResult processingResult) {// 添加到 重新執行隊列reprocessQueue.addAll(holders);// TODO 芋艿:監控相關,暫時無視replayedTasks += holders.size();// 提交任務結果給 TrafficShapertrafficShaper.registerFailure(processingResult);}
1. 概述
本文主要分享 任務批處理。Eureka-Server 集羣通過任務批處理同步應用實例註冊實例,所以本文也是爲 Eureka-Server 集羣同步的分享做鋪墊。
本文涉及類在 com.netflix.eureka.util.batcher
包下,涉及到主體類的類圖如下( 打開大圖 ):
推薦 Spring Cloud 書籍:
2. 整體流程
任務執行的整體流程如下( 打開大圖 ):
3. 任務處理器
com.netflix.eureka.util.batcher.TaskProcessor
,任務處理器接口。接口代碼如下:
|
4. 創建任務分發器
com.netflix.eureka.util.batcher.TaskDispatcher
,任務分發器接口。接口代碼如下:
|
com.netflix.eureka.util.batcher.TaskDispatchers
,任務分發器工廠類,用於創建任務分發器。其內部提供兩種任務分發器的實現:
com.netflix.eureka.cluster.ReplicationTaskProcessor
,實現 TaskDispatcher ,Eureka-Server 集羣任務處理器。感興趣的同學,可以點擊鏈接自己研究,我們將在 《Eureka 源碼解析 —— Eureka-Server 集羣同步》 有詳細解析。
4.1 批量任務執行分發器
調用 TaskDispatchers#createBatchingTaskDispatcher(...)
方法,創建批量任務執行的分發器,實現代碼如下:
|
4.2 單任務執行分發器
調用 TaskDispatchers#createNonBatchingTaskDispatcher(...)
方法,創建單任務執行的分發器,實現代碼如下:
|
5. 創建任務接收執行器
com.netflix.eureka.util.batcher.AcceptorExecutor
,任務接收執行器。創建構造方法代碼如下:
|
6. 創建任務執行器
com.netflix.eureka.util.batcher.TaskExecutors
,任務執行器。其內部提供創建單任務和批量任務執行器的兩種方法。TaskExecutors 構造方法如下:
|
6.1 創建批量任務執行器
調用 TaskExecutors#batchExecutors(...)
方法,創建批量任務執行器。實現代碼如下:
|
6.2 創建單任務執行器
調用 TaskExecutors#singleItemExecutors(...)
方法,創建批量任務執行器。實現代碼如下:
|
6.3 工作線程抽象類
com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnable
,任務工作線程抽象類。BatchWorkerRunnable 和 SingleTaskWorkerRunnable 都實現該類,差異在 #run()
的自定義實現。WorkerRunnable 實現代碼如下:
|
7. 網絡通信整形器
com.netflix.eureka.util.batcher.TrafficShaper
,網絡通信整形器。當任務執行發生請求限流,或是請求網絡失敗的情況,則延時 AcceptorRunner 將任務提交到工作任務隊列,從而避免任務很快去執行,再次發生上述情況。TrafficShaper 實現代碼如下:
|
8. 任務接收執行器【處理任務】
調用 AcceptorExecutor#process(...)
方法,添加任務到接收任務隊列。實現代碼如下:
|
9. 任務接收線程【調度任務】
後臺線程執行 AcceptorRunner#run(...)
方法,調度任務。實現代碼如下:
|
10. 任務執行器【執行任務】
10.1 批量任務工作線程
批量任務工作後臺線程( BatchWorkerRunnable )執行 #run(...)
方法,調度任務。實現代碼如下:
|
10.2 單任務工作線程
單任務工作後臺線程( SingleTaskWorkerRunnable )執行 #run(...)
方法,調度任務,和 BatchWorkerRunnable#run(...)
基本類似,就不囉嗦了。實現代碼如下:
|