一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

位元組武裝發表於2020-03-31

本文是【位元組視覺化系列】Kafka專欄文章。

通過本文你將瞭解到時間輪演算法思想,層級時間輪,時間輪的升級和降級。


時間輪,是一種實現延遲功能(定時器)的巧妙演算法,在Netty,Zookeeper,Kafka等各種框架中,甚至Linux核心中都有用到。


本文將參考Kafka的時間輪作為例子講解。


0 設計源於生活

開始之前給大家看塊寶珀中華年曆表。

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

圖片來自寶珀官網


這款手錶的錶盤融合了中華曆法中各種博大精深的計時元素。

上方位置的小錶盤顯示時辰數字及字元,24小時一週期;年份視窗顯示當年所屬生肖,12年一週期;

左邊位置顯示農曆月,12個月一週期; 農曆日, 30天一週期;

右邊位置顯示五行元素和十天干,10年一週期;

下方的錶盤顯示月相盈虧。

至於價格.....這個話題略過。


而時間輪,其設計正是來源於生活中的時鐘。


1 時間輪

如圖就是一個簡單的時間輪:


一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

圖中大圓的圓心位置表示的是當前的時間,隨著時間推移, 圓心處的時間也會不斷跳動。


下面我們對著這個圖,來說說Kafka的時間輪TimingWheel。


Kafka時間輪的底層就是一個環形陣列,而陣列中每個元素都存放一個雙向連結串列TimerTaskList,連結串列中封裝了很多延時任務。


Kafka中一個時間輪TimingWheel是由20個時間格組成,wheelSize = 20;每格的時間跨度是1ms,tickMs = 1ms。參照Kafka,上圖中也用了20個灰邊小圓表示時間格,為了動畫演示可以看得清楚,我們這裡每個小圓的時間跨度是1s。


所以現在整個時間輪的時間跨度就是 tickMs * wheelSize ,也就是 20s。從0s到19s,我們都分別有一個灰邊小圓來承載。


Kafka的時間輪還有一個錶盤指標 currentTime,表示時間輪當前所處的時間。也就是圖中用黑色粗線表示的圓,隨著時間推移, 這個指標也會不斷前進;

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!



新增定時任務

有了時間輪,現在可以往裡面新增定時任務了。我們用一個粉紅色的小圓來表示一個定時任務。

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!


這裡先講一下設定,每一個定時任務都有延時時間delayTime,和過期時間ExpiredTime
比如當前時間是10s,我們新增了個延時時間為2s的任務,那麼這個任務的過期時間就是12s,也就是當前時間10s再走兩秒,變成了12s的時候,就到了觸發這個定時任務的時間。

而時間輪上代表時間格的灰邊小圓上顯示的數字,可以理解為任務的過期時間。

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!


講清楚這些設定後,我們就開始新增定時任務吧。


初始的時候, 時間輪的指標定格在0。此時新增一個超時時間為2s的任務, 那麼這個任務將會插入到第二個時間格中。

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!



當時間輪的指標到達第二個時間格時, 會處理該時間格上對應的任務。在動畫上就是讓紅色的小圓消失!

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!


如果這個時候又插入一個延時時間為8s的任務進來, 這個任務的過期時間就是在當前時間2s的基礎上加8s, 也就是10s, 那麼這個任務將會插入到過期時間為10s的時間格中。

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!




2 "動態"時間輪

到目前為止,一切都很好理解。

那麼如果在當前時間是2s的時候, 插入一個延時時間為19s的任務時, 這個任務的過期時間就是在當前時間2s的基礎上加19s, 也就是21s。

請看下圖,當前的時間輪是沒有過期時間為21s的時間格。這個任務將會插入到過期時間為1s的時間格中,這是怎麼回事呢?

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

複用時間格


為了解答上面的問題,我們先來點魔法, 讓時間輪上的時間都動起來!

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

其實呢,當指標定格在2s的位置時, 時間格0s, 1s和2s就已經是過期的時間格。


也就是說指標可以用來劃分過期的時間格[0,2]和未來的時間格 [3,19]。而過期的時間格可以繼續複用。比如過期的時間格0s就變成了20s, 存放過期時間為20s的任務。


理解了時間格的複用之後,再看回剛剛的例子,當前時間是2s時,新增延時時間為19s的任務,那麼這個任務就會插入到過期時間為21s的時間格中。

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

3 時間輪升級

下面,新的問題來了,請坐好扶穩。

如果在當前時間是2s的時候, 插入一個延時時間為22s的任務, 這個任務的過期時間就是在2s的基礎上加22s,也就是24s。

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!


