1. 程式人生 > >Kafka解惑之時間輪(TimingWheel)

Kafka解惑之時間輪(TimingWheel)

Kafka中存在大量的延遲操作,比如延遲生產、延遲拉取以及延遲刪除等。Kafka並沒有使用JDK自帶的Timer或者DelayQueue來實現延遲的功能,而是基於時間輪自定義了一個用於實現延遲功能的定時器(SystemTimer)。JDK的Timer和DelayQueue插入和刪除操作的平均時間複雜度為O(nlog(n)),並不能滿足Kafka的高效能要求,而基於時間輪可以將插入和刪除操作的時間複雜度都降為O(1)。時間輪的應用並非Kafka獨有,其應用場景還有很多,在Netty、Akka、Quartz、Zookeeper等元件中都存在時間輪的蹤影。

參考下圖,Kafka中的時間輪(TimingWheel)是一個儲存定時任務的環形佇列,底層採用陣列實現,陣列中的每個元素可以存放一個定時任務列表(TimerTaskList)。TimerTaskList是一個環形的雙向連結串列,連結串列中的每一項表示的都是定時任務項(TimerTaskEntry),其中封裝了真正的定時任務TimerTask。
圖1


時間輪由多個時間格組成,每個時間格代表當前時間輪的基本時間跨度(tickMs)。時間輪的時間格個數是固定的,可用wheelSize來表示,那麼整個時間輪的總體時間跨度(interval)可以通過公式 tickMs × wheelSize計算得出。時間輪還有一個錶盤指標(currentTime),用來表示時間輪當前所處的時間,currentTime是tickMs的整數倍。currentTime可以將整個時間輪劃分為到期部分和未到期部分,currentTime當前指向的時間格也屬於到期部分,表示剛好到期,需要處理此時間格所對應的TimerTaskList的所有任務。

若時間輪的tickMs=1ms,wheelSize=20,那麼可以計算得出interval為20ms。初始情況下表盤指標currentTime指向時間格0,此時有一個定時為2ms的任務插入進來會存放到時間格為2的TimerTaskList中。隨著時間的不斷推移,指標currentTime不斷向前推進,過了2ms之後,當到達時間格2時,就需要將時間格2所對應的TimeTaskList中的任務做相應的到期操作。此時若又有一個定時為8ms的任務插入進來,則會存放到時間格10中,currentTime再過8ms後會指向時間格10。如果同時有一個定時為19ms的任務插入進來怎麼辦?新來的TimerTaskEntry會複用原來的TimerTaskList,所以它會插入到原本已經到期的時間格1中。總之,整個時間輪的總體跨度是不變的,隨著指標currentTime的不斷推進,當前時間輪所能處理的時間段也在不斷後移,總體時間範圍在currentTime和currentTime+interval之間。

如果此時有個定時為350ms的任務該如何處理?直接擴充wheelSize的大小麼?Kafka中不乏幾萬甚至幾十萬毫秒的定時任務,這個wheelSize的擴充沒有底線,就算將所有的定時任務的到期時間都設定一個上限,比如100萬毫秒,那麼這個wheelSize為100萬毫秒的時間輪不僅佔用很大的記憶體空間,而且效率也會拉低。Kafka為此引入了層級時間輪的概念,當任務的到期時間超過了當前時間輪所表示的時間範圍時,就會嘗試新增到上層時間輪中。
圖2
參考上圖,複用之前的案例,第一層的時間輪tickMs=1ms, wheelSize=20, interval=20ms。第二層的時間輪的tickMs為第一層時間輪的interval,即為20ms。每一層時間輪的wheelSize是固定的,都是20,那麼第二層的時間輪的總體時間跨度interval為400ms。以此類推,這個400ms也是第三層的tickMs的大小,第三層的時間輪的總體時間跨度為8000ms。

