高性能隊列Disruptor系列2--淺析Disruptor


1. Disruptor簡單介紹

Disruptor是一個由LMAX開源的Java並發框架。LMAX是一種新型零售金融交易平台,這個系統是建立在 JVM 平台上,核心是一個業務邏輯處理器,它能夠在一個線程里每秒處理 6 百萬訂單。業務邏輯處理器完全是運行在內存中(in-memory),使用事件源驅動方式(event sourcing),具有低延遲,高吞吐的特性。

disruptor有多快?官方給出了和ArrayBlockingQueue的比較圖表:

Disruptor可以用來解決並發編程中的一個普遍的問題: 消息隊列的處理(producer和consumer)。

2. 為什么Disruptor如此之快

Disruptor 相對於傳統方式的優點:

  • 無鎖,沒有競爭
  • 所有訪問者都記錄自己的序號的實現方式,允許多個生產者與多個消費者共享相同的數據結構
  • 緩存行填充,解決偽共享,提高cache命中率
  • 環形數組RingBuffer,避免GC開銷

3. Disruptor結構分析

在了解disruptor如何工作之前,我們先看一下disruptor一些重要組件的介紹(翻譯自官方文檔,略有修改):

  • Ring Buffer:Ring Buffer通常被認為是Disruptor的主要方面,但是從3.0開始Ring Buffer只負責數據(Events)的存儲和更新。對於一些高級用例,完全可以由用戶自己替換。
  • Sequence:Disruptor利用Sequences來標志一個特定的組件,每一個消費者(EventProcessor)都維護一個Sequence。Disruptor中大多數的並發代碼都是依賴於這些Sequence的移動,生產者對RingBuffer的互斥訪問,生產者與消費者之間的協調以及消費者之間的協調,都是通過Sequence實現。幾乎每一個重要的組件都包含Sequence。由於需要在線程間共享,所以Sequence是引用傳遞,並且是線程安全的;再次,Sequence支持CAS操作;最后,為了提高效率,Sequence通過padding來避免偽共享。
  • Sequencer:Sequencer是Disruptor的真正的核心,此接口有兩個實現類 SingleProducerSequencer、MultiProducerSequencer ,它們定義在生產者和消費者之間快速、正確地傳遞數據的並發算法。
  • Sequence Barriers:Sequence Barriers是由Sequencer創建的,包含Sequencer主發布的Sequence的引用和任何一個依賴消費者的Sequences。它包含了判斷是否有任何事件可供消費者處理的邏輯。
  • Wait Strategy:等待策略決定了消費者會等待event被生產者放入Disruptor。Disruptor提供了多個等待策略的實現。1. BusySpinWaitStrategy:自旋等待,類似Linux Kernel使用的自旋鎖。低延遲但同時對CPU資源的占用也多。2. BlockingWaitStrategy :使用鎖和條件變量。CPU資源的占用少,延遲大。3. SleepingWaitStrategy :在多次循環嘗試不成功后,選擇讓出CPU,等待下次調度,多次調度后仍不成功,嘗試前睡眠一個納秒級別的時間再嘗試。這種策略平衡了延遲和CPU資源占用,但延遲不均勻。5. YieldingWaitStrategy :在多次循環嘗試不成功后,選擇讓出CPU,等待下次調度。平衡了延遲和CPU資源占用,但延遲比較均勻。6. PhasedBackoffWaitStrategy :上面多種策略的綜合,CPU資源的占用少,延遲大。
  • Event:數據從生產者傳遞給消費者的數據單元。
  • EventProcessor:處理Disruptor中的events的主事件循環,擁有消費者Sequence的所有權。其中BatchEventProcessor即實現了有效率的event loop,而且可以回調給實現了EventHandler接口的類。
  • EventHandler:Disruptor 定義的事件處理接口,由用戶實現,用於處理事件,是Consumer的真正實現。
  • Producer:即生產者,只是泛指調用 Disruptor 發布事件的用戶代碼,Disruptor 沒有定義特定接口或類型。

將這些元素放入Disruptor的context中,Disruptor的整體結構圖如下:

多播事件

