Dubbo-時間輪設計

前言

Dubbo源碼閱讀分享系列文章,歡迎大家關注點贊

SPI實現部分

  1. Dubbo-SPI機制
  2. Dubbo-Adaptive實現原理
  3. Dubbo-Activate實現原理
  4. Dubbo SPI-Wrapper

註冊中心

  1. Dubbo-聊聊註冊中心的設計

通信

  1. Dubbo-聊聊通信模塊設計

什麼時間輪

時間是一種調度模型, 是一種高效的、批量管理定時任務的調度模型。時間輪一般會實現成一個環形結構,類似一個時鐘,分爲很多槽,一個槽代表一個時間間隔,每個槽使用雙向鏈表存儲定時任務;指針週期性地跳動,跳動到一個槽位,就執行該槽位的定時任務。 image.png

爲什麼要時間輪

至於爲什麼使用時間輪我們可以對比下我們所熟知的數據結構來分析一下時間輪的優勢,這樣我們就明白爲什麼會出現時間輪。

有序隊列

  1. 添加/刪除任務: 遍歷每一個節點, 找到相應的位置插入, 因此時間複雜度爲O(n);
  2. 執行任務: 取出最小定時任務爲首節點, 因此時間複雜度爲O(1);

紅黑樹

有序隊列的缺點在於添加/刪除任務,我們可以通過樹形結構來進行優化添加/刪除,也就是紅黑樹。

  1. 添加/刪除任務: 紅黑樹能將排序的的時間複雜度降到O(logN);
  2. 執行任務: 紅黑樹執行任務節點在最左側節點, 因此按照查詢時間複雜度爲O(logN);

最小堆

堆具有特點必須是完全二叉樹,任一結點的值是其子樹所有結點的最大值或最小值

  1. 添加/刪除任務: 時間複雜度爲O(logN);
  2. 執行任務: 最小節點爲根節點, 時間複雜度爲O(1);

時間輪

對於時間輪的實現一般是環狀+鏈表,這樣子整體複雜度爲:

  1. 添加/刪除任務: 時間複雜度爲O(logN);
  2. 執行任務: 最小節點爲根節點, 時間複雜度爲O(1);

整體上看看上去我們可以選擇紅黑樹、最小堆、時間輪,但是如果是多線程情況,紅黑樹、最小堆執行操作需要鎖住整個內容,而時間輪就不需要,類似分段式鎖的概念,因此更優選擇是時間輪。

時間輪原理

單層時間輪

下圖是一個單層時間輪,假設下圖時間輪的週期是1秒,時間輪中有10個槽位,則每個槽位代表的時間就是100ms,現在有A、B、C三個任務,分別是任務A(230ms後執行)、B(450ms之後運行)、C(1950ms之後運行)。我們可以看到任務A被放到了槽位2,任務B被放到了槽位4,任務C被放到了槽位9,當時間輪轉動到對應的槽時,就會從槽中取出任務判斷是否需要執行。這個裏面涉及一個週期概念,任務C具有一個週期,當時間輪完成一次循環,下次執行到9的時候,任務C纔會執行,目前Dubbo中採用單層時間輪機制。 image.png

多層時間輪

對應多層時間輪就是具有多個時間輪,下圖中具有兩個時間輪,第一層時間輪還是保持和單層時間輪一樣,第二層時間輪爲一個週期爲10秒的時間輪,還是按照上述案例,這個時候A、B任務還是被分配在第一層時間輪,對於C任務,當完成完成一個週期以後,第二層時間輪刻度會執行到1的位置,同時任務C也會被取出到第一層時間輪9的位置,當一層時間輪再次轉動到9的位置的時候,則會觸發任務C,這種將第二層的任務取出放入第一層中稱爲降層,它是爲了保證任務被處理的時間精度。Kafka內部就是採用的這種多層時間輪機制。 image.png

時間輪應用場景

  1. 心跳檢查,Netty中的心跳檢查就是採用時間輪形式;
  2. 超時處理,目前Dubbo中採用時間輪來處理超時調用;
  3. 分佈式鎖續期,目前在分佈式鎖Redisson通過時間輪定時給分佈式鎖續期;
  4. 定時任務,對於分佈式定時任務的調度就是採用的時間輪設計;
  5. 消息中間件,延時隊列消息的中間件一般採用時間輪實現;

Dubbo時間輪實現

Dubbo中時間輪的設計都位於org.apache.dubbo.common.timer包中,我們首先來看下核心接口的設計:

TimerTask

TimerTask封裝了要執行的任務,所有的定時任務都需要繼承TimerTask接口,TimerTask就是任務交接入口,該方法內部只有一個run方法,該方法接收一個Timeout類型。 image.png

Timeout

image.png Timeout與TimerTask一一對應,Timeout主要是爲了獲取定時任務的狀態以及操作定時任務,Timeout與TimerTask兩者的關係類似於線程池返回的Future對象與提交到線程池中的任務對象之間的關係。

Timer

Timer接口定義了定時器的基本行爲,核心是newTimeout方法:提交一個定時任務並返回關聯的Timeout對象。 image.png

HashedWheelTimeout

HashedWheelTimeout是Timeout的唯一實現,它的作用有兩個:

  1. 時間輪中雙向鏈表的節點,其中封裝了實際要執行的任務TimerTask;
  2. 定時任務TimerTask提交到 HashedWheelTimer 之後返回的,通過它可以查看定時任務的狀態、對定時任務進行取消、從雙向鏈表中移除等操作;

首先來看下HashedWheelTimeout核心字段,該核心字段的設計表明鏈表的結構是一個雙向鏈表:

//初始化狀態
private static final int ST_INIT = 0;
//被取消狀態
private static final int ST_CANCELLED = 1;
//過期狀態
private static final int ST_EXPIRED = 2;
//更新定時任務的狀態
private static final AtomicIntegerFieldUpdater<HashedWheelTimeout> STATE_UPDATER =
  AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimeout.class, "state");
//時間輪對象
private final HashedWheelTimer timer;
//實際執行的任務
private final TimerTask task;
//定時任務執行的時間
private final long deadline;

//默認狀態是初始化
@SuppressWarnings({"unused""FieldMayBeFinal""RedundantFieldInitialization"})
private volatile int state = ST_INIT;

//當前任務剩餘的時鐘週期數
long remainingRounds;

//當前定時任務在鏈表中的前驅節點和後繼節點 設計爲一個雙向鏈表
HashedWheelTimeout next;
HashedWheelTimeout prev;

//時間輪中的一個槽
//每個槽維護一個雙向鏈表,當時間輪指針轉到當前槽時,就會從槽所負責的雙向鏈表中取出任務進行處理
HashedWheelBucket bucket;

HashedWheelTimeout核心方法介紹:

  1. isCancelled、isExpired方法,主要用於檢查當前HashedWheelTimeout狀態;
  2. cancel方法將當前HashedWheelTimeout的狀態設置爲取消狀態,並將當前HashedWheelTimeout添加到 cancelledTimeouts隊列中等待銷燬;
  3. expire方法表示,當前到期的任務會調用該方法將會將當前HashedWheelTimeout設置爲過期狀態狀態,然後調用其中的TimerTask的run方法執行定時任務,不同類型的任務可以自己實現run方法;
  4. remove方法會將當前HashedWheelTimeout從時間輪中刪除;
@Override
  public boolean cancel() {
  //CAS變更狀態
  if (!compareAndSetState(ST_INIT, ST_CANCELLED)) {
    return false;
  }
  //任務被取消時,時間輪會將它暫存到時間輪所維護的canceledTimeouts隊列中.
  //當時間輪轉動到槽進行任務處理之前和時間輪退出運行時都會調用cancel,而
  //cancel會調用remove,從而清理該隊列中被取消的定時任務
  timer.cancelledTimeouts.add(this);
  return true;
}

void remove() {
  //獲取當前任務屬於哪個槽位
  HashedWheelBucket bucket = this.bucket;
  if (bucket != null) {
    //從雙向鏈表中移除節點
    bucket.remove(this);
  } else {
    //當前時間輪所維護的定時任務的數量
    timer.pendingTimeouts.decrementAndGet();
  }
}
public void expire() {
  //CAS修改定時任務狀態爲已過期
  if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {
    return;
  }

  try {
    //執行定時任務
    task.run(this);
  } catch (Throwable t) {
    if (logger.isWarnEnabled()) {
      logger.warn("An exception was thrown by " + TimerTask.class.getSimpleName() + '.', t);
    }
  }
}

HashedWheelBucket

image.png HashedWheelBucket是時間輪中的一個槽,它內部維護了雙向鏈表的首尾指針,雙向鏈表中的每一個節點就是一個HashedWheelTimeout對象,同時關聯了一個TimerTask定時任務。

private HashedWheelTimeout head;
private HashedWheelTimeout tail;

