本文是筆者在研究Disruptor過(guò)程中翻譯的Disruptor1.0論文精選,中間穿插了一些感想和說(shuō)明,均以“譯注”的形式說(shuō)明。
論文原地址:https://lmax-exchange.github.io/disruptor/files/Disruptor-1.0.pdf
Disruptor: 用于并發(fā)線程間數(shù)據(jù)交換的有界隊(duì)列的高性能替代
- Martin Thompson
- Dave Farley
- Michael Barker
- Patricia Gee
- Andrew Stewart
May-2011
1 摘要
LMAX是為了創(chuàng)造一個(gè)極高性能的金融交易所而成立。作為實(shí)現(xiàn)這一目標(biāo)的工作的一部分,我們已經(jīng)評(píng)估了這種系統(tǒng)的幾種設(shè)計(jì)方法,但是隨著開始測(cè)試,我們遇到了使用傳統(tǒng)方法的一些基本限制。
許多應(yīng)用程序依賴隊(duì)列在不同的處理階段之間交換數(shù)據(jù)。我們的性能測(cè)試表明,以這種方式使用隊(duì)列時(shí)的延遲成本與IO操作到磁盤(基于RAID或SSD的磁盤系統(tǒng))的成本大致處于相同的數(shù)量級(jí) - 非常非常慢。如果端對(duì)端操作中有多個(gè)隊(duì)列,將為總延遲增加數(shù)百微秒。顯然有優(yōu)化的空間。
進(jìn)一步的調(diào)查和對(duì)計(jì)算機(jī)科學(xué)的關(guān)注使我們認(rèn)識(shí)到,傳統(tǒng)方法固有問(wèn)題的結(jié)合(例如隊(duì)列和處理節(jié)點(diǎn)) ,導(dǎo)致了多線程實(shí)現(xiàn)中的爭(zhēng)用,這表明可能有更好的方法。
考慮現(xiàn)代CPU如何工作,我們喜歡稱之為“機(jī)械同情”,使用良好的設(shè)計(jì)實(shí)踐,強(qiáng)調(diào)關(guān)注焦點(diǎn),我們提出了一種我們稱之為Disruptor的數(shù)據(jù)結(jié)構(gòu)和使用模式。
測(cè)試表明,使用Disruptor進(jìn)行三級(jí)流水線的平均延遲比基于隊(duì)列的等效方法低3個(gè)數(shù)量級(jí)。此外,Disruptor在相同配置下處理大約8倍的吞吐量。
這些性能改進(jìn)代表著圍繞并行編程的思考的一個(gè)改變。這種新模式是需要高吞吐量和低延遲的任何異步事件處理架構(gòu)的理想基礎(chǔ)。
在LMAX,我們已經(jīng)建立了一個(gè)訂單匹配引擎,實(shí)時(shí)風(fēng)險(xiǎn)管理和一個(gè)高可用的內(nèi)存中事務(wù)處理系統(tǒng),所有這一切都取得了巨大的成功。這些系統(tǒng)中的每一個(gè)都制定了新的性能標(biāo)準(zhǔn),據(jù)我們所知,這些標(biāo)準(zhǔn)是無(wú)與倫比的。
然而,這并不是一個(gè)只有在金融業(yè)才有意義的專業(yè)解決方案。 Disruptor是一種通用的機(jī)制,它能夠以最大化性能的方式解決并發(fā)編程中的復(fù)雜問(wèn)題,而且實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單。盡管有些概念看起來(lái)很不尋常,但我們的經(jīng)驗(yàn)是,建立在這種模式上的系統(tǒng)要比類似的機(jī)制要簡(jiǎn)單得多。
Disruptor顯著減少了寫時(shí)爭(zhēng)用,并發(fā)開銷較低,并且比可當(dāng)前其他方法更加緩存友好,所有這些都會(huì)導(dǎo)致更高的吞吐量,更低的延遲且具有較少的抖動(dòng)。 在中等時(shí)鐘速率的處理器上,我們已經(jīng)看到每秒超過(guò)2500萬(wàn)條消息,延遲低于50納秒。 與我們看到的任何其他實(shí)現(xiàn)相比,這種性能是一個(gè)明顯的改進(jìn)。 這非常接近現(xiàn)代處理器在內(nèi)核之間交換數(shù)據(jù)的理論限制。
(譯注:一句話總結(jié),現(xiàn)有的隊(duì)列結(jié)構(gòu)限制了并發(fā)編程的效率,Disruptor用于代替隊(duì)列實(shí)現(xiàn)高效地線程間的數(shù)據(jù)交換。)
2 概述
Disruptor是我們努力在LMAX建立世界最高性能金融交易所的結(jié)果。 早期的設(shè)計(jì)集中在從SEDA和Actors派生的架構(gòu),使用管道(pipelines)提升吞吐量。 在對(duì)各種實(shí)現(xiàn)進(jìn)行了分析之后,很明顯,在管道之間的各個(gè)階段之間的排隊(duì)是控制成本的主要因素。我們發(fā)現(xiàn)隊(duì)列也引入了延遲和高水平的抖動(dòng)。 我們花了大量精力開發(fā)具有更好性能的新隊(duì)列實(shí)現(xiàn)。 然而,由于對(duì)生產(chǎn)者,消費(fèi)者和他們的數(shù)據(jù)存儲(chǔ)的設(shè)計(jì)問(wèn)題的混淆,作為基本數(shù)據(jù)結(jié)構(gòu)的隊(duì)列變得非常有限。 Disruptor是我們的工作成果,用于構(gòu)建一個(gè)能將這些問(wèn)題劃分清楚的并發(fā)結(jié)構(gòu)。
3并發(fā)的復(fù)雜性
在本文檔和計(jì)算機(jī)科學(xué)的上下文中,并發(fā)不僅意味著兩個(gè)或多個(gè)任務(wù)并行發(fā)生,而且他們也在爭(zhēng)奪資源的訪問(wèn)。競(jìng)爭(zhēng)資源可能是數(shù)據(jù)庫(kù),文件,套接字甚至內(nèi)存中的位置。
代碼的并發(fā)執(zhí)行大約是兩件事,互斥和變化的可見性。互斥是關(guān)于管理某些資源的競(jìng)爭(zhēng)更新。變化的可見性是關(guān)于控制何時(shí)使這些更改對(duì)其他線程可見。如果可以消除對(duì)競(jìng)爭(zhēng)更新的需求,可以避免互斥的需要。如果您的算法能夠保證任何給定的資源只被一個(gè)線程修改,那么就不需要相互排斥。讀和寫操作要求所有的更改都對(duì)其他線程可見。然而,只有爭(zhēng)用的寫操作需要相互排斥更改。
任何并發(fā)環(huán)境中,最昂貴的操作是爭(zhēng)用的寫訪問(wèn)。要將多個(gè)線程寫入相同的資源需要復(fù)雜且昂貴的協(xié)調(diào)。通常情況下,這是通過(guò)采用某種鎖定策略實(shí)現(xiàn)的。
3.1 鎖的成本
通過(guò)操作系統(tǒng)內(nèi)核的上下文切換實(shí)現(xiàn),會(huì)暫停線程去等待鎖直到釋放。執(zhí)行這樣的上下文切換,會(huì)丟失之前保存的數(shù)據(jù)和指令。
循環(huán)5億次64位計(jì)數(shù)操作使用時(shí)間對(duì)比:
3.2 CAS的成本
CAS(Compare And Swap)操作是特殊的機(jī)器碼指令,允許以原子操作對(duì)內(nèi)存中的字(word)進(jìn)行有條件地設(shè)置。如x86上的"lock cmpxchg"。
對(duì)于“遞增計(jì)數(shù)器實(shí)驗(yàn)”,每個(gè)線程可以循環(huán)讀取計(jì)數(shù)器,然后嘗試將其原子地設(shè)置為其新的遞增值。舊的和新的值作為該指令的參數(shù)提供。如果在執(zhí)行操作時(shí),計(jì)數(shù)器的值與提供的預(yù)期值相匹配,計(jì)數(shù)器將用新值更新。另一方面,如果實(shí)際值與預(yù)期值不等,CAS操作將失敗。然后由線程嘗試執(zhí)行更改以重試,重新讀取從該值增加的計(jì)數(shù)器,依此類推,直到更改成功。
可以不斷進(jìn)行重讀、重試,直到操作成功。與鎖相比,避免了上下文切換帶來(lái)的巨大開銷。但是并非沒(méi)有開銷。處理器必須鎖定指令流水線(instruction pipeline)來(lái)保證原子性,并且使用一個(gè)內(nèi)存屏障(memory barrier)使變化對(duì)于其他線程可見。
最優(yōu)算法是:一個(gè)線程執(zhí)行所有寫操作,其他線程讀取結(jié)果。在多處理器環(huán)境下讀取結(jié)果需要內(nèi)存屏障來(lái)保證變化對(duì)運(yùn)行在其他處理器上的線程可見。
3.3 內(nèi)存屏障
出于性能原因,現(xiàn)代處理器使用亂序的指令執(zhí)行和亂序的數(shù)據(jù)載入及存儲(chǔ)(內(nèi)存和執(zhí)行單元間)。處理器需要保證的只是,程序邏輯結(jié)果不受亂序影響。
處理器使用內(nèi)存屏障來(lái)指示內(nèi)存更新排序很重要的代碼段。它們是在線程之間實(shí)現(xiàn)硬件排序和改變可見性的手段。 編譯器可以添加軟件屏障,以確保編譯代碼的排序,這些軟件內(nèi)存障礙是作為處理器硬件屏障的附加而由處理器使用的。
讀內(nèi)存屏障通過(guò)在“內(nèi)存變化無(wú)效隊(duì)列”做標(biāo)記,來(lái)排序CPU裝載指令。寫內(nèi)存屏障通過(guò)在用于刷寫的存儲(chǔ)緩沖中做標(biāo)記,來(lái)排序存儲(chǔ)指令。一個(gè)完整的內(nèi)存屏障只在執(zhí)行它的CPU上排序裝載和存儲(chǔ)(指令)。
在JAVA中,通過(guò)volatile關(guān)鍵字實(shí)現(xiàn)讀寫屏障。由Java5內(nèi)存模型明確定義。
3.4 緩存行
現(xiàn)代處理器的緩存用法對(duì)于成功的高性能操作至關(guān)重要。目前來(lái)說(shuō),處理器在處理緩存中的數(shù)據(jù)和指令非常高效,相反的,當(dāng)緩存缺失(cache miss)時(shí)則非常低效。
硬件并非以字節(jié)或字長(zhǎng)為單位處理內(nèi)存。為了高效率,緩存被組織成大小為32到256字節(jié)的緩存行(cache-line),最常用的是64字節(jié)。這是高速緩存一致性協(xié)議的粒度級(jí)別。
上面的意思是:如果有兩個(gè)變量在相同的緩存行中,且這兩個(gè)變量是被不同線程寫入,這是就會(huì)存在寫入爭(zhēng)用問(wèn)題,就好像這兩個(gè)變量是一個(gè)變量一樣(兩個(gè)線程雖然操作的是各自的變量,但會(huì)導(dǎo)致同一個(gè)緩存行失效)。這就是偽共享(false sharding)。從高性能上考慮,重要的是要確保爭(zhēng)用最小化,獨(dú)立但并發(fā)寫入的變量不會(huì)共享相同的高速緩存行。當(dāng)以可預(yù)測(cè)的方式訪問(wèn)主存時(shí),CPU可以通過(guò)預(yù)測(cè)將要訪問(wèn)的位置進(jìn)行預(yù)讀取來(lái)隱藏訪問(wèn)主存的成本。但是如果是遍歷鏈表或樹這樣的結(jié)構(gòu)時(shí),CPU就不能預(yù)測(cè)下一步的訪問(wèn)位置了。這將最終導(dǎo)致訪問(wèn)效率下降2個(gè)數(shù)量級(jí)。
3.5 Queue的問(wèn)題
Queue通常使用鏈表或數(shù)組結(jié)構(gòu)存儲(chǔ)元素。使用無(wú)界隊(duì)列通常都需要保證生產(chǎn)者生產(chǎn)速度小于消費(fèi)者消費(fèi)速度,否則將會(huì)由于耗盡內(nèi)存導(dǎo)致災(zāi)難結(jié)果。為了避免這種結(jié)果,隊(duì)列通常是有界的。保持隊(duì)列有界要么使用數(shù)組,要么就是能夠主動(dòng)跟蹤大小。
隊(duì)列實(shí)現(xiàn)傾向于對(duì)head,tail和size變量進(jìn)行寫入爭(zhēng)用。 在使用中,由于消費(fèi)者和生產(chǎn)者之間的差異,隊(duì)列通常總是接近滿或接近空。 他們很少在平衡的中間地帶進(jìn)行生產(chǎn)和消費(fèi)的比例均勻匹配。 總是充滿或總是空的傾向?qū)е赂咚降臓?zhēng)用和/或昂貴的緩存一致性。 問(wèn)題是,即使使用諸如鎖或CAS變量的不同并發(fā)對(duì)象來(lái)分離頭尾機(jī)制,它們通常也占用相同的高速緩存行。
使用一個(gè)粗粒度的鎖管理整個(gè)queue的put和take非常容易實(shí)現(xiàn),但是也很容易成為吞吐量瓶頸。而使用更細(xì)粒度的控制就需要更復(fù)雜的并發(fā)設(shè)計(jì)。如果將并發(fā)問(wèn)題從queue的語(yǔ)義中剔除,queue的實(shí)現(xiàn)無(wú)非就是單生產(chǎn)者-單消費(fèi)者的模式。
在Java中,使用隊(duì)列還有一個(gè)問(wèn)題-隊(duì)列是垃圾的重要來(lái)源。首先,必須分配對(duì)象并將其放置在隊(duì)列中。 其次,如果支持鏈表,則必須分配表示列表節(jié)點(diǎn)的對(duì)象。 當(dāng)不再引用時(shí),需要重新聲明分配給支持隊(duì)列實(shí)現(xiàn)的所有這些對(duì)象。
(譯注:簡(jiǎn)單總結(jié),使用隊(duì)列時(shí),由于消費(fèi)者生產(chǎn)者之間的差異,導(dǎo)致并發(fā)損耗;存在偽共享問(wèn)題;隊(duì)列元素是垃圾重要來(lái)源。)
3.6 流水線和圖表
對(duì)于許多類問(wèn)題,將幾個(gè)處理階段組織成(wire together)管道是有意義的。 這樣的管道通常具有并行路徑,被組織成圖形狀的拓?fù)?topologies)。 每個(gè)階段之間的鏈接通常由隊(duì)列實(shí)現(xiàn),每個(gè)階段都有自己的線程。
這種方法并不廉價(jià) - 在每個(gè)階段,都必須承擔(dān)入隊(duì)和出隊(duì)的消耗。 當(dāng)路徑必須分叉(fork)時(shí),目標(biāo)的數(shù)量乘以這個(gè)成本,并且在這樣一個(gè)分支之后必須重新加入時(shí)會(huì)產(chǎn)生不可避免的爭(zhēng)用成本(譯注:如一個(gè)事件既需要被記錄日志,也需要被業(yè)務(wù)邏輯消費(fèi))。
如果依賴圖的表達(dá)不會(huì)導(dǎo)致階段之間放置隊(duì)列的消耗,將是最理想的。(譯注:將不同階段通過(guò)隊(duì)列組成拓?fù)鋱D時(shí),避免隊(duì)列本身的消耗)
4 LMAX Disruptor的設(shè)計(jì)思想
設(shè)計(jì)Disruptor就是為了解決以上問(wèn)題。之所以叫做"Disruptor",是因?yàn)樵贘ava7中,用于支持Fork-join的"Phaser"在處理依賴圖表時(shí)有相似的地方。(譯注:Phaser相位槍、Disruptor裂解炮都是星際迷航中的武器,Phaser是聯(lián)邦武器,Disruptor是克林貢的等價(jià)物)
4.1 內(nèi)存分配
環(huán)形緩沖(Ring Buffer)的所有內(nèi)存都是在啟動(dòng)時(shí)預(yù)分配的。環(huán)形緩沖既可以存儲(chǔ)條目(entry)指針的數(shù)組,也可以存儲(chǔ)代表?xiàng)l目的結(jié)構(gòu)數(shù)組。Java語(yǔ)言限制了條目以對(duì)象指針的形式與環(huán)形緩沖相關(guān)聯(lián)。每個(gè)條目都并非數(shù)據(jù)本身,而是一個(gè)容器。條目的預(yù)分配消除了支持垃圾回收編程語(yǔ)言的問(wèn)題,所有的條目在Disruptor實(shí)例生存周期中都將重用并一直存在。這些條目的內(nèi)存同時(shí)被分配,將有極大可能在主存中連續(xù)分配,這樣就支持了緩存位置預(yù)測(cè)(cache striding,緩存跨越)。這是由John Rose提出的建議,將“值類型”引入到j(luò)ava語(yǔ)言,允許類似C語(yǔ)言中的元組數(shù)組那樣,來(lái)保證內(nèi)存的連續(xù)分配而避免指針間接尋址。
在受管理的運(yùn)行時(shí)環(huán)境(如Java)中開發(fā)低延遲系統(tǒng)時(shí),垃圾收集可能會(huì)有問(wèn)題。 分配的內(nèi)存越多,垃圾收集器的負(fù)擔(dān)就越大。 垃圾收集者在對(duì)象生命周期非常短暫或有效永生(譯注:而非內(nèi)存泄露引起的永生)的情況下工作在最佳狀態(tài)。 在環(huán)形緩沖區(qū)中預(yù)先分配條目意味著對(duì)于垃圾收集器而言,它是永生的,因此代表著很小的負(fù)擔(dān)。
在重負(fù)載隊(duì)列系統(tǒng)中進(jìn)行備份,會(huì)引起處理速率的下降,導(dǎo)致已分配對(duì)象生存的時(shí)候更久,將會(huì)被垃圾收集器提升到老年代。這意味著兩點(diǎn):一不同代之間的對(duì)象復(fù)制會(huì)造成延遲抖動(dòng);二垃圾收集器回收老年代對(duì)象代價(jià)更大,而且當(dāng)內(nèi)存碎片空間需要壓縮時(shí),會(huì)增加“Stop the world”停頓的可能性。在大內(nèi)存堆中可能會(huì)導(dǎo)致每GB幾秒的停頓。
4.2 梳理問(wèn)題
我們?cè)谒嘘?duì)列實(shí)現(xiàn)中總結(jié)了以下問(wèn)題:
a. 被交換元素如何存儲(chǔ)
b. 如何協(xié)調(diào)生產(chǎn)者聲明(claiming 取得)下一個(gè)用于交換(exchange)的序號(hào)
c. 在新元素可用時(shí)如何協(xié)調(diào)要通知的消費(fèi)者
在使用垃圾回收的語(yǔ)言設(shè)計(jì)金融交易系統(tǒng)時(shí),內(nèi)存分配過(guò)多可能會(huì)造成問(wèn)題。 所以,正如我們描述的使用支持鏈表的隊(duì)列不是一個(gè)好辦法。 如果可以預(yù)先分配用于處理階段之間的數(shù)據(jù)交換的整個(gè)存儲(chǔ)空間,則垃圾收集將被最小化。 此外,如果這種分配可以在統(tǒng)一的塊中執(zhí)行,則將以對(duì)現(xiàn)代處理器采用的緩存策略非常友好的方式來(lái)完成該數(shù)據(jù)的遍歷。 滿足此要求的數(shù)據(jù)結(jié)構(gòu)是預(yù)先填充所有插槽的數(shù)組。 在創(chuàng)建環(huán)形緩沖區(qū)時(shí),Disruptor利用抽象工廠模式預(yù)先分配條目。 當(dāng)聲明條目時(shí),生產(chǎn)者可以將其數(shù)據(jù)復(fù)制到預(yù)先分配的結(jié)構(gòu)中。
在大多數(shù)處理器上,對(duì)序列號(hào)進(jìn)行余數(shù)計(jì)算的成本非常高,這決定了環(huán)中的插槽。 通過(guò)使環(huán)形緩沖器大小為2的平方,可以大大降低該成本。可以使用大小(size)減去1的位掩碼來(lái)有效地執(zhí)行求余(remainder)操作。(如size為8,序號(hào)為14,則序號(hào)1110 & 111 = 110即6)(譯注:注意,此處原文應(yīng)該有誤,實(shí)際上這里應(yīng)為求模操作,這部分的源碼會(huì)在后續(xù)講)
就像先前描述的,有界隊(duì)列的頭部和維護(hù)遭受爭(zhēng)用。環(huán)形緩沖器的數(shù)據(jù)結(jié)構(gòu)免除了這種爭(zhēng)用和并發(fā)原語(yǔ),因?yàn)檫@些問(wèn)題被轉(zhuǎn)移到了環(huán)形緩沖器關(guān)聯(lián)的生產(chǎn)者和消費(fèi)者的屏障(barrier)中。屏障邏輯如下:
大多數(shù)情況下Disruptor只使用一個(gè)生產(chǎn)者。通常生產(chǎn)者是文件讀取器或網(wǎng)絡(luò)監(jiān)聽器。當(dāng)只有一個(gè)生產(chǎn)者時(shí),當(dāng)然沒(méi)有序號(hào)(sequence)和條目分配的爭(zhēng)用。
在有多個(gè)生產(chǎn)者的不常見情況下,多個(gè)生產(chǎn)者將會(huì)競(jìng)爭(zhēng)聲明(claim)環(huán)形緩沖器中下一個(gè)條目。這種(多生產(chǎn)者對(duì)同一個(gè)條目的)爭(zhēng)用可以使用一個(gè)簡(jiǎn)單的CAS操作得到管理。
當(dāng)生產(chǎn)者將關(guān)聯(lián)數(shù)據(jù)復(fù)制進(jìn)聲明的條目中后,會(huì)通過(guò)提交這個(gè)條目的序號(hào)讓此條目對(duì)消費(fèi)者可見。這可以不使用CAS而使用一個(gè)簡(jiǎn)單的忙碌旋轉(zhuǎn)(busy spin)直到其他生產(chǎn)者在各自的提交中到達(dá)該序號(hào)。生產(chǎn)者可以提前一個(gè)游標(biāo)表示下一個(gè)可用條目的消費(fèi)。生產(chǎn)者可以在寫入環(huán)形緩沖區(qū)之前,通過(guò)跟蹤消費(fèi)者的序號(hào)作為簡(jiǎn)單的讀取操作來(lái)避免環(huán)繞環(huán)。
消費(fèi)者在讀取條目前先等待環(huán)形緩沖器中的序列可用。有多種等待策略可供選擇。如果CPU資源很寶貴,消費(fèi)者可以做鎖的條件等待生產(chǎn)者發(fā)送通知。這種做法會(huì)造成爭(zhēng)用,只有在CPU資源比延遲或吞吐更重要時(shí)才會(huì)用。消費(fèi)者也可以循環(huán)檢查代表當(dāng)前可用序列的游標(biāo)。這樣可以使用CPU資源交換更小的延遲,可以選擇用或者不用線程退讓(thread yield)。如果我們不使用鎖和條件變量,我們已經(jīng)打破了生產(chǎn)者和消費(fèi)者之間的競(jìng)爭(zhēng)依賴關(guān)系,這樣做非常好。無(wú)鎖的多生產(chǎn)者 - 多消費(fèi)者隊(duì)列確實(shí)存在,但是它們需要在頭部,尾部,大小計(jì)數(shù)器上進(jìn)行多個(gè)CAS操作。 Disruptor不能忍受這樣的CAS爭(zhēng)用。
4.3 序列(Sequencing)
序列是Disruptor中管理并發(fā)的核心概念。每個(gè)生產(chǎn)者和消費(fèi)者在同環(huán)形緩沖器交互時(shí),都采用嚴(yán)格的序列概念。當(dāng)生產(chǎn)者要求環(huán)形中一個(gè)條目時(shí),會(huì)要求序列中的下一個(gè)槽位。下一個(gè)可用槽位的序列可以是單生產(chǎn)者時(shí)的簡(jiǎn)單計(jì)數(shù)器,或者是多生產(chǎn)者時(shí)的使用CAS操作的原子計(jì)數(shù)器。當(dāng)序列值被聲明時(shí),環(huán)形緩沖器中的這個(gè)條目就可以被那個(gè)特定的生產(chǎn)者寫入了。當(dāng)生產(chǎn)者完成更新條目時(shí),它可以通過(guò)更新單獨(dú)的計(jì)數(shù)器來(lái)提交更改,該計(jì)數(shù)器表示環(huán)形緩沖區(qū)上的光標(biāo),以為消費(fèi)者提供可用的最新條目。環(huán)形緩沖器游標(biāo)可以被生產(chǎn)者使用內(nèi)存屏障以忙循環(huán)(busy spin)讀取和寫入,而無(wú)須使用以下的CAS操作:
long expectedSequence = claimedSequence – 1;
while (cursor != expectedSequence) {
// busy spin
}
cursor = claimedSequence;
消費(fèi)者通過(guò)使用內(nèi)存屏障來(lái)讀取游標(biāo)等待給定的序列可用。 一旦光標(biāo)被更新,內(nèi)存屏障就可以確保在緩沖區(qū)中條目的更改對(duì)于等待光標(biāo)前進(jìn)的消費(fèi)者是可見的。
每個(gè)消費(fèi)者都包含一個(gè)自己的處理環(huán)形緩沖器條目的序列。這些消費(fèi)者序列允許生產(chǎn)者跟蹤消費(fèi)者,防止繞環(huán)(譯注:prevent the ring from wrapping)。消費(fèi)者序列還允許消費(fèi)者以有序的方式在同一條目上協(xié)調(diào)工作。
在只有一個(gè)生產(chǎn)者的情況下,無(wú)論消費(fèi)者圖表的復(fù)雜程度如何,都不需要鎖或CAS操作。 可以通過(guò)上述討論的序列上的內(nèi)存屏障來(lái)實(shí)現(xiàn)整個(gè)并發(fā)協(xié)調(diào)。
4.4 批量效應(yīng)(Batching Effect)
當(dāng)消費(fèi)者等待環(huán)形緩沖器下一個(gè)游標(biāo)序列時(shí),可能會(huì)出現(xiàn)一個(gè)有趣的不太可能在隊(duì)列上出現(xiàn)的現(xiàn)象。如果消費(fèi)者發(fā)現(xiàn)環(huán)形緩沖區(qū)游標(biāo)自上次檢查以來(lái)已經(jīng)前進(jìn)了多步,則可以(譯注:從落后位置)一直處理到該序號(hào)位置,而不會(huì)涉及并發(fā)機(jī)制。 這導(dǎo)致滯后的消費(fèi)者在生產(chǎn)者加速向前時(shí)迅速趕上生產(chǎn)者的步伐,從而平衡系統(tǒng)。這種批處理增加了吞吐量,同時(shí)減少和平滑了延遲。基于我們的觀察結(jié)果,這種效應(yīng)不論負(fù)載如何將總是保持常量時(shí)間的延遲,直到存儲(chǔ)子系統(tǒng)飽和,根據(jù)利特爾法則(Little's Law),輪廓是線性的。這與我們?cè)跁r(shí)隊(duì)列觀察到的延遲的“J”曲線效應(yīng)非常不同。
(譯注:代碼實(shí)現(xiàn)為BatchEventProcessor)
4.5 依賴圖(Dependency Graphs)
一個(gè)隊(duì)列代表了生產(chǎn)者和消費(fèi)者之間僅一步流水線依賴。如果消費(fèi)者組成了一個(gè)依賴的鏈條或圖表結(jié)構(gòu),那么在圖結(jié)構(gòu)每個(gè)階段間都需要隊(duì)列。在依賴階段的圖中, 這樣增加了許多次的隊(duì)列固定消耗。在設(shè)計(jì)LMAX金融交易系統(tǒng)時(shí),我們的分析表明,使用基于隊(duì)列方法的消耗決定了處理交易的總消耗。
因?yàn)樵贒isruptor模式中,生產(chǎn)者和消費(fèi)者問(wèn)題是分開的,只在核心的一個(gè)環(huán)形結(jié)構(gòu)中表示出一個(gè)消費(fèi)者復(fù)雜依賴圖就成為了可能。這導(dǎo)致執(zhí)行的固定成本大大降低,從而提升了吞吐量同時(shí)減少了延遲。
單一一個(gè)環(huán)形緩沖器可以用來(lái)存儲(chǔ)可以表達(dá)整個(gè)工作流復(fù)雜結(jié)構(gòu)的條目。必須注意這種結(jié)構(gòu)的設(shè)計(jì),在被獨(dú)立消費(fèi)者寫入狀態(tài)時(shí)不能導(dǎo)致偽共享問(wèn)題。
(譯注:多任務(wù)流水線之間可能有依賴,如ABC為三個(gè)任務(wù),A->B->C是基本的流程,在多線程任務(wù)中,只有完成A的線程才能執(zhí)行B,再執(zhí)行C。)
4.6 Disruptor類設(shè)計(jì)
Disruptor框架的核心關(guān)系類圖描述如下。圖中省略了一些可以簡(jiǎn)化編程模型的工具類。生產(chǎn)者通過(guò)ProcuderBarrier按順序要求條目,將變化寫入要求的條目,再通過(guò)ProcuderBarrier把條目提交回去,讓它們可供消費(fèi)。每個(gè)消費(fèi)者需要做的是提供一個(gè)BatchHandler實(shí)現(xiàn),用于接收新條目可用時(shí)的回調(diào)。這種編程模型很類似Actor模型的事件驅(qū)動(dòng)模型。將通常合并在一起的隊(duì)列實(shí)現(xiàn)問(wèn)題分離開,將允許更大的設(shè)計(jì)自由度。一個(gè)環(huán)形緩沖器是Disruptor模式核心存在,提供了無(wú)爭(zhēng)用的數(shù)據(jù)存儲(chǔ)交換。并發(fā)問(wèn)題也被分成生產(chǎn)者和消費(fèi)者同環(huán)形緩沖器的交互。ProducerBarrier管理任意在環(huán)形緩沖器中聲明槽位的并發(fā)問(wèn)題,同時(shí)跟蹤獨(dú)立的消費(fèi)者來(lái)阻止環(huán)繞環(huán)(prevent the ring from wrapping)。當(dāng)新條目可用時(shí),ConsumerBarrier通知消費(fèi)者,消費(fèi)者可以被構(gòu)造成一個(gè)處理流水線的多階段依賴圖。
(譯注:Disruptor自發(fā)布以來(lái)已有若干變化。
Entry -> Event
Consumer -> EventProcessor
ProcuderBarrier -> AbstractSequencer
Consumer -> EventProcessor
ConsumerBarrier -> SequenceBarrier)
4.7 代碼樣例
單生產(chǎn)者和單消費(fèi)者模式。(譯注:由于代碼設(shè)計(jì)發(fā)生了較大變化,以下代碼只用作理解)
// Callback handler which can be implemented by consumers
final BatchHandler batchHandler = new BatchHandler()
{
public void onAvailable(final ValueEntry entry) throws Exception
{
// process a new entry as it becomes available.
}
public void onEndOfBatch() throws Exception
{
// useful for flushing results to an IO device if necessary.
}
public void onCompletion()
{
// do any necessary clean up before shutdown
}
};
RingBuffer ringBuffer = new RingBuffer(ValueEntry.ENTRY_FACTORY, SIZE, ClaimStrategy.Option.SINGLE_THREADED, WaitStrategy.Option.YIELDING);
ConsumerBarrier consumerBarrier = ringBuffer.createConsumerBarrier();
BatchConsumer batchConsumer = new BatchConsumer(consumerBarrier, batchHandler);
ProducerBarrier producerBarrier = ringBuffer.createProducerBarrier(batchConsumer);
// Each consumer can run on a separate thread
EXECUTOR.submit(batchConsumer);
// Producers claim entries in sequence ValueEntry
entry = producerBarrier.nextEntry();
// copy data into the entry container
// make the entry available to consumers
producerBarrier.commit(entry);
5 吞吐量性能測(cè)試
作為參考,我們選擇Doug Lea的出色的java.util.concurrent.ArrayBlockingQueue
,基于我們的測(cè)試它在任何有界隊(duì)列中具有最高性能。 測(cè)試以阻塞編程風(fēng)格進(jìn)行,以匹配Disruptor的編程風(fēng)格。 以下詳細(xì)的測(cè)試案例可在Disruptor開源項(xiàng)目中找到。 注意:運(yùn)行測(cè)試需要能夠并行執(zhí)行至少4個(gè)線程的系統(tǒng)。
(譯注:方便起見,使用P1代表第一個(gè)生產(chǎn)者,P2代表第二個(gè)生產(chǎn)者,C1代表第一個(gè)消費(fèi)者,C2代表第二個(gè)消費(fèi)者,以此類推)
單播:1P-1C
P1-> C1
三步流水線:1P-3C
P1 -> C1 -> C2 -> C3
多生產(chǎn)者: 3P-1C
P1,P2,P3 -> C1
多播(多消費(fèi)者):1P-3C
P1 -> C1, C2, C3
鉆石型:1P-3C
P1 -> C1, C2 -> C3
對(duì)于上述配置,與Disruptor的屏障配置相比,每個(gè)數(shù)據(jù)流弧應(yīng)用了ArrayBlockingQueue。 下表顯示了使用Java 1.6.0_25 64位Sun JVM,Windows 7,不帶超線程的Intel Core i7 860 @ 2.8 GHz和Intel Core i7-2720QM(Ubuntu 11.04)的操作的性能結(jié)果,并充分利用3次處理5億條消息的運(yùn)行結(jié)果。 不同的JVM執(zhí)行結(jié)果可能會(huì)有很大差異,下面的數(shù)字并不是我們觀察到的最高結(jié)果。
單位:操作/秒
6 延遲性能測(cè)試
為了測(cè)量延遲,我們采用三級(jí)流水線,并以小于飽和度的速度生成事件。這是通過(guò)在注入事件之前等待1微秒,然后再注入下一個(gè)并重復(fù)5000萬(wàn)次來(lái)實(shí)現(xiàn)。要按照這個(gè)精度要求,需要使用CPU的時(shí)間戳計(jì)數(shù)器(TSC)。我們選擇具有不變的TSC的CPU,因?yàn)橛捎谑‰姾退郀顟B(tài),較舊的處理器遭受頻率變化的影響。英特爾Nehalem和更高級(jí)的處理器使用一個(gè)不變的TSC,可以在Ubuntu 11.04上運(yùn)行的最新的Oracle JVM訪問(wèn)。該測(cè)試沒(méi)有使用CPU綁定。
為了比較,我們?cè)俅问褂肁rrayBlockingQueue。我們可以使用ConcurrentLinkedQueue,這可能會(huì)給出更好的結(jié)果,但是我們希望使用有界隊(duì)列實(shí)現(xiàn)來(lái)確保生產(chǎn)者不要超過(guò)消費(fèi)者創(chuàng)造背壓。以下結(jié)果是2.2Ghz Core i7-2720QM在Ubuntu 11.04上運(yùn)行Java 1.6.0_25 64位。
Disruptor的平均延遲時(shí)間為52納秒,而ArrayBlockingQueue32,757納秒。分析顯示使用鎖和信令通過(guò)條件變量是ArrayBlockingQueue的延遲的主要原因。
7 結(jié)論
Disruptor是提高吞吐量,減少并發(fā)執(zhí)行上下文之間的延遲并確保可預(yù)測(cè)延遲的重大進(jìn)步,這在許多應(yīng)用程序中是一個(gè)重要的考慮因素。我們的測(cè)試表明,它比在線程間交換數(shù)據(jù)的同類方法性能要好。我們認(rèn)為這是這種數(shù)據(jù)交換的最高性能機(jī)制。通過(guò)關(guān)注跨線程數(shù)據(jù)交換中涉及的關(guān)注點(diǎn)的清晰分離,通過(guò)消除寫入爭(zhēng)用、最小化讀取爭(zhēng)用和確保代碼與現(xiàn)代處理器使用的緩存工作良好,我們已經(jīng)創(chuàng)建了一種高效的機(jī)制來(lái)在任何應(yīng)用程序之間交換數(shù)據(jù)。
允許消費(fèi)者在沒(méi)有任何爭(zhēng)用的情況下處理?xiàng)l目達(dá)到給定閾值的批處理效應(yīng),在高性能系統(tǒng)中引入了一個(gè)新特性。對(duì)于大多數(shù)系統(tǒng),當(dāng)負(fù)載和爭(zhēng)用增加時(shí),延遲指數(shù)增加,即典型的“J”曲線。隨著Disruptor上的負(fù)載增加,延遲保持幾乎平坦,直到內(nèi)存子系統(tǒng)發(fā)生飽和。
我們相信Disruptor建立了高性能計(jì)算的新基準(zhǔn),并且非常適合繼續(xù)利用當(dāng)前處理器和計(jì)算機(jī)設(shè)計(jì)的趨勢(shì)。
引用:
1 Staged Event Driven Architecture – http://www.eecs.harvard.edu/~mdw/proj/seda/
2 Actor model – http://dspace.mit.edu/handle/1721.1/6952
3 Java Memory Model - http://www.ibm.com/developerworks/library/j-jtp02244/index.html
4 Phasers -
http://gee.cs.oswego.edu/dl/jsr166/dist/jsr166ydocs/jsr166y/Phaser.html
5 Value Types - http://blogs.oracle.com/jrose/entry/tuples_in_the_vm
6 Little’s Law - http://en.wikipedia.org/wiki/Little%27s_law
7 ArrayBlockingQueue - http://download.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/ArrayBlockingQueue.html
8 ConcurrentLinkedQueue -
http://download.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/ConcurrentLinkedQueue.html