Disruptor是一個低延遲(low-latency),高吞吐量(high-throughput)的事件發(fā)布訂閱框架。通過Disruptor,可以在一個JVM中發(fā)布事件,和訂閱事件。相對于Java中的阻塞隊列(ArrayBlockingQueue,LinkedBlockingQueue),Disruptor的優(yōu)點是性能更高。它采用了一種無鎖的數(shù)據(jù)結(jié)構(gòu)設(shè)計,利用環(huán)形數(shù)組(RingBuffer)來存放事件,通過對象復(fù)用減少垃圾回收進一步提高性能。
從"慢日志"說起
線上有一個接口最近頻繁報警(tp99變高),通過監(jiān)控報警系統(tǒng)定位到問題主要出現(xiàn)在日志打印環(huán)節(jié)。接口方法入?yún)⒑统鰠⒍紩蛴?info"日志,我們采用的日志是logback。它默認(rèn)的是同步打印日志,在日志報文過大時,磁盤IO耗時會變得更加明顯。某個慢請求90%的處理時間都消耗在日志打印中。于是我們決定采用異步的方式打印日志。sl4j2日志框架支持異步的日志打印,改成異步日志打印之后接口性能報警消失。而sl4j2高性能的秘密就在于Disruptor。
Disruptor解決的問題
設(shè)想一下,在一個JVM中當(dāng)我們有多個消息的生產(chǎn)者線程,一個消費者線程時,他們之間如何進行高并發(fā)、線程安全的協(xié)調(diào)?很簡單,用一個阻塞隊列。
當(dāng)我們有多個消息的生產(chǎn)者線程,多個消費者線程,并且每一條消息需要被所有的消費者都消費一次(這就不是一般隊列,只消費一次的語義了),該怎么做?
這時仍然需要一個隊列。但是:
- 每個消費者需要自己維護一個指針,知道自己消費了隊列中多少數(shù)據(jù)。這樣同一條消息,可以被多個人獨立消費。
- 隊列需要一個全局指針,指向最后一條被所有生產(chǎn)者加入的消息。消費者在消費數(shù)據(jù)時,不能消費到這個全局指針之后的位置——因為這個全局指針,已經(jīng)是代表隊列中最后一條可以被消費的消息了。
- 需要協(xié)調(diào)所有消費者,在消費完所有隊列中的消息后,阻塞等待。
- 如果消費者之間有依賴關(guān)系,即對同一條消息的消費順序,在業(yè)務(wù)上有固定的要求,那么還需要處理誰先消費,誰后消費同一條消息的問題。
總而言之,如果有多個生產(chǎn)者,多個消費者,并且同一條消息要給到所有的消費者都去處理一下,需要做到以上4點。這是不容易的。
LMAX Disruptor,正是這種場景下,滿足以上4點要求的單機跨線程消息傳遞、分發(fā)的開源、高性能實現(xiàn)。
關(guān)鍵概念
RingBuffer
應(yīng)用需要傳遞的消息在Disruptor中稱為Event(事件)。
RingBuffer是Event的數(shù)組,實現(xiàn)了阻塞隊列的語義:
如果RingBuffer滿了,則生產(chǎn)者會阻塞等待。
如果RingBuffer空了,則消費者會阻塞等待。Sequence
在上文中,我提到“每個消費者需要自己維護一個指針”。這里的指針就是一個單調(diào)遞增長整數(shù)(及其基于CAS的加法、獲取操作),稱為Sequence。
除了每個消費者需要維護一個指針外,RingBuffer自身也要維護一個全局指針(如上一節(jié)第2點所提到的),記錄最后一條可以被消費的消息。
生產(chǎn)場景實現(xiàn)
生產(chǎn)者往RingBuffer中發(fā)送一條消息(RingBuffer.publish())時:
- 生產(chǎn)者的私有sequence會+1
- 檢查生產(chǎn)者的私有sequence與RingBuffer中Event個數(shù)的關(guān)系。如果發(fā)現(xiàn)Event數(shù)組滿了(下圖紅框中的判斷),則阻塞(下圖綠框中的等待)。
- RingBuffer會在Event數(shù)組中(sequencer+1) % BUFFER_SIZE的地方,放入Event。這里的取模操作,就體現(xiàn)了Event數(shù)組用到最后,則回到頭部繼續(xù)放,所謂"Ring" Buffer的輪循復(fù)用語義。
消費場景實現(xiàn)
消費者從RingBuffer循環(huán)隊列中獲取一條消息時:
- 從消費者私有Sequence,可以知道它自己消費到了RingBuffer隊列中的哪一條消息。
- 從RingBuffer的全局指針Sequence,可以知道RingBuffer中最后一條沒有被消費的消息在什么位置。
- N = (RuingBuffer的全局指針Sequence - 消費者私有Sequence),就是當(dāng)前消費者,還可以消費多少Event。
- 如果以上差值N為0,說明當(dāng)前消費者已經(jīng)消費過RingBuffer中的所有消息了。那么當(dāng)前消費者會阻塞。等待生產(chǎn)者加入更多的消息。
- 如果RingBuffer中,還有可以被當(dāng)前消費者消費的Event,即N > 0,
那么消費者,會一口氣獲取所有可以被消費的N個Event。這種一口氣消費盡量多的Event,是高性能的體現(xiàn)。
從RingBuffer中每獲取一個Event,都會回調(diào)綠框中的eventHandler——這是應(yīng)用注冊的Event處理方法,執(zhí)行應(yīng)用的Event消費業(yè)務(wù)邏輯。
高性能的實現(xiàn)細(xì)節(jié)
無鎖,無鎖就沒有鎖競爭。當(dāng)生產(chǎn)者、消費者線程數(shù)很高時,意義重大。所以,
往大里說,每個消費者維護自己的Sequence,基本沒有跨線程共享的狀態(tài)。
往小里說,Sequence的加法是CAS實現(xiàn)的。
當(dāng)生產(chǎn)者需要判斷RingBuffer是否已滿時,用CAS比較原先RingBuffer的Event個數(shù),和假定放入新Event后Event的個數(shù)。
如果CAS返回false,說明在判斷期間,別的生產(chǎn)者加入了新Event;或者別的消費者拿走了Event。那么當(dāng)前判斷無效,需要重新判斷。對象的復(fù)用,JVM運行時,一怕創(chuàng)建大對象,二怕創(chuàng)建很多小對象。這都會導(dǎo)致JVM堆碎片化、對象元數(shù)據(jù)存儲的額外開銷大。這是高性能Java應(yīng)用的噩夢。
為了解決第二點“很多小對象”,主流開源框架都會自己維護、復(fù)用對象池。LMAX Disruptor也不例外。
生產(chǎn)者不是創(chuàng)建新的Event對象,放入到RingBuffer中。而是從RingBuffer中取出一個已有的Event對象,更新它所指向的業(yè)務(wù)數(shù)據(jù),來代表一個邏輯上的新Event。