淺談Disruptor

zhong0316發表於2019-03-04

Disruptor是一個低延遲(low-latency),高吞吐量(high-throughput)的事件釋出訂閱框架。通過Disruptor,可以在一個JVM中釋出事件,和訂閱事件。相對於Java中的阻塞佇列(ArrayBlockingQueue,LinkedBlockingQueue),Disruptor的優點是效能更高。它採用了一種無鎖的資料結構設計,利用環形陣列(RingBuffer)來存放事件,通過物件複用減少垃圾回收進一步提高效能。

從”慢日誌”說起

線上有一個介面最近頻繁報警(tp99變高),通過監控報警系統定位到問題主要出現在日誌列印環節。介面方法入參和出參都會列印”info”日誌,我們採用的日誌是logback。它預設的是同步列印日誌,在日誌報文過大時,磁碟IO耗時會變得更加明顯。某個慢請求90%的處理時間都消耗在日誌列印中。於是我們決定採用非同步的方式列印日誌。sl4j2日誌框架支援非同步的日誌列印,改成非同步日誌列印之後介面效能報警消失。而sl4j2高效能的祕密就在於Disruptor。

Disruptor解決的問題

設想一下,在一個JVM中當我們有多個訊息的生產者執行緒,一個消費者執行緒時,他們之間如何進行高併發、執行緒安全的協調?很簡單,用一個阻塞佇列。
當我們有多個訊息的生產者執行緒,多個消費者執行緒,並且每一條訊息需要被所有的消費者都消費一次(這就不是一般佇列,只消費一次的語義了),該怎麼做?
這時仍然需要一個佇列。但是:

  1. 每個消費者需要自己維護一個指標,知道自己消費了佇列中多少資料。這樣同一條訊息,可以被多個人獨立消費。
  2. 佇列需要一個全域性指標,指向最後一條被所有生產者加入的訊息。消費者在消費資料時,不能消費到這個全域性指標之後的位置——因為這個全域性指標,已經是代表佇列中最後一條可以被消費的訊息了。
  3. 需要協調所有消費者,在消費完所有佇列中的訊息後,阻塞等待。
  4. 如果消費者之間有依賴關係,即對同一條訊息的消費順序,在業務上有固定的要求,那麼還需要處理誰先消費,誰後消費同一條訊息的問題。

總而言之,如果有多個生產者,多個消費者,並且同一條訊息要給到所有的消費者都去處理一下,需要做到以上4點。這是不容易的。
LMAX Disruptor,正是這種場景下,滿足以上4點要求的單機跨執行緒訊息傳遞、分發的開源、高效能實現。

關鍵概念

  1. RingBuffer
    應用需要傳遞的訊息在Disruptor中稱為Event(事件)。
    RingBuffer是Event的陣列,實現了阻塞佇列的語義:
    如果RingBuffer滿了,則生產者會阻塞等待。
    如果RingBuffer空了,則消費者會阻塞等待。

  2. Sequence
    在上文中,我提到“每個消費者需要自己維護一個指標”。這裡的指標就是一個單調遞增長整數(及其基於CAS的加法、獲取操作),稱為Sequence。
    除了每個消費者需要維護一個指標外,RingBuffer自身也要維護一個全域性指標(如上一節第2點所提到的),記錄最後一條可以被消費的訊息。

生產場景實現

生產者往RingBuffer中傳送一條訊息(RingBuffer.publish())時:

  1. 生產者的私有sequence會+1
  2. 檢查生產者的私有sequence與RingBuffer中Event個數的關係。如果發現Event陣列滿了(下圖紅框中的判斷),則阻塞(下圖綠框中的等待)。
  3. RingBuffer會在Event陣列中(sequencer+1) % BUFFER_SIZE的地方,放入Event。這裡的取模操作,就體現了Event陣列用到最後,則回到頭部繼續放,所謂”Ring” Buffer的輪循複用語義。

消費場景實現

消費者從RingBuffer迴圈佇列中獲取一條訊息時:

  1. 從消費者私有Sequence,可以知道它自己消費到了RingBuffer佇列中的哪一條訊息。
  2. 從RingBuffer的全域性指標Sequence,可以知道RingBuffer中最後一條沒有被消費的訊息在什麼位置。
  3. N = (RuingBuffer的全域性指標Sequence – 消費者私有Sequence),就是當前消費者,還可以消費多少Event。
  4. 如果以上差值N為0,說明當前消費者已經消費過RingBuffer中的所有訊息了。那麼當前消費者會阻塞。等待生產者加入更多的訊息。
  5. 如果RingBuffer中,還有可以被當前消費者消費的Event,即N > 0,
    那麼消費者,會一口氣獲取所有可以被消費的N個Event。這種一口氣消費盡量多的Event,是高效能的體現。
    從RingBuffer中每獲取一個Event,都會回撥綠框中的eventHandler——這是應用註冊的Event處理方法,執行應用的Event消費業務邏輯。

高效能的實現細節

  1. 無鎖,無鎖就沒有鎖競爭。當生產者、消費者執行緒數很高時,意義重大。所以,
    往大里說,每個消費者維護自己的Sequence,基本沒有跨執行緒共享的狀態。
    往小裡說,Sequence的加法是CAS實現的。
    當生產者需要判斷RingBuffer是否已滿時,用CAS比較原先RingBuffer的Event個數,和假定放入新Event後Event的個數。
    如果CAS返回false,說明在判斷期間,別的生產者加入了新Event;或者別的消費者拿走了Event。那麼當前判斷無效,需要重新判斷。

  2. 物件的複用,JVM執行時,一怕建立大物件,二怕建立很多小物件。這都會導致JVM堆碎片化、物件後設資料儲存的額外開銷大。這是高效能Java應用的噩夢。
    為了解決第二點“很多小物件”,主流開源框架都會自己維護、複用物件池。LMAX Disruptor也不例外。
    生產者不是建立新的Event物件,放入到RingBuffer中。而是從RingBuffer中取出一個已有的Event物件,更新它所指向的業務資料,來代表一個邏輯上的新Event。