顯然當前時間輪是無法找到過期時間格為24秒的時間格,因為當前過期時間最大的時間格才到21s。而且我們也沒辦法像前面那樣再複用時間格,因為除了過期時間為2s的時間格,其他的時間格都還沒過期呢。當前時間輪無法承載這個定時任務, 那麼應該怎麼辦呢?

當然我們可以選擇擴充套件時間輪上的時間格, 但是這樣一來,時間輪就失去了意義。

是時候要升級時間輪了!

我們先來理解下多層時間輪之間的聯絡。


4 層級時間輪

如圖是一個兩層的時間輪:

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

第二層時間輪也是由20個時間格組成, 每個時間格的跨度是20s。

圖中展示了每個時間格對應的過期時間範圍, 我們可以清晰地看到, 第二層時間輪的第0個時間格的過期時間範圍是 [0,19]。也就是說, 第二層時間輪的一個時間格就可以表示第一層時間輪的所有(20個)時間格;

為了進一步理清第一層時間輪和第二層時間輪的關係, 我們拉著時間的小手, 一起觀看下面的動圖:

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

可以看到,第二層時間輪同樣也有自己的指標, 每當第一層時間輪走完一個週期,第二層時間輪的指標就會推進一格。


新增定時任務

回到一開始的問題,在當前時間是2s的時候, 插入一個延時時間為22s的任務,該任務過期時間為24s。

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

當第一層時間輪容納不下時,進入第二層時間輪,並插入到過期時間為[20,39]的時間格中。


我們再來個例子,如果在當前時間是2s的時候, 插入一個延時時間為350s的任務, 這個任務的過期時間就是在2s的基礎上加350s,也就是352s。

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

從圖中可以看到,該任務插入到第二層時間輪過期時間為[340,359]s的時間格中,也就是第17格的位置。



5 "動態"層級時間輪

通常來說, 第二層時間輪的第0個時間格是用來表示第一層時間輪的, 這一格是存放不了任務的, 因為超時時間0-20s的任務, 第一層時間輪就可以處理了。


但是! 事情往往沒這麼簡單, 我們時間輪上的時間格都是可以複用的! 那麼這在第二層時間輪上又是怎麼體現的呢?


下面是魔法時間, 我們讓時間輪上的過期時間都動起來!

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!


從圖中可以看到,當第一層時間輪的指標定格在1s時,超時時間0s的時間格就過期了。而這個時候,第二層時間輪第0個時間格的時間範圍就從[0,19]分為了過期的[0],和未過期的[1,19]。而過期的[0]就會被新的過期時間[400]複用。


第二層時間輪第0個時間格的過期時間範圍演變如下:

[0-19]

[400][1,19]

[400,401][2,19]

......

[400,419]


所以,如果在當前時間是2s的時候, 插入一個延時時間為399s的任務, 這個任務的過期時間就是在2s的基礎上加399s,也就是401s。如圖,這個任務還是會插到第二層時間輪第0個時間格中去。

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!


6 時間輪降級

還是用回這個大家都已經耳熟能詳的例子,在當前時間是2s的時候, 插入一個延時時間為22s的任務,該任務過期時間為24s。最後進入第二層時間輪,並插入到過期時間為[20,39]的時間格中。

當二層時間輪上的定時任務到期後,時間輪是怎麼做的呢?


一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!

從圖中可以看到,隨著當前時間從2s繼續往前推進,一直到20s的時候,總共經過了18s。此時第二層時間輪中,超時時間為[20-39s]的時間格上的任務到期。

原本超時時間為24s的任務會被取出來,重新加入時間輪。此時該定時任務的延時時間從原本的22s,到現在還剩下4s(22s-18s)。最後停留在第一層時間輪超時時間為24s的時間格,也就是第4個時間格。

隨著當前時間繼續推進,再經過4s後,該定時任務到期被執行。


從這裡可以看出時間輪的巧妙之處,兩層時間輪只用了40個陣列元素,卻可以承載[0-399s]的定時任務。而三層時間輪用60個陣列元素,就可以承載[0-7999s]的定時任務!

一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!


7 時間輪的推進

從動畫中可以注意到, 隨著時間推進, 時間輪的指標迴圈往復地定格在每一個時間格上, 每一次都要判斷當前定格的時間格里是不是有任務存在;

其中有很多時間格都是沒有任務的, 指標定格在這種空的時間格中, 就是一次"空推進";

比如說, 插入一個延時時間400s的任務, 指標就要執行399次"空推進", 這是一種浪費!

那麼Kafka是怎麼解決這個問題的呢?這就要從延遲佇列DelayQueue開始講起了!
時間輪搭配延遲佇列DelayQueue,會發生什麼化學反應呢?請關注公眾號【位元組武裝】後續更新。


往期回顧



一張圖理解Kafka時間輪(TimingWheel),看不懂算我輸!


相關文章