HashedWheelBucket維護雙向鏈表的頭尾節點,可以遍歷整個鏈表,因此具備了維護任務的能力,接下來我們來看一下HashedWheelBucket的核心方法。

  1. addTimeout方法新增HashedWheelTimeout到鏈表尾部;
  2. pollTimeout方法移除雙向鏈表中的頭結點,並將其返回;
  3. remove方法用於移除雙向鏈表的指定節點;
  4. clearTimeouts方法循環調用pollTimeout方法處理整個雙向鏈表,並返回所有未超時或者未被取消的任務,該方法會在時間輪停止的時候被調用;
  5. expireTimeouts方法遍歷雙向鏈表中的全部 HashedWheelTimeout節點,分別處理以下三種情況,定時任務已到期,則會通過remove方法取出,並調用其expire方法執行任務邏輯。定時任務已被取消,則通過remove方法取出直接丟棄。定時任務還未到期,則會將remainingRounds(剩餘時鐘週期)減一;
void addTimeout(HashedWheelTimeout timeout) {
  //空判斷一下
  assert timeout.bucket == null;
timeout.bucket = this;
//如果頭節點爲空 說明整個鏈表爲空 則設置頭尾爲當前節點
if (head == null) {
  head = tail = timeout;
else {
  //添加到未節點
  tail.next = timeout;
  timeout.prev = tail;
  tail = timeout;
}
}

/**
* Expire all {@link HashedWheelTimeout}s for the given {@code deadline}.
*/

void expireTimeouts(long deadline) {
  HashedWheelTimeout timeout = head;

  //時間輪指針轉到某個槽時從雙向鏈表頭節點開始遍歷
  while (timeout != null) {
    HashedWheelTimeout next = timeout.next;
    //當前任務到期
    if (timeout.remainingRounds <= 0) {
      //移除
      next = remove(timeout);
      if (timeout.deadline <= deadline) {
        //執行任務
        timeout.expire();
      } else {
        // The timeout was placed into a wrong slot. This should never happen.
        throw new IllegalStateException(String.format(
          "timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
      }
    } else if (timeout.isCancelled()) {
      //任務被取消 被移除
      next = remove(timeout);
    } else {
      //時鐘週期減一
      timeout.remainingRounds--;
    }
    //判斷下一個節點
    timeout = next;
  }
}

void clearTimeouts(Set<Timeout> set) {
  for (; ; ) {
    HashedWheelTimeout timeout = pollTimeout();
    if (timeout == null) {
      return;
    }
    if (timeout.isExpired() || timeout.isCancelled()) {
      continue;
    }
    set.add(timeout);
  }
}

HashedWheelTimer

HashedWheelTimer實現了Timer接口,它通過時間輪算法實現了一個定時器。可以通過newTimeout方法可以向時間輪中添加定時任務,該任務會先被暫存到timeouts隊列中,等時間輪轉動到某個槽時,會將該timeouts隊列中的任務轉移到某個槽所負責的雙向鏈表中。從雙向鏈表的頭部開始迭代,對每個定時任務HashedWheelTimeout進行計算,屬於當前時鐘週期則取出運行,不屬於則將其剩餘的時鐘週期數減一操作。此外還提供停止時間輪的stop方法,以及判斷時間輪是否終止的方法。

HashedWheelTimer核心字段
//時間輪處理定時任務邏輯
private final Worker worker = new Worker();
//時間輪內部處理定時任務的線程
private final Thread workerThread;

private static final int WORKER_STATE_INIT = 0;
private static final int WORKER_STATE_STARTED = 1;
private static final int WORKER_STATE_SHUTDOWN = 2;


//時間輪狀態 0 - init, 1 - started, 2 - shut down
@SuppressWarnings({"unused""FieldMayBeFinal"})
private volatile int workerState;
//時間輪每個槽所代表的時間
private final long tickDuration;
//時間輪的環形隊列,數組每個元素都是一個槽,一個槽負責維護一個雙向鏈表,用於存儲定時任務
private final HashedWheelBucket[] wheel;
//wheel.length - 1 
private final int mask;
//CountDownLatch保證線程已經啓動
private final CountDownLatch startTimeInitialized = new CountDownLatch(1);
//外部向時間輪提交的定時任務
private final Queue<HashedWheelTimeout> timeouts = new LinkedBlockingQueue<>();
//用於暫存被取消的定時任務
private final Queue<HashedWheelTimeout> cancelledTimeouts = new LinkedBlockingQueue<>();
//時間輪剩餘的待處理的定時任務數量
private final AtomicLong pendingTimeouts = new AtomicLong(0);
//最多允許多少個任務等待執行
private final long maxPendingTimeouts;
//當前時間輪的啓動時間
private volatile long startTime;
時間輪初始化

時間輪的初始化是在HashedWheelTimer的構造函數中完成的,主要就是創建HashedWheelBucket數組,以及創建workerThread工作線程,該線程就是負責處理時間輪中的定時任務的線程。

public HashedWheelTimer(
  ThreadFactory threadFactory,
  long tickDuration, TimeUnit unit, int ticksPerWheel,
  long maxPendingTimeouts)
 
{
  //參數校驗
  if (threadFactory == null) {
    throw new NullPointerException("threadFactory");
  }
  if (unit == null) {
    throw new NullPointerException("unit");
  }
  if (tickDuration <= 0) {
    throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);
  }
  if (ticksPerWheel <= 0) {
    throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
  }

  //圓環有多少時間間隔 將ticksPerWheel轉化爲一個大於等於該值的2^n的值
  wheel = createWheel(ticksPerWheel);
  //快速計算槽的位置
  mask = wheel.length - 1;

  //時間輪每個槽的時間間隔
  this.tickDuration = unit.toNanos(tickDuration);

  //邊界值檢查
  if (this.tickDuration >= Long.MAX_VALUE / wheel.length) {
    throw new IllegalArgumentException(String.format(
      "tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
      tickDuration, Long.MAX_VALUE / wheel.length));
  }
  //工作線程
  workerThread = threadFactory.newThread(worker);
  //最多允許多少個任務等待執行
  this.maxPendingTimeouts = maxPendingTimeouts;

  //限制timer實例個數,最大不超過64
  if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT &&
      WARNED_TOO_MANY_INSTANCES.compareAndSet(falsetrue)) {
    reportTooManyInstances();
  }
}
private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
  if (ticksPerWheel <= 0) {
  throw new IllegalArgumentException(
    "ticksPerWheel must be greater than 0: " + ticksPerWheel);
}
if (ticksPerWheel > 1073741824) {
  throw new IllegalArgumentException(
    "ticksPerWheel may not be greater than 2^30: " + ticksPerWheel);
}
//計算創建多少個槽
ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
//初始化時間輪數組
HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
for (int i = 0; i < wheel.length; i++) {
  wheel[i] = new HashedWheelBucket();
}
return wheel;
}
提交定時任務

