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

1. Disruptor簡單介紹

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

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

Disruptor能夠用來解決併發編程中的一個廣泛的問題: 消息隊列的處理(producer和consumer)。github

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入門

相關文章
相關標籤/搜索