對於之前所說的350ms的定時任務,顯然第一層時間輪不能滿足條件,所以就升級到第二層時間輪中,最終被插入到第二層時間輪中時間格17所對應的TimerTaskList中。如果此時又有一個定時為450ms的任務,那麼顯然第二層時間輪也無法滿足條件,所以又升級到第三層時間輪中,最終被插入到第三層時間輪中時間格1的TimerTaskList中。注意到在到期時間在[400ms,800ms)區間的多個任務(比如446ms、455ms以及473ms的定時任務)都會被放入到第三層時間輪的時間格1中,時間格1對應的TimerTaskList的超時時間為400ms。隨著時間的流逝,當次TimerTaskList到期之時,原本定時為450ms的任務還剩下50ms的時間,還不能執行這個任務的到期操作。這裡就有一個時間輪降級的操作,會將這個剩餘時間為50ms的定時任務重新提交到層級時間輪中,此時第一層時間輪的總體時間跨度不夠,而第二層足夠,所以該任務被放到第二層時間輪到期時間為[40ms,60ms)的時間格中。再經歷了40ms之後,此時這個任務又被“察覺”到,不過還剩餘10ms,還是不能立即執行到期操作。所以還要再有一次時間輪的降級,此任務被新增到第一層時間輪到期時間為[10ms,11ms)的時間格中,之後再經歷10ms後,此任務真正到期,最終執行相應的到期操作。

設計源於生活。我們常見的鐘表就是一種具有三層結構的時間輪,第一層時間輪tickMs=1ms, wheelSize=60,interval=1min,此為秒鐘;第二層tickMs=1min,wheelSize=60,interval=1hour,此為分鐘;第三層tickMs=1hour,wheelSize為12,interval為12hours,此為時鐘。

在Kafka中第一層時間輪的引數同上面的案例一樣:tickMs=1ms, wheelSize=20, interval=20ms,各個層級的wheelSize也固定為20,所以各個層級的tickMs和interval也可以相應的推算出來。Kafka在具體實現時間輪TimingWheel時還有一些小細節:

  1. TimingWheel在建立的時候以當前系統時間為第一層時間輪的起始時間(startMs),這裡的當前系統時間並沒有簡單的呼叫System.currentTimeMillis(),而是呼叫了Time.SYSTEM.hiResClockMs,這是因為currentTimeMillis()方法的時間精度依賴於作業系統的具體實現,有些作業系統下並不能達到毫秒級的精度,而Time.SYSTEM.hiResClockMs實質上是採用了System.nanoTime()/1_000_000來將精度調整到毫秒級。也有其他的某些騷操作可以實現毫秒級的精度,但是筆者並不推薦,System.nanoTime()/1_000_000是最有效的方法。(如對此有想法,可在留言區探討。)
  2. TimingWheel中的每個雙向環形連結串列TimerTaskList都會有一個哨兵節點(sentinel),引入哨兵節點可以簡化邊界條件。哨兵節點也稱為啞元節點(dummy node),它是一個附加的連結串列節點,該節點作為第一個節點,它的值域中並不儲存任何東西,只是為了操作的方便而引入的。如果一個連結串列有哨兵節點的話,那麼線性表的第一個元素應該是連結串列的第二個節點。
  3. 除了第一層時間輪,其餘高層時間輪的起始時間(startMs)都設定為建立此層時間輪時前面第一輪的currentTime。每一層的currentTime都必須是tickMs的整數倍,如果不滿足則會將currentTime修剪為tickMs的整數倍,以此與時間輪中的時間格的到期時間範圍對應起來。修剪方法為:currentTime = startMs - (startMs % tickMs)。currentTime會隨著時間推移而推薦,但是不會改變為tickMs的整數倍的既定事實。若某一時刻的時間為timeMs,那麼此時時間輪的currentTime = timeMs - (timeMs % tickMs),時間每推進一次,每個層級的時間輪的currentTime都會依據此公式推進。
  4. Kafka中的定時器只需持有TimingWheel的第一層時間輪的引用,並不會直接持有其他高層的時間輪,但是每一層時間輪都會有一個引用(overflowWheel)指向更高一層的應用,以此層級呼叫而可以實現定時器間接持有各個層級時間輪的引用。

關於時間輪的細節就描述到這裡,各個元件中時間輪的實現大同小異。讀者讀到這裡是否會好奇文中一直描述的一個情景——“隨著時間的流逝”或者“隨著時間的推移”,那麼在Kafka中到底是怎麼推進時間的呢?類似採用JDK中的scheduleAtFixedRate來每秒推進時間輪?顯然這樣並不合理,TimingWheel也失去了大部分意義。

Kafka中的定時器藉助了JDK中的DelayQueue來協助推進時間輪。具體做法是對於每個使用到的TimerTaskList都會加入到DelayQueue中,“每個使用到的TimerTaskList”特指有非哨兵節點的定時任務項TimerTaskEntry的TimerTaskList。DelayQueue會根據TimerTaskList對應的超時時間expiration來排序,最短expiration的TimerTaskList會被排在DelayQueue的隊頭。Kafka中會有一個執行緒來獲取DelayQueue中的到期的任務列表,有意思的是這個執行緒所對應的名稱叫做“ExpiredOperationReaper”,可以直譯為“過期操作收割機”,和“SkimpyOffsetMap”的取名有的一拼。當“收割機”執行緒獲取到DelayQueue中的超時的任務列表TimerTaskList之後,既可以根據TimerTaskList的expiration來推進時間輪的時間,也可以就獲取到的TimerTaskList執行相應的操作,對立面的TimerTaskEntry該執行過期操作的就執行過期操作,該降級時間輪的就降級時間輪。

讀者讀到這裡或許又非常的困惑,文章開頭明確指明的DelayQueue不適合Kafka這種高效能要求的定時任務,為何這裡還要引入DelayQueue呢?注意對於定時任務項TimerTaskEntry插入和刪除操作而言,TimingWheel時間複雜度為O(1),效能高出DelayQueue很多,如果直接將TimerTaskEntry插入DelayQueue中,那麼效能顯然難以支撐。就算我們根據一定的規則將若干TimerTaskEntry劃分到TimerTaskList這個組中,然後再將TimerTaskList插入到DelayQueue中,試想下如果這個TimerTaskList中又要多新增一個TimerTaskEntry該如何處理?對於DelayQueue而言,這類操作顯然變得力不從心。

分析到這裡可以發現,Kafka中的TimingWheel專門用來執行插入和刪除TimerTaskEntry的操作,而DelayQueue專門負責時間推進的任務。再試想一下,DelayQueue中的第一個超時任務列表的expiration為200ms,第二個超時任務為840ms,這裡獲取DelayQueue的隊頭只需要O(1)的時間複雜度。如果採用每秒定時推進,那麼獲取到第一個超時的任務列表時執行的200次推進中有199次屬於“空推進”,而獲取到第二個超時任務時有需要執行639次“空推進”,這樣會無故空耗機器的效能資源,這裡採用DelayQueue來輔助以少量空間換時間,從而做到了“精準推進”。Kafka中的定時器真可謂是“知人善用”,用TimingWheel做最擅長的任務新增和刪除操作,而用DelayQueue做最擅長的時間推進工作,相輔相成。

歡迎支援《RabbitMQ實戰指南》以及關注微信公眾號:朱小廝的部落格。

相關推薦

Kafka解惑時間TimingWheel

Kafka中存在大量的延遲操作,比如延遲生產、延遲拉取以及延遲刪除等。Kafka並沒有使用JDK自帶的Timer或者DelayQueue來實現延遲的功能,而是基於時間輪自定義了一個用於實現延遲功能的定時器(SystemTimer)。JDK的Timer和Delay

kafka系列初步認識

Kafka是什麼 釋出/訂閱訊息中介軟體 也被稱為分散式流平臺 Kafka的誕生 最初是為了解決LinkedIn資料通道問題,最後捐獻給了Apache,是Apache的頂級專案。 Kafka適合的場景 使用者行為跟蹤,可

時間演算法TimingWheel是如何實現的?

前言 我在2. SOFAJRaft原始碼分析—JRaft的定時任務排程器是怎麼做的?這篇文章裡已經講解過時間輪演算法在JRaft中是怎麼應用的,但是我感覺我並沒有講解清楚這個東西,導致看了這篇文章依然和沒看是一樣的,所以我打算重新說透時間輪演算法。 時間輪的應用並非 JRaft 獨有,其應用場景還有很多,在

FPGA學習數碼管封裝顯示時間

rtu 環境 tro gb2312 配置 fpga 模塊 rtl 顯示時間 一、實驗目的:學習數碼管封裝以及顯示時間。二、實驗環境:FPGA開發板AX301,Quartus ii三、實驗介紹:將數碼管顯示模塊封裝起來,同時通過不斷讀取RTC時鐘的時分秒值,將之顯示在數碼管。

asp.net core mcroservices 架構 分散式日誌:整合kafka

    一 kafka介紹                      

API開發介面安全----sign有效時間

之前生成的sign和校驗sign我們已經完全掌握了、但是僅僅憑藉這樣的sign是無法滿足我們的需求的,如果一個黑客通過抓包抓到你的資料 他可以去修改你的header為這樣的 body為那樣的 也是可以通過sign校驗的 那麼我們怎麼解決呢 下面將詳細的 為大家解說 首先想到這樣的一個問題 我麼首先應當考慮到

程序猿的量化交易17--CointraderTemporal實體5

