Eureka 源碼解析 —— 任務批處理

摘要: 原創出處 http://www.iocoder.cn/Eureka/batch-tasks/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!

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

  • 1. 概述
  • 2. 整體流程
  • 3. 任務處理器
  • 4. 創建任務分發器

  • 5. 創建任務接收執行器
  • 6. 創建任務執行器
  • 7. 網絡通信整形器
  • 8. 任務接收執行器【處理任務】
  • 9. 任務接收線程【調度任務】
  • 10. 任務執行器【執行任務】


  • 1. 概述

    本文主要分享 任務批處理。Eureka-Server 集羣通過任務批處理同步應用實例註冊實例,所以本文也是爲 Eureka-Server 集羣同步的分享做鋪墊。

    本文涉及類在 com.netflix.eureka.util.batcher 包下,涉及到主體類的類圖如下( 打開大圖 ):

    • 紫色部分 —— 任務分發器
    • 藍色部分 —— 任務接收器
    • 紅色部分 —— 任務執行器
    • 綠色部分 —— 任務處理器
    • 黃色部分 —— 任務持有者( 任務 )

    推薦 Spring Cloud 書籍

    2. 整體流程

    任務執行的整體流程如下( 打開大圖 ):

    • 細箭頭 —— 任務執行經歷的操作
    • 粗箭頭 —— 任務隊列流轉的方向
    • 不同於一般情況下,任務提交了立即同步或異步執行,任務的執行拆分了三層隊列

      • 第一層,接收隊列( acceptorQueue ),重新處理隊列( reprocessQueue )。

        • 藍線:分發器在收到任務執行請求後,提交到接收隊列,任務實際未執行
        • 黃線:執行器的工作線程處理任務失敗,將符合條件( 見 「3. 任務處理器」 )的失敗任務提交到重新執行隊列。
        • 第二層,待執行隊列( processingOrder )

          • 粉線:接收線程( Runner )將重新執行隊列,接收隊列提交到待執行隊列。

        • 第三層,工作隊列( workQueue )

          • 粉線:接收線程( Runner )將待執行隊列的任務根據參數( maxBatchingSize )將任務合併成批量任務,調度( 提交 )到工作隊列。
          • 黃線:執行器的工作線程,一個工作線程可以拉取一個批量任務進行執行。



    • 三層隊列的好處

      • 接收隊列,避免處理任務的阻塞等待。
      • 接收線程( Runner )合併任務,將相同任務編號( 是的,任務是帶有編號的 )的任務合併,只執行一次。
      • Eureka-Server 爲集羣同步提供批量操作多個應用實例的接口,一個批量任務可以一次調度接口完成,避免多次調用的開銷。當然,這樣做的前提是合併任務,這也導致 Eureka-Server 集羣之間對應用實例的註冊和下線帶來更大的延遲。畢竟,Eureka 是在 CAP 之間,選擇了 AP


    3. 任務處理器

    com.netflix.eureka.util.batcher.TaskProcessor ,任務處理器接口。接口代碼如下:


    public interface TaskProcessor<T> {
    /
    * A processed task/task list ends up in one of the following states:
    * <ul>
    * <li>{@code Success} processing finished successfully</li>
    * <li>{@code TransientError} processing failed, but shall be retried later</li>
    * <li>{@code PermanentError} processing failed, and is non recoverable</li>
    * </ul>
    /
    enum ProcessingResult {
    /
    成功
    /
    Success,
    /
    擁擠錯誤
    /
    Congestion,
    /
    瞬時錯誤
    /
    TransientError,
    /
    永久錯誤
    /
    PermanentError
    }
    /
    處理單任務
    * In non-batched mode a single task is processed at a time.
    /
    ProcessingResult process(T task);
    /*
    * 處理批量任務
    For batched mode a collection of tasks is run at a time. The result is provided for the aggregated result,
    * and all tasks are handled in the same way according to what is returned (for example are rescheduled, if the
    * error is transient).
    */
    ProcessingResult process(List<T> tasks);
    }
    • ProcessingResult ,處理任務結果。
      • Success ,成功。
      • Congestion ,擁擠錯誤,任務將會被重試。例如,請求被限流。
      • TransientError ,瞬時錯誤,任務將會被重試。例如,網絡請求超時。
      • PermanentError ,永久錯誤,任務將會被丟棄。例如,執行時發生程序異常。

    • #process(task) 方法,處理單任務。
    • #process(tasks) 方法,處理批量任務。

    4. 創建任務分發器

    com.netflix.eureka.util.batcher.TaskDispatcher ,任務分發器接口。接口代碼如下:


    public interface TaskDispatcher<ID, T> {
    void process(ID id, T task, long expiryTime);
    void shutdown();
    }
    • #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 <ID> 任務編號泛型
    13: * @param <T> 任務泛型
    14: * @return 批量任務執行的分發器
    15: */
    16: public static <ID, T> TaskDispatcher<ID, T> createBatchingTaskDispatcher(String id,
    17: int maxBufferSize,
    18: int workloadSize,
    19: int workerCount,
    20: long maxBatchingDelay,
    21: long congestionRetryDelayMs,
    22: long networkFailureRetryMs,
    23: TaskProcessor<T> taskProcessor) {
    24: // 創建 任務接收執行器
    25: final AcceptorExecutor<ID, T> acceptorExecutor = new AcceptorExecutor<>(
    26: id, maxBufferSize, workloadSize, maxBatchingDelay, congestionRetryDelayMs, networkFailureRetryMs
    27: );
    28: // 創建 批量任務執行器
    29: final TaskExecutors<ID, T> taskExecutor = TaskExecutors.batchExecutors(id, workerCount, taskProcessor, acceptorExecutor);
    30: // 創建 批量任務分發器
    31: return new TaskDispatcher<ID, T>() {
    32: @Override
    33: public void process(ID id, T task, long expiryTime) {
    34: acceptorExecutor.process(id, task, expiryTime);
    35: }
    36:
    37: @Override
    38: public void shutdown() {
    39: acceptorExecutor.shutdown();
    40: taskExecutor.shutdown();
    41: }
    42: };
    43: }
    • 第 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 <ID> 任務編號泛型
    12: * @param <T> 任務泛型
    13: * @return 單任務執行的分發器
    14: /
    15: public static <ID, T> TaskDispatcher<ID, T> createNonBatchingTaskDispatcher(String id,
    16: int maxBufferSize,
    17: int workerCount,
    18: long maxBatchingDelay,
    19: long congestionRetryDelayMs,
    20: long networkFailureRetryMs,
    21: TaskProcessor<T> taskProcessor) {
    22: // 創建 任務接收執行器
    23: final AcceptorExecutor<ID, T> acceptorExecutor = new AcceptorExecutor<>(
    24: id, maxBufferSize, / workloadSize = 1 */1, maxBatchingDelay, congestionRetryDelayMs, networkFailureRetryMs
    25: );
    26: final TaskExecutors<ID, T> taskExecutor = TaskExecutors.singleItemExecutors(id, workerCount, taskProcessor, acceptorExecutor);
    27: return new TaskDispatcher<ID, T>() {
    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 ,任務接收執行器。創建構造方法代碼如下:


    1: class AcceptorExecutor<ID, T> {
    2:
    3: private static final Logger logger = LoggerFactory.getLogger(AcceptorExecutor.class);
    4:
    5: /
    6: * 待執行隊列最大數量
    7: * {@link #processingOrder}
    8: /
    9: private final int maxBufferSize;
    10: /
    11: 單個批量任務包含任務最大數量
    12: /
    13: private final int maxBatchingSize;
    14: /
    15: 批量任務等待最大延遲時長,單位:毫秒
    16: /
    17: private final long maxBatchingDelay;
    18:
    19: /
    20: 是否關閉
    21: /
    22: private final AtomicBoolean isShutdown = new AtomicBoolean(false);
    23: /
    24: 接收任務隊列
    25: /
    26: private final BlockingQueue<TaskHolder<ID, T>> acceptorQueue = new LinkedBlockingQueue<>();
    27: /
    28: 重新執行任務隊列
    29: /
    30: private final BlockingDeque<TaskHolder<ID, T>> reprocessQueue = new LinkedBlockingDeque<>();
    31: /
    32: 接收任務線程
    33: /
    34: private final Thread acceptorThread;
    35:
    36: /
    37: 待執行任務映射
    38: /
    39: private final Map<ID, TaskHolder<ID, T>> pendingTasks = new HashMap<>();
    40: /
    41: 待執行隊列
    42: /
    43: private final Deque<ID> processingOrder = new LinkedList<>();
    44:
    45: /
    46: 單任務工作請求信號量
    47: /
    48: private final Semaphore singleItemWorkRequests = new Semaphore(0);
    49: /
    50: 單任務工作隊列
    51: /
    52: private final BlockingQueue<TaskHolder<ID, T>> singleItemWorkQueue = new LinkedBlockingQueue<>();
    53:
    54: /
    55: 批量任務工作請求信號量
    56: /
    57: private final Semaphore batchWorkRequests = new Semaphore(0);
    58: /
    59: 批量任務工作隊列
    60: /
    61: private final BlockingQueue<List<TaskHolder<ID, T>>> batchWorkQueue = new LinkedBlockingQueue<>();
    62:
    63: /
    64: 網絡通信整形器
    65: */
    66: private final TrafficShaper trafficShaper;
    67:
    68: AcceptorExecutor(String id,
    69: int maxBufferSize,
    70: int maxBatchingSize,
    71: long maxBatchingDelay,
    72: long congestionRetryDelayMs,
    73: long networkFailureRetryMs) {
    74: this.maxBufferSize = maxBufferSize;
    75: this.maxBatchingSize = maxBatchingSize;
    76: this.maxBatchingDelay = maxBatchingDelay;
    77:
    78: // 創建 網絡通信整形器
    79: this.trafficShaper = new TrafficShaper(congestionRetryDelayMs, networkFailureRetryMs);
    80:
    81: // 創建 接收任務線程
    82: ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
    83: this.acceptorThread = new Thread(threadGroup, new AcceptorRunner(), "TaskAcceptor-" + id);
    84: this.acceptorThread.setDaemon(true);
    85: this.acceptorThread.start();
    86:
    87: // TODO (省略代碼)芋艿:監控相關,暫時無視
    88: }
    89: }
    • 第 5 至 61 行 :屬性。比較多哈,請耐心理解。
      • 眼尖如你,會發現 AcceptorExecutor 即存在單任務工作隊列( singleItemWorkQueue ),又存在批量任務工作隊列( batchWorkQueue ) ,在 「9. 任務接收線程【調度任務】」 會解答這個疑惑。

    • 第 78 至 79 行 :創建網絡通信整形器。在 「7. 網絡通信整形器」 詳細解析。
    • 第 81 至 85 行 :創建接收任務線程

    6. 創建任務執行器

    com.netflix.eureka.util.batcher.TaskExecutors ,任務執行器。其內部提供創建單任務和批量任務執行器的兩種方法。TaskExecutors 構造方法如下:


    class TaskExecutors<ID, T> {
    private static final Logger logger = LoggerFactory.getLogger(TaskExecutors.class);
    /
    * 是否關閉
    /
    private final AtomicBoolean isShutdown;
    /
    工作線程池
    /
    private final List<Thread> workerThreads;
    TaskExecutors(WorkerRunnableFactory<ID, T> workerRunnableFactory, int workerCount, AtomicBoolean isShutdown) {
    this.isShutdown = isShutdown;
    this.workerThreads = new ArrayList<>();
    // 創建 工作線程池
    ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
    for (int i = 0; i < workerCount; i++) {
    WorkerRunnable<ID, T> runnable = workerRunnableFactory.create(i);
    Thread workerThread = new Thread(threadGroup, runnable, runnable.getWorkerName());
    workerThreads.add(workerThread);
    workerThread.setDaemon(true);
    workerThread.start();
    }
    }
    /*
    * 創建工作線程工廠
    @param <ID> 任務編號泛型
    * @param <T> 批量任務執行器
    */
    interface WorkerRunnableFactory<ID, T> {
    WorkerRunnable<ID, T> create(int idx);
    }
    }
    • workerThreads 屬性,工作線程工作任務隊列會被工作線程池併發拉取,併發執行
    • com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnableFactory ,創建工作線程工廠接口。單任務和批量任務執行器的工作線程實現不同,通過自定義工廠實現類創建。

    6.1 創建批量任務執行器

    調用 TaskExecutors#batchExecutors(...) 方法,創建批量任務執行器。實現代碼如下:


    /*
    創建批量任務執行器
    @param name 任務執行器名
    * @param workerCount 任務執行器工作線程數
    * @param processor 任務處理器
    * @param acceptorExecutor 接收任務執行器
    * @param <ID> 任務編號泛型
    * @param <T> 任務泛型
    * @return 批量任務執行器
    /
    static <ID, T> TaskExecutors<ID, T> batchExecutors(final String name,
    int workerCount,
    final TaskProcessor<T> processor,
    final AcceptorExecutor<ID, T> acceptorExecutor) {
    final AtomicBoolean isShutdown = new AtomicBoolean();
    final TaskExecutorMetrics metrics = new TaskExecutorMetrics(name);
    // 創建批量任務執行器
    return new TaskExecutors<>(new WorkerRunnableFactory<ID, T>() { // 批量任務工作線程工廠
    @Override
    public WorkerRunnable<ID, T> create(int idx) {
    return new BatchWorkerRunnable<>("TaskBatchingWorker-" + name + '-' + idx / 線程名 */, isShutdown, metrics, processor, acceptorExecutor);
    }
    }, workerCount, isShutdown);
    }
    • com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnable.BatchWorkerRunnable ,批量任務工作線程。

    6.2 創建單任務執行器

    調用 TaskExecutors#singleItemExecutors(...) 方法,創建批量任務執行器。實現代碼如下:


    /*
    創建單任務執行器
    @param name 任務執行器名
    * @param workerCount 任務執行器工作線程數
    * @param processor 任務處理器
    * @param acceptorExecutor 接收任務執行器
    * @param <ID> 任務編號泛型
    * @param <T> 任務泛型
    * @return 單任務執行器
    /
    static <ID, T> TaskExecutors<ID, T> singleItemExecutors(final String name,
    int workerCount,
    final TaskProcessor<T> processor,
    final AcceptorExecutor<ID, T> acceptorExecutor) {
    final AtomicBoolean isShutdown = new AtomicBoolean();
    final TaskExecutorMetrics metrics = new TaskExecutorMetrics(name);
    // 創建單任務執行器
    return new TaskExecutors<>(new WorkerRunnableFactory<ID, T>() { // 單任務工作線程工廠
    @Override
    public WorkerRunnable<ID, T> create(int idx) {
    return new SingleTaskWorkerRunnable<>("TaskNonBatchingWorker-" + name + '-' + idx / 線程名 */, isShutdown, metrics, processor, acceptorExecutor);
    }
    }, workerCount, isShutdown);
    }
    • com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnable.SingleTaskWorkerRunnable ,單任務工作線程。

    6.3 工作線程抽象類

    com.netflix.eureka.util.batcher.TaskExecutors.WorkerRunnable ,任務工作線程抽象類。BatchWorkerRunnable 和 SingleTaskWorkerRunnable 都實現該類,差異在 #run() 的自定義實現。WorkerRunnable 實現代碼如下:


    abstract static class WorkerRunnable<ID, T> implements Runnable {
    /
    * 線程名
    /
    final String workerName;
    /
    是否關閉
    /
    final AtomicBoolean isShutdown;
    final TaskExecutorMetrics metrics;
    /
    任務處理器
    /
    final TaskProcessor<T> processor;
    /
    任務接收執行器
    */
    final AcceptorExecutor<ID, T> taskDispatcher;
    // ... 省略構造方法和 getting 方法。
    }

    7. 網絡通信整形器

    com.netflix.eureka.util.batcher.TrafficShaper ,網絡通信整形器。當任務執行發生請求限流,或是請求網絡失敗的情況,則延時 AcceptorRunner 將任務提交到工作任務隊列,從而避免任務很快去執行,再次發生上述情況。TrafficShaper 實現代碼如下:


    class TrafficShaper {
    /
    * Upper bound on delay provided by configuration.
    /
    private static final long MAX_DELAY = 30 1000;
    /
    * 請求限流延遲重試時間,單位:毫秒
    /
    private final long congestionRetryDelayMs;
    /
    網絡失敗延遲重試時長,單位:毫秒
    /
    private final long networkFailureRetryMs;
    /
    最後請求限流時間戳,單位:毫秒
    /
    private volatile long lastCongestionError;
    /
    最後網絡失敗時間戳,單位:毫秒
    /
    private volatile long lastNetworkFailure;
    TrafficShaper(long congestionRetryDelayMs, long networkFailureRetryMs) {
    this.congestionRetryDelayMs = Math.min(MAX_DELAY, congestionRetryDelayMs);
    this.networkFailureRetryMs = Math.min(MAX_DELAY, networkFailureRetryMs);
    }
    void registerFailure(ProcessingResult processingResult) {
    if (processingResult == ProcessingResult.Congestion) {
    lastCongestionError = System.currentTimeMillis();
    } else if (processingResult == ProcessingResult.TransientError) {
    lastNetworkFailure = System.currentTimeMillis();
    }
    }
    /
    計算提交延遲,單位:毫秒
    @return 延遲
    */
    long transmissionDelay() {
    // 無延遲
    if (lastCongestionError == -1 && lastNetworkFailure == -1) {
    return 0;
    }
    long now = System.currentTimeMillis();
    // 計算最後請求限流帶來的延遲
    if (lastCongestionError != -1) {
    long congestionDelay = now - lastCongestionError;
    if (congestionDelay >= 0 && congestionDelay < congestionRetryDelayMs) { // 範圍內
    return congestionRetryDelayMs - congestionDelay; // 補充延遲
    }
    lastCongestionError = -1; // 重置時間戳
    }
    // 計算最後網絡失敗帶來的延遲
    if (lastNetworkFailure != -1) {
    long failureDelay = now - lastNetworkFailure;
    if (failureDelay >= 0 && failureDelay < networkFailureRetryMs) { // 範圍內
    return networkFailureRetryMs - failureDelay; // 補充延遲
    }
    lastNetworkFailure = -1; // 重置時間戳
    }
    // 無延遲
    return 0;
    }
    }

    8. 任務接收執行器【處理任務】

    調用 AcceptorExecutor#process(...) 方法,添加任務到接收任務隊列。實現代碼如下:


    // AcceptorExecutor.java
    void process(ID id, T task, long expiryTime) {
    acceptorQueue.add(new TaskHolder<ID, T>(id, task, expiryTime));
    acceptedTasks++;
    }
    • 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;
      }

    9. 任務接收線程【調度任務】

    後臺線程執行 AcceptorRunner#run(...) 方法,調度任務。實現代碼如下:


    1: @Override
    2: public void run() {
    3: long scheduleTime = 0;
    4: while (!isShutdown.get()) {
    5: try {
    6: // 處理完輸入隊列( 接收隊列 + 重新執行隊列 )
    7: drainInputQueues();
    8:
    9: // 待執行任務數量
    10: int totalItems = processingOrder.size();
    11:
    12: // 計算調度時間
    13: long now = System.currentTimeMillis();
    14: if (scheduleTime < now) {
    15: scheduleTime = now + trafficShaper.transmissionDelay();
    16: }
    17:
    18: // 調度
    19: if (scheduleTime <= now) {
    20: // 調度批量任務
    21: assignBatchWork();
    22: // 調度單任務
    23: assignSingleItemWork();
    24: }
    25:
    26: // 1)任務執行器無任務請求,正在忙碌處理之前的任務;或者 2)任務延遲調度。睡眠 10 秒,避免資源浪費。
    27: // If no worker is requesting data or there is a delay injected by the traffic shaper,
    28: // sleep for some time to avoid tight loop.
    29: if (totalItems == processingOrder.size()) {
    30: Thread.sleep(10);
    31: }
    32: } catch (InterruptedException ex) {
    33: // Ignore
    34: } catch (Throwable e) {
    35: // Safe-guard, so we never exit this loop in an uncontrolled way.
    36: logger.warn("Discovery AcceptorThread error", e);
    37: }
    38: }
    39: }
    • 第 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 queue
      11: 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 ),放棄較早的任務。

      • 第 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 )。



    • 第 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 秒,避免資源浪費。



    10. 任務執行器【執行任務】

    10.1 批量任務工作線程

    批量任務工作後臺線程( BatchWorkerRunnable )執行 #run(...) 方法,調度任務。實現代碼如下:


    //
    1: @Override
    2: public void run() {
    3: try {
    4: while (!isShutdown.get()) {
    5: // 獲取批量任務
    6: List<TaskHolder<ID, T>> holders = getWork();
    7:
    8: // TODO 芋艿:監控相關,暫時無視
    9: metrics.registerExpiryTimes(holders);
    10:
    11: // 獲得實際批量任務
    12: List<T> tasks = getTasksOf(holders);
    13: // 調用處理器執行任務
    14: ProcessingResult result = processor.process(tasks);
    15: switch (result) {
    16: case Success:
    17: break;
    18: case Congestion:
    19: case TransientError:
    20: taskDispatcher.reprocess(holders, result); // 提交重新處理
    21: break;
    22: case PermanentError:
    23: logger.warn("Discarding {} tasks of {} due to permanent error", holders.size(), workerName);
    24: }
    25:
    26: // TODO 芋艿:監控相關,暫時無視
    27: metrics.registerTaskResult(result, tasks.size());
    28: }
    29: } catch (InterruptedException e) {
    30: // Ignore
    31: } catch (Throwable e) {
    32: // Safe-guard, so we never exit this loop in an uncontrolled way.
    33: logger.warn("Discovery WorkerThread error", e);
    34: }
    35: }
    • 第 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 ) 執行任務。當任務執行結果爲 CongestionTransientError ,調用 AcceptorExecutor#reprocess(...) 提交整個批量任務重新處理,實現代碼如下:


      // AcceptorExecutor.java
      void reprocess(List<TaskHolder<ID, T>> holders, ProcessingResult processingResult) {
      // 添加到 重新執行隊列
      reprocessQueue.addAll(holders);
      // TODO 芋艿:監控相關,暫時無視
      replayedTasks += holders.size();
      // 提交任務結果給 TrafficShaper
      trafficShaper.registerFailure(processingResult);
      }


    10.2 單任務工作線程

    單任務工作後臺線程( SingleTaskWorkerRunnable )執行 #run(...) 方法,調度任務,和 BatchWorkerRunnable#run(...) 基本類似,就不囉嗦了。實現代碼如下:


    @Override
    // SingleTaskWorkerRunnable.java
    public void run() {
    try {
    while (!isShutdown.get()) {
    // 發起請求信號量,並獲得單任務的工作隊列
    BlockingQueue<TaskHolder<ID, T>> workQueue = taskDispatcher.requestWorkItem();
    TaskHolder<ID, T> taskHolder;
    // 【循環】獲取單任務,直到成功
    while ((taskHolder = workQueue.poll(1, TimeUnit.SECONDS)) == null) {
    if (isShutdown.get()) {
    return;
    }
    }
    // TODO 芋艿:監控相關,暫時無視
    metrics.registerExpiryTime(taskHolder);
    if (taskHolder != null) {
    // 調用處理器執行任務
    ProcessingResult result = processor.process(taskHolder.getTask());
    switch (result) {
    case Success:
    break;
    case Congestion:
    case TransientError:
    taskDispatcher.reprocess(taskHolder, result); // 提交重新處理
    break;
    case PermanentError:
    logger.warn("Discarding a task of {} due to permanent error", workerName);
    }
    // TODO 芋艿:監控相關,暫時無視
    metrics.registerTaskResult(result, 1);
    }
    }
    } catch (InterruptedException e) {
    // Ignore
    } catch (Throwable e) {
    // Safe-guard, so we never exit this loop in an uncontrolled way.
    logger.warn("Discovery WorkerThread error", e);
    }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章