Queue和Disruptor之間最大的差異。當有多個消費者監聽在同一Disruptor的所有事件,一個單一的事件只會被發送到一個單一的消費者。Disruptor一個使用的case是當你需要對同樣的數據進行不一樣的操作的時候。LMAX典型的例子是,我們有三個操作,日志(輸入數據寫入持久性日志文件),復制(將輸入數據發送到另一台機器以確保有數據的遠程復制),和業務邏輯(實際處理工作)。普通的Executor-style處理,可能是利用WorkPool並行的來處理這些不同的事件。這樣卻不是實現這個目標最有效的途徑。

如上圖所示,我們有三個EventHandler(JournalConsumer, ReplicationConsumer and ApplicationConsumer)監聽着Disruptor,每一個Handler都會順序的收到Disruptor里所有可用的消息,這樣就使得這些消費者可以並行的處理這些消息了。

為了支持現實中並行處理的應用,必須支持消費者之間的協調。回到上面的例子,防止業務邏輯的消費還在繼續,日志和復制的消費者已經完成了他們的任務是必須的。我們把這個概念稱為門,或者更准確地說,這個行為的超級集合的特征叫做門。門發生在兩個地方。首先,我們需要確保生產者不超過消費者。這是通過添加有關消費者到Disruptor時通過調用RingBuffer.addgatingconsumers() 實現的。其次,通過實現一個SequenceBarrier(內存屏障)的結構可以實現必須先完成某些操作的需求。

參考圖1,有三個消費者監聽喚醒隊列中的事件,在圖中有一個依賴圖,ApplicationConsumer依賴於 JournalConsumer 和 ReplicationConsumer,這就說明 JournalConsumer 和 ReplicationConsumer可以互相自由的並發,這層依賴關系可以從 ApplicationConsumer的 SequenceBarrier連接到 JournalConsumer和 ReplicationConsumer的 Sequences看出來。值得注意的是 Sequencer和下游消費者之間的關系。作用之一就是確保發布不會覆蓋Ring Buffer。為了做到這一點,下游消費者沒有一個序列比RingBuffer的Sequence還要小,比RingBuffer的size還要小,然而,利用這個依賴圖可以做一些有意思的操作,因為ApplicationConsumers Sequence是小於JournalConsumer 和 ReplicationConsumer(這就是依賴圖所保證的),Sequencer只用關注ApplicationConsumer的Sequence即可,其實一般意義上,Sequencer只用知道消費者的Sequences依賴樹中的葉子節點即可。

事件預分配

Disruptor的設計的一個目標就是能被用在一個低延遲的環境中。在低延遲系統中,必須減少或移除內存分配操作,基於Java開發的目的就是減少垃圾回收。(在低延遲的C/C++系統中,大內存分配也存在問題,因為內存分配器也會存在競爭)

為了實現低延遲,Disruptor允許用戶對事件的內存進行預分配,在構造過程和用戶提供的EventFactory中都會在Disruptor 的 RingBuffer中為每個實體分配。當發布新數據到Disruptor中,API就會允許用戶獲取構造方法的對象,以至於可以調用方法或者更新字段。Disruptor對這些操作提供並發安全性的保障。

可選的無鎖操作

另一個關鍵的實現低延遲的細節就是在Disruptor中利用無鎖的算法,所有內存的可見性和正確性都是利用內存屏障或者CAS操作。使用CAS來保證多線程安全,與大部分並發隊列使用的鎖相比,CAS顯然要快很多。CAS是CPU級別的指令,更加輕量,不必像鎖一樣需要操作系統提供支持,所以每次調用不需要在用戶態與內核態之間切換,也不需要上下文切換。

只有一個用例中鎖是必須的,那就是BlockingWaitStrategy(阻塞等待策略),唯一的實現方法就是使用Condition實現消費者在新事件到來前等待。許多低延遲系統使用忙等待去避免Condition的抖動,然而在系統忙等待的操作中,性能可能會顯著降低,尤其是在CPU資源嚴重受限的情況下,例如虛擬環境下的WEB服務器。

參考資料:
LMAX Disruptor
Spark性能優化指南——基礎篇- - 美團點評技術團隊
Disruptor入門


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM