票池暫訂使用disruptor來做訊息佇列,把最近對disruptor的調研結果整理一下。大部分文字都是把disruptor和其它網站上看到的資料翻譯一下。
原文:http://www.oraclejavamagazine-digital.com/javamagazine/20120304/?pg=56&pm=1&u1=friend#pg56
Disruptor是什嗎?
Disruptor是一個線程間通訊的架構,即在多線程間共用資料。它是由LMAX公司開發的可信訊息傳遞架構的一部分,以便用非常快速的方法來在多組件之間傳遞資料。它的一個核心思想是理解並適應硬體工作方式來達到最優的效果。
在很多(並行)架構裡,普遍使用隊列來共用資料(例如傳遞訊息)。圖1就是使用隊列來傳遞訊息的一個(裡面藍色的小圈圈表示一個線程)。這種架構允許生產線程(圖1裡的stage1)在消費線程(圖1裡的stage2)處理不過來的情況下,還可以繼續後面的工作,隊列在其中用來做為訊息的緩衝區。
圖1
在最簡單的情況下,disruptor可以用來替代圖1架構裡的隊列,也就是線程間通過disruptor來傳遞資料。在disruptor裡儲存訊息的資料結構是環狀緩衝區(RingBuffer - 後面都用RingBuffer這個術語)。生產線程stage1將訊息放到RingBuffer裡,然後消費線程stage2從RingBuffer裡讀取訊息,2。
圖2
從圖2裡可以看到,RingBuffer裡的每一個元素都有一個序號(sequence number)來索引,RingBuffer維護當前最新放置的元素的序號,這個序號一直遞增,(通過求餘來得到元素在RingBuffer下面的數組下標)。
Disruptor的關鍵特性是無鎖編程,這個是通過單一寫線程的方式實現的 - 即一塊資料永遠只有一個線程寫入。通過遵循這個編程原則來避免使用昂貴的同步鎖或CAS操作,這就是為什麼Disruptor這麼快的原因。
因為RingBuffer規避了鎖,而且每個EventProcessor維護自己的序號。
向Disruptor發布訊息
往RingBuffer裡寫入訊息使用兩步提交的方式。首先,生產線程Stage1需要確定RingBuffer裡下一個空閑槽,3。
圖3
RingBuffer維護了最後一次寫入的序號(圖3裡的18號),因此就可以推知下一個閒置槽號。RingBuffer通過檢查所有從RingBuffer讀取訊息的EventProcessor的序號,以判別下一個槽號是否空閑。
圖4示範了索取下一個空閑槽序號的過程。
圖4
當生產線程拿到了下一個序利號之後,它從RingBuffer裡拿到槽裡儲存的對象並執行任何操作。這個過程中,因為RingBuffer的最新序號依然是18,因此其它線程無法讀取19號槽裡面的事件 - 生產線程還在處理它。
圖5
圖5示範了RingBuffer在提交變更後的情況。當生產線程處理完第19號槽的資料後,它告訴RingBuffer將其公布出來。這個時候,RingBuffer才會更新它維護的序號,任何等待讀取第19號槽裡的資料的線程才能讀取它。
從RingBuffer裡讀取資訊
Disruptor架構裡提供了一個叫做BatchEventProcessor來從RingBuffer裡讀取資料。當生產線程向RingBuffer要求下一個可寫入的空閑槽的序號時,同時一個EventProcessor(類似消費者,但其並消費RingBuffer裡的元素 - 即不從RingBuffer裡移除任何元素)也會維護其最後所處理的資料的序號,並要求下一個可處理的資料的序號。
圖6示範了EventProcessor等待處理下一個可讀取資料序利號的過程。
圖6
EventProcessor不是直接從RingBuffer裡擷取下一個可讀取資料的序號,而是通過一個SequenceBarrier對象來做的,稍後我們談這個細節。
圖6裡,EventProcessor(即消費者線程Stage2)最後看到的是第16號槽的資料,它希望處理下一個(第17號)槽的資料,因此它執行SequenceBarrier的waitFor(17)函數調用。線程Stage2可以一直等待下一個可讀序號,因為如果尚沒有資料生產出來的話,它什麼也不需要做。但跟圖6所示的一樣,RingBuffer裡最新可用資料已經到18號槽了,因此waitFor返回18,即告訴EventProcessor可以一直讀到第18號的所有資料。7。
圖7
這種模式提供了很好的批處理行為,可以使用這種批處理代碼來實現EventHandler,在Disruptor裡效能測試FizzBuzzEventHandler就是一個很好的例子。
處理系統組件之間的依賴關係
Disruptor處理系統內部多組件的依賴關係,而不引入任何線程競爭的做法很有意思。Disruptor遵循的是單線程寫入,多線程讀取的做法。Disruptor的原始設計是支援幾步具有特定順序的串列流水線操作 - 這種操作在企業級的系統裡很常見。圖8演了一個標準的三步流水線操作:
圖8
首先,所有事件都會寫入硬碟(日誌“Journaling”操作),以便容災恢複。第二所有事件會備份(Replication操作)到第二台伺服器上,只有這些步驟都完成之後系統才能處理實際的業務操作(Business Logic)。
串列做這三步操作是一個合理的做法,但不是最有效率的。日誌和備份操作可以並行,因為它們相互獨立。但業務操作不行,因為它依賴前兩者,圖9示範了這個依賴關係。
圖9
如果使用Disruptor,前兩步(日誌和備份)可以直接讀RingBuffer。跟圖7示意的,它們都使用一個屏障(Sequence Barrier)來得到RingBuffer下一個可讀取的序號。它們各自維護自己的序號,這樣方便它們自己知道已經讀到哪了,並使用BatchEventProcessor來處理事件(日誌和備份)。
業務線程也會從同一個RingBuffer裡讀取事件,不過只能處理前兩個線程處理完的事件。這個限制通過第二個SequenceBarrier來實現,它被配置來讀取日誌線程和備份線程的序號,返回它們的最小值,以告訴業務安全執行緒讀取的範圍。
只有每一個EventProcessor都使用序號屏障(Sequence Barrier)來確定可以安全處理的事件範圍,才能從RingBuffer裡讀取資料。10。
圖10
雖然有很多線程讀取不同的序利號,但由雩都是簡單遞增自己內部的序利號,所以線程間沒有競爭。
多個生產線程
Disruptor也支援,但是本文沒有說如何支援,放在後面寫。
結論
雖然disruptor的原理已經比較熟悉了,但是其API還不是很瞭解,我寫了一個實驗性的代碼,來完善我的理解 - 不過隨著理解的深入,代碼會不斷更新:
https://github.com/shiyimin/12306ngpm/blob/8be9178d318618f905aaed45fa6025df09371c31/trunk/tpms/src/test/java/org/ng12306/tpms/DisruptorConceptProofTest.java