提交定時任務發生在初始化之後,由newTimeout方法完成任務提交,方法內部將待處理的任務數量加1,然後啓動時間輪線程,這時worker的run方法就會被系統調度運行。然後將該定時任務封裝成HashedWheelTimeout加入到timeouts隊列中。start之後,時間輪就開始運行起來了,直到外界調用stop方法終止退出。

public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
  if (task == null) {
    throw new NullPointerException("task");
  }
  if (unit == null) {
    throw new NullPointerException("unit");
  }
  //任務數加1
  long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();

  //判斷是否超過最大任務數
  if (maxPendingTimeouts > 0 && pendingTimeoutsCount > maxPendingTimeouts) {
    pendingTimeouts.decrementAndGet();
    throw new RejectedExecutionException("Number of pending timeouts ("
                                         + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending "
                                         + "timeouts (" + maxPendingTimeouts + ")");
  }

  //啓動時間輪
  start();

  //計算定時任務的deadline
  long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;

  //參數校驗
  if (delay > 0 && deadline < 0) {
    deadline = Long.MAX_VALUE;
  }
  //創建一個HashedWheelTimeout對象
  HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
  //被暫存到timeouts隊列中
  timeouts.add(timeout);
  return timeout;
}
public void start() {
  //判斷時間輪狀態
  //1.如果是初始化, 則啓動worker線程, 啓動整個時間輪
  //2. 如果已經啓動則略過
  //3. 如果是已經停止,則報錯
  switch (WORKER_STATE_UPDATER.get(this)) {
    case WORKER_STATE_INIT:
      if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
        workerThread.start();
      }
      break;
    case WORKER_STATE_STARTED:
      break;
    case WORKER_STATE_SHUTDOWN:
      throw new IllegalStateException("cannot be started once stopped");
    default:
      throw new Error("Invalid WorkerState");
  }

  //等待worker線程初始化時間輪的啓動時間
  while (startTime == 0) {
    try {
      //countDownLatch來確保調度的線程已經被啓動
      startTimeInitialized.await();
    } catch (InterruptedException ignore) {
      // Ignore - it will be ready very soon.
    }
  }
}