eas 建表 times create bject cloud temp 存儲 時間 轉載須要註明:http://blog.csdn.net/minimicall,http://cloudtrader.top/ 這一小節說明一個時間實體Temporal實體,它的代碼非常

排序算法高速排序Java

大於 一個數 大小 main div 移動 swap 交換 system //高速排序 public class Quick_Sort { // 排序的主要算法 private int Partition(int[] data, int start, int en

構建法學習4

控制 重要 protect 運算 包裝 二義性 lin c++ 基類 本周學習的內容是兩人合作 計算機只關心編譯生成的機器碼,你的程序采用哪種縮進風格,變量名有無統一的規範等,與機器碼的執行無關。但是,做一個有商業價值的項目,或者在團隊裏工作,代碼規範相當重要。“代碼規

DOC窗口cd命令windows

背景 soft 文件 ack images doc 完整路徑 com 技術分享   創作背景:在java學習過程中遇到“使用cd命令將路徑轉入源文件‘Hello.java‘所在的路徑”,對cd些許功能作以總結。   提一概念:cd的全稱是Change Directory,改

爬蟲庫BeautifulSoup學習

不必要 baidu html left 官方 blank 正則 文本處理 比較 BeautifulSoup官方介紹文檔:https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html 四大對象種

爬蟲庫BeautifulSoup學習

子節點 rom lac repr 文檔 strong 爬蟲 time contents 遍歷文檔樹:   1、查找子節點   .contents     tag的.content屬性可以將tag的子節點以列表的方式輸出。   print soup.body.cont

學習MVC租房網站- 前臺註冊和登錄

設置 rup 密碼錯誤 發送短信 mvc 短信驗證 ont 上傳 錯誤 在上一篇<學習MVC之租房網站(七)-房源管理和配圖上傳>完成了在後臺新增、編輯房源信息以及上傳房源配圖的功能。到此後臺開發便告一段落了,開始實現前臺的功能,也是從用戶的登錄、註冊開始。 前

構建法學習5

成員 nbsp 9.png 多少 影響 .cn ges png img 本周學習的是構建之法第五章 團隊和流程 團隊有共同的特點:1. 團隊有一致的集體目標,團隊要一起完成這目標。一個團隊的成員不一定要同時工作,例如接力賽跑。(王屋村搬磚的“非團隊”成員則不然,每個人想搬多

爬蟲庫BeautifulSoup學習

所有 字符串 判斷 href gin int 過濾器 amp link 探索文檔樹: find_all(name,attrs,recursive,text,**kwargs) 方法搜索當前tag的所有tag子節點,並判斷是否符合過濾器的條件 1、name參數,可

學習MVC租房網站-房源顯示和搜索

下使用 server epic 位置 edit 電商 給定 針對 富文本 在上一篇<學習MVC之租房網站(八)- 前臺註冊和登錄>完成了前臺用戶的註冊、登錄、重置密碼等功能,然後要實現與業務相關的功能,包括房源的顯示、檢索等。 一 房源顯示 房源顯示內容較多

《構建法》

提取 軟件服務 use 內部 模型 以及 標註 發展 靈活性 本周閱讀了第10~12章,進一步學習了在開發一個軟件時需要考慮並做到的幾個方面。 第10章 典型用戶和場景 作為軟件,目的是為了實現用戶的需求,所以,開發軟件最大的目的不是“軟件工程”,而是“用戶”。

構建法學習6

客戶 需求 現在 保持 變化 經理 論證 規格 沒有 本周學習的是第六章——敏捷流程 在軟件工程的語境裏,“敏捷流程”是一系列價值觀和方法論的集合。從2001年開始,一些軟件界的專家開始倡導“敏捷”的價值觀和流程,他們肯定了流行做法的價值,但是強調敏捷的做法更能帶來價值。

【Shiro】Apache Shiro架構身份認證Authentication

trac pretty asm 安全保障 軟件測試 釋放 model tac 讀取配置文件 Shiro系列文章: 【Shiro】Apache Shiro架構之權限認證(Authorization) 【Shiro】Apache Shiro架構之集成web

菜鳥教程工具使用——JRebel與Windows服務的Tomcat集成

-m end 個人 再見 proc key pre 安裝 target 之前寫過一篇Tomcat借助JRebel支持熱部署的文章——《借助JRebel使Tomcat支持熱部署 》。介紹的是在開發、測試環境中的配置。可是正式的部署環境。我們不會通過命令行來啟動Tomcat,