Worker

Worker實現了Runnable接口,也就是時間輪內部的工作線程,工作線程來處理放入時間輪中的定時任務。對於該方法核心就是run方法,

        public void run() {
            //初始化startTime時間輪初始化以後的時候
            startTime = System.nanoTime();
            if (startTime == 0) {
                // We use 0 as an indicator for the uninitialized value here, so make sure it's not 0 when initialized.
                startTime = 1;
            }

            //喚醒阻塞線程
            startTimeInitialized.countDown();
            do {
                //判斷是否到了處理槽的時間 如果沒到sleep
                final long deadline = waitForNextTick();
                if (deadline > 0) {
                    //獲取對應槽
                    int idx = (int) (tick & mask);
                    //清理用戶主動取消的定時任務
                    processCancelledTasks();
                    //獲取當前指針對應的槽位
                    HashedWheelBucket bucket =
                            wheel[idx];
                    //將timeouts隊列中的定時任務轉移到時間輪中對應的槽中
                    transferTimeoutsToBuckets();
                    //處理該槽位的雙向鏈表中的定時任務
                    bucket.expireTimeouts(deadline);
                    tick++;
                }
                //運行狀態一直循環
            } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

            //執行到此處說明時間輪被停止了
            //清除所有槽中的任務, 並加入到未處理任務列表
            for (HashedWheelBucket bucket : wheel) {
                bucket.clearTimeouts(unprocessedTimeouts);
            }
            //將還沒有加入到槽中的待處理定時任務隊列中的任務取出, 如果是未取消的任務,
            //則加入到未處理任務隊列中
            for (; ; ) {
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    break;
                }
                if (!timeout.isCancelled()) {
                    unprocessedTimeouts.add(timeout);
                }
            }
            //最後再次清理cancelledTimeouts隊列中用戶主動取消的定時任務
            processCancelledTasks();
        }
整體流程回顧
  1. 時間輪指針轉動,時間輪週期開始;
  2. 清理用戶主動取消的定時任務,這些定時任務在用戶取消時,會記錄到 cancelledTimeouts 隊列中。在每次指針轉動的時候,時間輪都會清理該隊列;
  3. 將緩存在timeouts隊列中的定時任務轉移到時間輪中對應的槽中;
  4. 根據當前指針定位對應槽,處理該槽位的雙向鏈表中的定時任務;
  5. 檢測時間輪的狀態。如果時間輪處於運行狀態,則循環執行上述步驟,不斷執行定時任務。如果時間輪處於停止狀態,則執行下面的步驟獲取到未被執行的定時任務並加入 unprocessedTimeouts 隊列:遍歷時間輪中每個槽位,並調用 clearTimeouts方法;對timeouts隊列中未被加入槽中循環調用poll;
  6. 最後再次清理cancelledTimeouts隊列中用戶主動取消的定時任務;

Dubbo時間輪應用

在Dubbo中有關於時間輪的應用有兩個核心的抽象類,一個是AbstractRetryTask,另外一個是AbstractTimerTask,關於AbstractRetryTask重試機制我們在註冊中心的時候已經介紹完成,這裏重點看下AbstractTimerTask實現。 image.png 關於AbstractTimerTask有是三個實現類,一個用來關閉連接,一個是心跳檢查,一個是重連接,AbstractTimerTask會調用不同的實現,我們來看下HeartbeatTimerTask實現,

    protected void doTask(Channel channel) {
        try {
            //獲取最後一次讀寫時間
            Long lastRead = lastRead(channel);
            Long lastWrite = lastWrite(channel);
            if ((lastRead != null && now() - lastRead > heartbeat)
                    || (lastWrite != null && now() - lastWrite > heartbeat)) {
                //最後一次讀寫時間超過心跳時間,就會發送心跳請求
                Request req = new Request();
                req.setVersion(Version.getProtocolVersion());
                req.setTwoWay(true);
                req.setEvent(HEARTBEAT_EVENT);
                //發送心跳信息
                channel.send(req);
                if (logger.isDebugEnabled()) {
                    logger.debug("Send heartbeat to remote channel " + channel.getRemoteAddress()
                            + ", cause: The channel has no data-transmission exceeds a heartbeat period: "
                            + heartbeat + "ms");
                }
            }
        } catch (Throwable t) {
            logger.warn("Exception when heartbeat to remote channel " + channel.getRemoteAddress(), t);
        }
    }

結束

歡迎大家點點關注,點點贊!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章