Storm、Log4j2高性能之—Disruptor隊(duì)列
1. Disruptor簡(jiǎn)介
Disruptor(https://github.com/LMAX-Exchange/disruptor)是英國(guó)外匯交易公司LMAX開(kāi)發(fā)的一個(gè)高性能隊(duì)列钉蒲,研發(fā)的初衷是解決內(nèi)存隊(duì)列的延遲問(wèn)題(在性能測(cè)試中發(fā)現(xiàn)竟然與I/O操作處于同樣的數(shù)量級(jí))怠益。基于Disruptor開(kāi)發(fā)的系統(tǒng)單線程能支撐每秒600萬(wàn)訂單饥脑,2010年在QCon演講后乳附,獲得了業(yè)界關(guān)注。2011年伴澄,企業(yè)應(yīng)用軟件專家Martin Fowler專門撰寫長(zhǎng)文介紹赋除。同年它還獲得了Oracle官方的Duke大獎(jiǎng)。
像Storm非凌、Log4j 2在內(nèi)的很多知名項(xiàng)目都應(yīng)用了Disruptor以獲取高性能举农。
需要特別說(shuō)明的是這里所說(shuō)的隊(duì)列是系統(tǒng)內(nèi)部的內(nèi)存隊(duì)列,其內(nèi)部為大數(shù)數(shù)組實(shí)現(xiàn)的環(huán)形隊(duì)列(*RingBuffer*)清焕,而不是Kafka這樣的分布式隊(duì)列并蝗。
既然是Storm、Log4j2等將其作為內(nèi)存隊(duì)列使用秸妥,那么自然會(huì)有這幾個(gè)疑問(wèn):
(1)為什么不直接使用java語(yǔ)言的內(nèi)置隊(duì)列呢?
(2)相比Java的內(nèi)置隊(duì)列沃粗,Disruptor的優(yōu)勢(shì)在哪粥惧?
(3)Disruptor是怎么樣比java內(nèi)置隊(duì)列擁有優(yōu)勢(shì)的呢?
接下來(lái)我們將主要從Disruptor本身的實(shí)現(xiàn)策略的關(guān)鍵點(diǎn)及Java內(nèi)置隊(duì)列的實(shí)現(xiàn)進(jìn)行比較總結(jié)最盅,而不過(guò)多注重代碼層面的解讀突雪。
2. Java內(nèi)置隊(duì)列
首先我們需要看一下Java的內(nèi)置隊(duì)列起惕,如下所示。
隊(duì)列 | 有界性 | 鎖 | 數(shù)據(jù)結(jié)構(gòu) |
---|---|---|---|
ArrayBlockingQueue | bounded | 加鎖 | arraylist |
LinkedBlockingQueue | optionally-bounded | 加鎖 | linkedlist |
ConcurrentLinkedQueue | unbounded | 無(wú)鎖 | linkedlist |
LinkedTransferQueue | unbounded | 無(wú)鎖 | linkedlist |
PriorityBlockingQueue | unbounded | 加鎖 | heap |
DelayQueue | unbounded | 加鎖 | heap |
2.1 Java內(nèi)置隊(duì)列的數(shù)據(jù)結(jié)構(gòu)
一般可以總結(jié)為三類:數(shù)組咏删、鏈表和堆惹想。其中,堆一般情況下是為了實(shí)現(xiàn)帶有優(yōu)先級(jí)特性的隊(duì)列,此博文中督函,我們只關(guān)心無(wú)優(yōu)先級(jí)的隊(duì)列操作嘀粱,所以此處不做過(guò)多贅述挺尿,僅從數(shù)組和鏈表兩種數(shù)據(jù)結(jié)構(gòu)來(lái)看:基于數(shù)組線程安全的隊(duì)列麻蹋,其中最典型的就是ArrayBlockingQueue样刷,它主要通過(guò)加鎖的方式來(lái)保證線程安全亿傅;基于鏈表的線程安全隊(duì)列分成LinkedBlockingQueue和ConcurrentLinkedQueue兩大類限寞,前者也通過(guò)鎖的方式來(lái)實(shí)現(xiàn)線程安全皆愉,而后者以及上面表格中的LinkedTransferQueue都是通過(guò)原子變量compare and swap(以下簡(jiǎn)稱“CAS”)這種不加鎖的方式來(lái)實(shí)現(xiàn)的蝉绷。通過(guò)不加鎖的方式實(shí)現(xiàn)的隊(duì)列都是無(wú)界的(無(wú)法保證隊(duì)列的長(zhǎng)度在確定的范圍內(nèi))徽职;而加鎖的方式叫倍,可以實(shí)現(xiàn)有界隊(duì)列偷卧。在穩(wěn)定性要求特別高的系統(tǒng)中,為了防止生產(chǎn)者速度過(guò)快吆倦,導(dǎo)致內(nèi)存溢出涯冠,只能選擇有界隊(duì)列;同時(shí)逼庞,為了減少Java的垃圾回收對(duì)系統(tǒng)性能的影響蛇更,會(huì)盡量選擇array/heap格式的數(shù)據(jù)結(jié)構(gòu)。這樣篩選下來(lái)赛糟,符合條件的隊(duì)列就只有ArrayBlockingQueue派任。ArrayBlockingQueue在實(shí)際使用過(guò)程中,會(huì)因?yàn)榧渔i和同現(xiàn)代CPU更底層設(shè)計(jì)而相悖的編碼理論產(chǎn)生的偽共享等出現(xiàn)嚴(yán)重的性能問(wèn)題璧南。
2.2加鎖
現(xiàn)實(shí)編程過(guò)程中掌逛,加鎖通常會(huì)嚴(yán)重地影響性能。線程會(huì)因?yàn)楦?jìng)爭(zhēng)不到鎖而被掛起司倚,等鎖被釋放的時(shí)候豆混,線程又會(huì)被恢復(fù),這個(gè)過(guò)程中存在著很大的開(kāi)銷动知,并且通常會(huì)有較長(zhǎng)時(shí)間的中斷皿伺,因?yàn)楫?dāng)一個(gè)線程正在等待鎖時(shí),它不能做任何其他事情盒粮。如果一個(gè)線程在持有鎖的情況下被延遲執(zhí)行鸵鸥,例如發(fā)生了缺頁(yè)錯(cuò)誤、調(diào)度延遲或者其它類似情況,那么所有需要這個(gè)鎖的線程都無(wú)法執(zhí)行下去妒穴。如果被阻塞線程的優(yōu)先級(jí)較高宋税,而持有鎖的線程優(yōu)先級(jí)較低,就會(huì)發(fā)生優(yōu)先級(jí)反轉(zhuǎn)讼油,所以相比之下鎖的壞處杰赛,不僅局限于性能的開(kāi)銷,更加危險(xiǎn)的是安全問(wèn)題的隱患! Disruptor論文中講述了一個(gè)實(shí)驗(yàn): 這個(gè)測(cè)試程序調(diào)用了一個(gè)函數(shù)矮台,該函數(shù)會(huì)對(duì)一個(gè)64位的計(jì)數(shù)器循環(huán)自增5億次乏屯。 機(jī)器環(huán)境:2.4G 6核 運(yùn)算:64位的計(jì)數(shù)器累加5億次
Method | Time (ms) |
---|---|
Single thread | 300 |
Single thread with CAS | 5,700 |
Single thread with lock | 10,000 |
Single thread with volatile write | 4,700 |
Two threads with CAS | 30,000 |
Two threads with lock | 224,000 |
CAS操作比單線程無(wú)鎖慢了1個(gè)數(shù)量級(jí);有鎖且多線程并發(fā)的情況下嘿架,速度比單線程無(wú)鎖慢3個(gè)數(shù)量級(jí)瓶珊。可見(jiàn)無(wú)鎖速度最快耸彪。 單線程情況下伞芹,不加鎖的性能 > CAS操作的性能 > 加鎖的性能。 在多線程情況下蝉娜,為了保證線程安全唱较,必須使用CAS或鎖,這種情況下召川,CAS的性能超過(guò)鎖的性能南缓,前者大約是后者的8倍。 綜上可知荧呐,加鎖的性能相對(duì)來(lái)說(shuō)是最差的汉形。
3. 鎖和CAS及內(nèi)存屏障與線程安全
保證線程安全一般分成兩種方式:鎖和原子變量。
3.1 鎖
圖1 通過(guò)加鎖的方式實(shí)現(xiàn)線程安全
采取加鎖的方式倍阐,即是最無(wú)奈的處理方式(悲觀鎖)概疆,默認(rèn)線程會(huì)沖突,訪問(wèn)數(shù)據(jù)時(shí)峰搪,先加上鎖再訪問(wèn)岔冀,訪問(wèn)之后再解鎖。通過(guò)鎖界定一個(gè)臨界區(qū)概耻,同時(shí)只有一個(gè)線程進(jìn)入使套。如上圖所示,Thread2訪問(wèn)Entry的時(shí)候鞠柄,加了鎖侦高,Thread1就不能再執(zhí)行訪問(wèn)Entry的代碼,從而保證線程安全春锋。
ArrayBlockingQueue通過(guò)加鎖的方式實(shí)現(xiàn)的offer方法矫膨,保證線程安全。
3.2 原子變量
原子變量能夠保證原子性的操作期奔,意思是某個(gè)任務(wù)在執(zhí)行過(guò)程中侧馅,要么全部成功,要么全部失敗回滾呐萌,恢復(fù)到執(zhí)行之前的初態(tài)馁痴,不存在初態(tài)和成功之間的中間狀態(tài)。例如CAS操作肺孤,要么比較并交換成功罗晕,要么比較并交換失敗。由CPU保證原子性赠堵。
通過(guò)原子變量可以實(shí)現(xiàn)線程安全小渊。執(zhí)行某個(gè)任務(wù)的時(shí)候,先假定不會(huì)有沖突茫叭,若不發(fā)生沖突酬屉,則直接執(zhí)行成功;當(dāng)發(fā)生沖突的時(shí)候揍愁,則執(zhí)行失敗呐萨,回滾再重新操作,直到不發(fā)生沖突莽囤。
圖2 通過(guò)原子變量CAS實(shí)現(xiàn)線程安全
如圖所示谬擦,Thread1和Thread2都要把Entry加1。若不加鎖朽缎,也不使用CAS惨远,有可能Thread1取到了myValue=1,Thread2也取到了myValue=1话肖,然后相加北秽,Entry中的value值為2。這與預(yù)期不相符狼牺,我們預(yù)期的是Entry的值經(jīng)過(guò)兩次相加后等于3羡儿。
CAS會(huì)先把Entry現(xiàn)在的value跟線程當(dāng)初讀出的值相比較,若相同是钥,則賦值掠归;若不相同,則賦值執(zhí)行失敗悄泥。一般會(huì)通過(guò)while/for循環(huán)來(lái)重新執(zhí)行虏冻,直到賦值成功。
值得重點(diǎn)提及的是CAS和MemoryBarrier一樣弹囚,是CPU級(jí)別的指令厨相,后者的實(shí)現(xiàn)是java中對(duì)應(yīng)的謎一樣的volatile關(guān)鍵字。
在高度競(jìng)爭(zhēng)的情況下,鎖的性能將超過(guò)原子變量的性能蛮穿,但是更真實(shí)的競(jìng)爭(zhēng)情況下庶骄,原子變量的性能將超過(guò)鎖的性能。更加值得祝賀的是與此同時(shí)原子變量不會(huì)有死鎖等災(zāi)難性問(wèn)題践磅。
3.3 內(nèi)存屏障
它是一個(gè)CPU指令单刁。沒(méi)錯(cuò),又一次府适,我們?cè)谟懻揅PU級(jí)別的東西羔飞,以便獲得我們想要的性能(Martin著名的Mechanical Sympathy理論)¢艽海基本上逻淌,它是這樣一條指令:a)確保一些特定操作執(zhí)行的順序;b)影響一些數(shù)據(jù)的可見(jiàn)性(可能是某些指令執(zhí)行后的結(jié)果)疟暖。
編譯器和CPU可以在保證輸出結(jié)果一樣的情況下對(duì)指令重排序卡儒,使性能得到優(yōu)化。插入一個(gè)內(nèi)存屏障誓篱,相當(dāng)于告訴CPU和編譯器先于這個(gè)命令的必須先執(zhí)行朋贬,后于這個(gè)命令的必須后執(zhí)行。
內(nèi)存屏障另一個(gè)作用是強(qiáng)制更新一次不同CPU的緩存窜骄。例如锦募,一個(gè)寫屏障會(huì)把這個(gè)屏障前寫入的數(shù)據(jù)刷新到緩存,這樣任何試圖讀取該數(shù)據(jù)的線程將得到最新值邻遏,而不用考慮到底是被哪個(gè)cpu核心或者哪顆CPU執(zhí)行的糠亩。
內(nèi)存屏障和Java有什么關(guān)系?
現(xiàn)在我知道你在想什么——這不是匯編程序准验。它是Java赎线。
這里有個(gè)神奇咒語(yǔ)叫volatile(我覺(jué)得這個(gè)詞在Java規(guī)范中從未被解釋清楚)。如果你的字段是volatile糊饱,Java內(nèi)存模型將在寫操作后插入一個(gè)寫屏障指令垂寥,在讀操作前插入一個(gè)讀屏障指令。
這意味著如果你對(duì)一個(gè)volatile字段進(jìn)行寫操作另锋,你必須知道:
1滞项、一旦你完成寫入,任何訪問(wèn)這個(gè)字段的線程將會(huì)得到最新的值夭坪。
2文判、在你寫入前,會(huì)保證所有之前發(fā)生的事已經(jīng)發(fā)生室梅,并且任何更新過(guò)的數(shù)據(jù)值也是可見(jiàn)的戏仓,因?yàn)閮?nèi)存屏障會(huì)把之前的寫入值都刷新到緩存疚宇。
內(nèi)存屏障對(duì)性能的影響
內(nèi)存屏障作為另一個(gè)CPU級(jí)的指令,沒(méi)有鎖那樣大的開(kāi)銷赏殃。內(nèi)核并沒(méi)有在多個(gè)線程間干涉和調(diào)度敷待。但凡事都是有代價(jià)的。內(nèi)存屏障的確是有開(kāi)銷的——編譯器/cpu不能重排序指令嗓奢,導(dǎo)致不可以盡可能地高效利用CPU讼撒,另外刷新緩存亦會(huì)有開(kāi)銷浑厚。所以不要以為用volatile代替鎖操作就一點(diǎn)事都沒(méi)股耽。正所謂沒(méi)有最好的,只有最適合的钳幅!相比之下內(nèi)存屏障屏蔽掉一些指令重排序優(yōu)化帶來(lái)的開(kāi)銷代價(jià)遠(yuǎn)遠(yuǎn)小于鎖而已!
4. 偽共享
4.1 什么是共享
下圖是計(jì)算的基本結(jié)構(gòu)物蝙。L1、L2敢艰、L3分別表示一級(jí)緩存诬乞、二級(jí)緩存、三級(jí)緩存钠导,越靠近CPU的緩存震嫉,速度越快,容量也越小牡属。所以L1緩存很小但很快票堵,并且緊靠著在使用它的CPU內(nèi)核;L2大一些逮栅,也慢一些悴势,并且仍然只能被一個(gè)單獨(dú)的CPU核使用;L3更大措伐、更慢特纤,并且被單個(gè)插槽上的所有CPU核共享;最后是主存侥加,由全部插槽上的所有CPU核共享捧存。
圖3 計(jì)算機(jī)CPU與緩存示意圖
當(dāng)CPU執(zhí)行運(yùn)算的時(shí)候,它先去L1查找所需的數(shù)據(jù)担败、再去L2昔穴、然后是L3,如果最后這些緩存中都沒(méi)有氢架,所需的數(shù)據(jù)就要去主內(nèi)存拿傻咖。走得越遠(yuǎn),運(yùn)算耗費(fèi)的時(shí)間就越長(zhǎng)岖研。所以如果你在做一些很頻繁的事卿操,你要盡量確保數(shù)據(jù)在L1緩存中警检。
另外,線程之間共享一份數(shù)據(jù)的時(shí)候害淤,需要一個(gè)線程把數(shù)據(jù)寫回主存扇雕,而另一個(gè)線程訪問(wèn)主存中相應(yīng)的數(shù)據(jù)。
下面是從CPU訪問(wèn)不同層級(jí)數(shù)據(jù)的時(shí)間概念:
從CPU到 | 大約需要的CPU周期 | 大約需要的時(shí)間 |
---|---|---|
主存 | 約60-80ns | |
QPI 總線傳輸(between sockets, not drawn) | 約20ns | |
L3 cache | 約40-45 cycles | 約15ns |
L2 cache | 約10 cycles | 約3ns |
L1 cache | 約3-4 cycles | 約1ns |
寄存器 | 1 cycle |
可見(jiàn)CPU讀取主存中的數(shù)據(jù)會(huì)比從L1中讀取慢了近2個(gè)數(shù)量級(jí)窥摄。
4.2 緩存行
Cache是由很多個(gè)cache line組成的镶奉。每個(gè)cache line通常是64字節(jié),并且它有效地引用主內(nèi)存中的一塊兒地址崭放。一個(gè)Java的long類型變量是8字節(jié)哨苛,因此在一個(gè)緩存行中可以存8個(gè)long類型的變量。
CPU每次從主存中拉取數(shù)據(jù)時(shí)币砂,會(huì)把相鄰的數(shù)據(jù)也存入同一個(gè)cache line建峭。
在訪問(wèn)一個(gè)long數(shù)組的時(shí)候,如果數(shù)組中的一個(gè)值被加載到緩存中决摧,它會(huì)自動(dòng)加載另外7個(gè)亿蒸。因此你能非常快的遍歷這個(gè)數(shù)組掌桩。事實(shí)上边锁,你可以非常快速的遍歷在連續(xù)內(nèi)存塊中分配的任意數(shù)據(jù)結(jié)構(gòu)波岛。
4.3 偽共享及其解決手段
ArrayBlockingQueue有三個(gè)成員變量:- takeIndex:需要被取走的元素下標(biāo) - putIndex:可被元素插入的位置的下標(biāo) - count:隊(duì)列中元素的數(shù)量
這三個(gè)變量很容易放到一個(gè)緩存行中茅坛,但是之間修改沒(méi)有太多的關(guān)聯(lián)。所以每次修改盆色,都會(huì)使之前緩存的數(shù)據(jù)失效灰蛙,從而不能完全達(dá)到共享的效果。
圖4 ArrayBlockingQueue偽共享示意圖
如上圖所示隔躲,當(dāng)生產(chǎn)者線程put一個(gè)元素到ArrayBlockingQueue時(shí)摩梧,putIndex會(huì)修改,從而導(dǎo)致消費(fèi)者線程的緩存中的緩存行無(wú)效宣旱,需要從主存中重新讀取仅父。
這種無(wú)法充分使用緩存行特性的現(xiàn)象,稱為偽共享浑吟,這里我們可以理解為最小緩存的內(nèi)容由于某種原因或者cache大小的對(duì)齊方式導(dǎo)致的同主存中變化不一致而需要重新刷新CacheLine的情況笙纤。
對(duì)于偽共享,一般的解決方案是组力,增大數(shù)組元素的間隔使得由不同線程存取的元素位于不同的緩存行上省容,以空間換時(shí)間(兜兜轉(zhuǎn)轉(zhuǎn),終究要回到最初的起點(diǎn)OS和計(jì)算機(jī)組成原理的層面來(lái)優(yōu)化代碼可以提升的空間燎字,即文中提到的以空間換時(shí)間)腥椒。
以下為對(duì)應(yīng)的Disruptor實(shí)現(xiàn)的部分填充代碼截取阿宅。
package com.lmax.disruptor;
import sun.misc.Unsafe;
import com.lmax.disruptor.util.Util;
class LhsPadding
{
protected long p1, p2, p3, p4, p5, p6, p7;
}
class Value extends LhsPadding
{
protected volatile long value;
}
class RhsPadding extends Value
{
protected long p9, p10, p11, p12, p13, p14, p15;
}
/**
* <p>Concurrent sequence class used for tracking the progress of
* the ring buffer and event processors. Support a number
* of concurrent operations including CAS and order writes.
*
* <p>Also attempts to be more efficient with regards to false
* sharing by adding padding around the volatile field.
*/
public class Sequence extends RhsPadding...
值得特別說(shuō)明的是,早在jdk1.8中笼蛛,有專門的注解@Contended來(lái)避免偽共享洒放,更優(yōu)雅地解決問(wèn)題。
5. Disruptor的加速措施
Disruptor通過(guò)以下設(shè)計(jì)來(lái)解決隊(duì)列速度慢的問(wèn)題:
-
環(huán)形數(shù)組結(jié)構(gòu)
為了避免垃圾回收滨砍,采用數(shù)組而非鏈表往湿。同時(shí),數(shù)組對(duì)處理器的緩存機(jī)制更加友好惋戏。
元素位置定位领追,數(shù)組長(zhǎng)度2^n,通過(guò)位運(yùn)算日川,相取模運(yùn)算加快定位的速度蔓腐。下標(biāo)采取遞增的形式。不用擔(dān)心index溢出的問(wèn)題龄句。 index是long類型,即使100萬(wàn)QPS的處理速度散罕,也需要30萬(wàn)年才能用完分歇。
與此同時(shí),只需要 維護(hù)一個(gè)隊(duì)尾指針即可欧漱。減少了隊(duì)頭隊(duì)尾在多N生產(chǎn)者和N消費(fèi)者條件下的競(jìng)爭(zhēng)問(wèn)題职抡。
-
競(jìng)爭(zhēng)點(diǎn)的分離和合并策略
代碼層面的Strategy和RingBuffer的維護(hù)中可以較為明顯的體現(xiàn)出來(lái),此處不做展開(kāi)误甚。
競(jìng)爭(zhēng)點(diǎn)采用cacheline填充及細(xì)粒度的Java volatile和CAS
無(wú)鎖設(shè)計(jì)
每個(gè)生產(chǎn)者或者消費(fèi)者線程缚甩,會(huì)先申請(qǐng)可以操作的元素在數(shù)組中的位置,申請(qǐng)到之后窑邦,直接在該位置寫入或者讀取數(shù)據(jù)擅威。
下面忽略數(shù)組的環(huán)形結(jié)構(gòu),介紹一下如何實(shí)現(xiàn)無(wú)鎖設(shè)計(jì)冈钦。整個(gè)過(guò)程通過(guò)原子變量CAS郊丛,保證操作的線程安全,不要讓 Ring 重疊;如何通知消費(fèi)者瞧筛;生產(chǎn)者一端的批處理厉熟;以及多個(gè)生產(chǎn)者如何協(xié)同工作。
5.1 生產(chǎn)者及ProducerBarriers組件
Disruptor 代碼 給 消費(fèi)者 提供了一些接口和輔助類较幌,但是沒(méi)有給寫入 Ring Buffer 的 生產(chǎn)者 提供接口揍瑟。這是因?yàn)槌四阈枰郎a(chǎn)者之外,沒(méi)有別人需要訪問(wèn)它乍炉。盡管如此绢片,Ring Buffer 還是與消費(fèi)端一樣提供了一個(gè) ProducerBarrier 對(duì)象嘁字,讓生產(chǎn)者通過(guò)它來(lái)寫入 Ring Buffer。
寫入 Ring Buffer 的過(guò)程涉及到兩階段提交 (two-phase commit)杉畜。首先纪蜒,你的生產(chǎn)者需要申請(qǐng) buffer 里的下一個(gè)節(jié)點(diǎn)。然后此叠,當(dāng)生產(chǎn)者向節(jié)點(diǎn)寫完數(shù)據(jù)纯续,它將會(huì)調(diào)用 ProducerBarrier 的 commit 方法。
那么讓我們首先來(lái)看看第一步灭袁♀恚“給我 Ring Buffer 里的下一個(gè)節(jié)點(diǎn)”,這句話聽(tīng)起來(lái)很簡(jiǎn)單茸歧。的確倦炒,從生產(chǎn)者角度來(lái)看它很簡(jiǎn)單:簡(jiǎn)單地調(diào)用 ProducerBarrier 的 nextEntry() 方法,這樣會(huì)返回給你一個(gè) Entry 對(duì)象软瞎,這個(gè)對(duì)象就是 Ring Buffer 的下一個(gè)節(jié)點(diǎn)逢唤。
5.1.2 ProducerBarrier 如何防止 Ring Buffer 重疊
在后臺(tái),由 ProducerBarrier 負(fù)責(zé)所有的交互細(xì)節(jié)來(lái)從 Ring Buffer 中找到下一個(gè)節(jié)點(diǎn)涤浇,然后才允許生產(chǎn)者向它寫入數(shù)據(jù)鳖藕。
在這幅圖中,我們假設(shè)只有一個(gè)生產(chǎn)者寫入 Ring Buffer只锭。過(guò)一會(huì)兒我們?cè)偬幚矶鄠€(gè)生產(chǎn)者的復(fù)雜問(wèn)題著恩。
ConsumerTrackingProducerBarrier 對(duì)象擁有所有正在訪問(wèn) Ring Buffer 的 消費(fèi)者 列表。這看起來(lái)有點(diǎn)兒奇怪-我從沒(méi)有期望 ProducerBarrier 了解任何有關(guān)消費(fèi)端那邊的事情蜻展。但是等等喉誊,這是有原因的。因?yàn)槲覀儾幌肱c隊(duì)列“混為一談”(隊(duì)列需要追蹤隊(duì)列的頭和尾纵顾,它們有時(shí)候會(huì)指向相同的位置)伍茄,Disruptor 由消費(fèi)者負(fù)責(zé)通知它們處理到了哪個(gè)序列號(hào),而不是 Ring Buffer片挂。所以幻林,如果我們想確定我們沒(méi)有讓 Ring Buffer 重疊,需要檢查所有的消費(fèi)者們都讀到了哪里音念。
在上圖中沪饺,有一個(gè) 消費(fèi)者 順利的讀到了最大序號(hào) 12(用紅色/粉色高亮)。第二個(gè)消費(fèi)者 有點(diǎn)兒落后——可能它在做 I/O 操作之類的——它停在序號(hào) 3闷愤。因此消費(fèi)者 2 在趕上消費(fèi)者 1 之前要跑完整個(gè) Ring Buffer 一圈的距離整葡。
現(xiàn)在生產(chǎn)者想要寫入 Ring Buffer 中序號(hào) 3 占據(jù)的節(jié)點(diǎn),因?yàn)樗?Ring Buffer 當(dāng)前游標(biāo)的下一個(gè)節(jié)點(diǎn)讥脐。但是 ProducerBarrier 明白現(xiàn)在不能寫入遭居,因?yàn)橛幸粋€(gè)消費(fèi)者正在占用它啼器。所以,ProducerBarrier 停下來(lái)自旋 (spins)俱萍,等待端壳,直到那個(gè)消費(fèi)者離開(kāi)。
5.1.3 申請(qǐng)下一個(gè)節(jié)點(diǎn)
現(xiàn)在可以想像消費(fèi)者 2 已經(jīng)處理完了一批節(jié)點(diǎn)枪蘑,并且向前移動(dòng)了它的序號(hào)损谦。可能它挪到了序號(hào) 9(因?yàn)橄M(fèi)端的批處理方式岳颇,現(xiàn)實(shí)中我會(huì)預(yù)計(jì)它到達(dá) 12照捡,但那樣的話這個(gè)例子就不夠有趣了)。
上圖顯示了當(dāng)消費(fèi)者 2 挪動(dòng)到序號(hào) 9 時(shí)發(fā)生的情況话侧。在這張圖中我已經(jīng)忽略了ConsumerBarrier栗精,因?yàn)樗鼪](méi)有參與這個(gè)場(chǎng)景。
ProducerBarier 會(huì)看到下一個(gè)節(jié)點(diǎn)——序號(hào) 3 那個(gè)已經(jīng)可以用了瞻鹏。它會(huì)搶占這個(gè)節(jié)點(diǎn)上的 Entry(我還沒(méi)有特別介紹 Entry 對(duì)象悲立,基本上它是一個(gè)放寫入到某個(gè)序號(hào)的 Ring Buffer 數(shù)據(jù)的桶),把下一個(gè)序號(hào)(13)更新成 Entry 的序號(hào)乙漓,然后把 Entry 返回給生產(chǎn)者级历。生產(chǎn)者可以接著往 Entry 里寫入數(shù)據(jù)。
5.1.4 提交新的數(shù)據(jù)
兩階段提交的第二步是——對(duì)叭披,提交。
綠色表示最近寫入的 Entry玩讳,序號(hào)是 13 涩蜘。
當(dāng)生產(chǎn)者結(jié)束向 Entry 寫入數(shù)據(jù)后,它會(huì)要求 ProducerBarrier 提交熏纯。
ProducerBarrier 先等待 Ring Buffer 的游標(biāo)追上當(dāng)前的位置(對(duì)于單生產(chǎn)者這毫無(wú)意義-比如同诫,我們已經(jīng)知道游標(biāo)到了 12 ,而且沒(méi)有其他人正在寫入 Ring Buffer)樟澜。然后 ProducerBarrier 更新 Ring Buffer 的游標(biāo)到剛才寫入的 Entry 序號(hào)-在我們這兒是 13误窖。接下來(lái),ProducerBarrier 會(huì)讓消費(fèi)者知道 buffer 中有新東西了秩贰。它戳一下 ConsumerBarrier 上的 WaitStrategy 對(duì)象說(shuō)-“有事情發(fā)生了霹俺!”(注意-不同的 WaitStrategy 實(shí)現(xiàn)以不同的方式來(lái)實(shí)現(xiàn)提醒,取決于它是否采用阻塞模式毒费。)
現(xiàn)在消費(fèi)者 1 可以讀 Entry 13 的數(shù)據(jù)丙唧,消費(fèi)者 2 可以讀 Entry 13 以及前面的所有數(shù)據(jù),然后它們都過(guò)得很 happy觅玻。
5.1.5 ProducerBarrier 上的批處理
有趣的是 Disruptor 可以同時(shí)在生產(chǎn)者和 消費(fèi)者 兩端實(shí)現(xiàn)批處理想际。還記得伴隨著程序運(yùn)行培漏,消費(fèi)者 2 最后達(dá)到了序號(hào) 9 嗎?ProducerBarrier 可以在這里做一件很狡猾的事-它知道 Ring Buffer 的大小胡本,也知道最慢的消費(fèi)者位置牌柄。因此它能夠發(fā)現(xiàn)當(dāng)前有哪些節(jié)點(diǎn)是可用的。
如果 ProducerBarrier 知道 Ring Buffer 的游標(biāo)指向 12侧甫,而最慢的消費(fèi)者在 9 的位置珊佣,它就可以讓生產(chǎn)者寫入節(jié)點(diǎn) 3,4闺骚,5彩扔,6,7 和 8僻爽,中間不需要再次檢查消費(fèi)者的位置虫碉。
5.1.6 多個(gè)生產(chǎn)者的場(chǎng)景
其實(shí)還有一些細(xì)節(jié)。
ProducerBarrier 拿到的序號(hào)直接來(lái)自 Ring Buffer 的游標(biāo)胸梆。然而敦捧,如果你看過(guò)代碼的話,你會(huì)發(fā)現(xiàn)它是通過(guò) ClaimStrategy 獲取的碰镜。我省略這個(gè)對(duì)象是為了簡(jiǎn)化示意圖兢卵,在單個(gè)生產(chǎn)者的情況下它不是很重要。
在多個(gè)生產(chǎn)者的場(chǎng)景下绪颖,你還需要其他東西來(lái)追蹤序號(hào)秽荤。這個(gè)序號(hào)是指當(dāng)前可寫入的序號(hào)。注意這和“向 Ring Buffer 的游標(biāo)加 1”不一樣-如果你有一個(gè)以上的生產(chǎn)者同時(shí)在向 Ring Buffer 寫入柠横,就有可能出現(xiàn)某些 Entry 正在被生產(chǎn)者寫入但還沒(méi)有提交的情況窃款。
讓我們復(fù)習(xí)一下如何申請(qǐng)寫入節(jié)點(diǎn)。每個(gè)生產(chǎn)者都向 ClaimStrategy 申請(qǐng)下一個(gè)可用的節(jié)點(diǎn)牍氛。生產(chǎn)者 1 拿到序號(hào) 13晨继,這和上面單個(gè)生產(chǎn)者的情況一樣。生產(chǎn)者 2 拿到序號(hào) 14搬俊,盡管 Ring Buffer的當(dāng)前游標(biāo)僅僅指向 12紊扬。這是因?yàn)?ClaimSequence 不但負(fù)責(zé)分發(fā)序號(hào),而且負(fù)責(zé)跟蹤哪些序號(hào)已經(jīng)被分配唉擂。
現(xiàn)在每個(gè)生產(chǎn)者都擁有自己的寫入節(jié)點(diǎn)和一個(gè)嶄新的序號(hào)餐屎。
我把生產(chǎn)者 1 和它的寫入節(jié)點(diǎn)涂上綠色,把生產(chǎn)者 2 和它的寫入節(jié)點(diǎn)涂上粉色楔敌。
現(xiàn)在假設(shè)生產(chǎn)者 1 還生活在童話里啤挎,因?yàn)槟承┰驔](méi)有來(lái)得及提交數(shù)據(jù)。生產(chǎn)者 2 已經(jīng)準(zhǔn)備好提交了,并且向 ProducerBarrier 發(fā)出了請(qǐng)求庆聘。
就像我們先前在 commit 示意圖中看到的一樣胜臊,ProducerBarrier 只有在 Ring Buffer 游標(biāo)到達(dá)準(zhǔn)備提交的節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)時(shí)它才會(huì)提交。在當(dāng)前情況下伙判,游標(biāo)必須先到達(dá)序號(hào) 13 我們才能提交節(jié)點(diǎn) 14 的數(shù)據(jù)象对。但是我們不能這樣做,因?yàn)樯a(chǎn)者 1 正盯著一些閃閃發(fā)光的東西宴抚,還沒(méi)來(lái)得及提交勒魔。因此 ClaimStrategy 就停在那兒自旋 (spins), 直到 Ring Buffer 游標(biāo)到達(dá)它應(yīng)該在的位置菇曲。
現(xiàn)在生產(chǎn)者 1 從迷糊中清醒過(guò)來(lái)并且申請(qǐng)?zhí)峤还?jié)點(diǎn) 13 的數(shù)據(jù)(生產(chǎn)者 1 發(fā)出的綠色箭頭代表這個(gè)請(qǐng)求)冠绢。ProducerBarrier 讓 ClaimStrategy 先等待 Ring Buffer 的游標(biāo)到達(dá)序號(hào) 12,當(dāng)然現(xiàn)在已經(jīng)到了常潮。因此 Ring Buffer 移動(dòng)游標(biāo)到 13弟胀,讓 ProducerBarrier 戳一下 WaitStrategy 告訴所有人都知道 Ring Buffer 有更新了。現(xiàn)在 ProducerBarrier 可以完成生產(chǎn)者 2 的請(qǐng)求喊式,讓 Ring Buffer 移動(dòng)游標(biāo)到 14孵户,并且通知所有人都知道。
你會(huì)看到岔留,盡管生產(chǎn)者在不同的時(shí)間完成數(shù)據(jù)寫入夏哭,但是 Ring Buffer 的內(nèi)容順序總是會(huì)遵循 nextEntry() 的初始調(diào)用順序。也就是說(shuō)献联,如果一個(gè)生產(chǎn)者在寫入 Ring Buffer 的時(shí)候暫停了竖配,只有當(dāng)它解除暫停后,其他等待中的提交才會(huì)立即執(zhí)行里逆。
5.2 消費(fèi)者流程
消費(fèi)者的流程與生產(chǎn)者非常類似械念,這兒就不多描述了。
5.3生產(chǎn)者的等待策略
暫時(shí)只有休眠1ns运悲。
LockSupport.parkNanos(1);
5.4 消費(fèi)者的等待策略
名稱 | 措施 | 適用場(chǎng)景 |
---|---|---|
BlockingWaitStrategy | 加鎖 | CPU資源緊缺,吞吐量和延遲并不重要的場(chǎng)景 |
BusySpinWaitStrategy | 自旋 | 通過(guò)不斷重試项钮,減少切換線程導(dǎo)致的系統(tǒng)調(diào)用班眯,而降低延遲。推薦在線程綁定到固定的CPU的場(chǎng)景下使用 |
PhasedBackoffWaitStrategy | 自旋 + yield + 自定義策略 | CPU資源緊缺烁巫,吞吐量和延遲并不重要的場(chǎng)景 |
SleepingWaitStrategy | 自旋 + yield + sleep | 性能和CPU資源之間有很好的折中署隘。延遲不均勻 |
TimeoutBlockingWaitStrategy | 加鎖,有超時(shí)限制 | CPU資源緊缺亚隙,吞吐量和延遲并不重要的場(chǎng)景 |
YieldingWaitStrategy | 自旋 + yield + 自旋 | 性能和CPU資源之間有很好的折中磁餐。延遲比較均勻 |
5.5 Log4j2應(yīng)用效果
Log4j 2相對(duì)于Log4j 1最大的優(yōu)勢(shì)在于多線程并發(fā)場(chǎng)景下性能更優(yōu)。該特性源自于Log4j 2的異步模式采用了Disruptor來(lái)處理。在Log4j 2的配置文件中可以配置WaitStrategy诊霹,默認(rèn)是Timeout策略羞延。下面是Log4j 2中對(duì)WaitStrategy的配置官方文檔:
System Property | Default Value | Description |
---|---|---|
AsyncLogger.WaitStrategy | Timeout | Valid values: Block, Timeout, Sleep, Yield. Block is a strategy that uses a lock and condition variable for the I/O thread waiting for log events. Block can be used when throughput and low-latency are not as important as CPU resource. Recommended for resource constrained/virtualised environments. Timeout is a variation of the Block strategy that will periodically wake up from the lock condition await() call. This ensures that if a notification is missed somehow the consumer thread is not stuck but will recover with a small latency delay (default 10ms). Sleep is a strategy that initially spins, then uses a Thread.yield(), and eventually parks for the minimum number of nanos the OS and JVM will allow while the I/O thread is waiting for log events. Sleep is a good compromise between performance and CPU resource. This strategy has very low impact on the application thread, in exchange for some additional latency for actually getting the message logged. Yield is a strategy that uses a Thread.yield() for waiting for log events after an initially spinning. Yield is a good compromise between performance and CPU resource, but may use more CPU than Sleep in order to get the message logged to disk sooner. |
性能差異
loggers all async采用的是Disruptor,而Async Appender采用的是ArrayBlockingQueue隊(duì)列脾还。
由圖可見(jiàn)伴箩,單線程情況下,loggers all async與Async Appender吞吐量相差不大鄙漏,但是在64個(gè)線程的時(shí)候嗤谚,loggers all async的吞吐量比Async Appender增加了12倍,是Sync模式的68倍怔蚌。
圖8 Log4j 2各個(gè)模式性能比較
6. 總結(jié)
Disruptor相對(duì)于傳統(tǒng)方式的優(yōu)點(diǎn):
- 沒(méi)有競(jìng)爭(zhēng)=沒(méi)有鎖=非彻剑快。
- 所有訪問(wèn)者都記錄自己的序號(hào)的實(shí)現(xiàn)方式(Sequencer)桦踊,允許多個(gè)生產(chǎn)者與多個(gè)消費(fèi)者共享相同的數(shù)據(jù)結(jié)構(gòu)椅野。
- 在每個(gè)對(duì)象中都能跟蹤序列號(hào)(ring buffer,claim Strategy钞钙,生產(chǎn)者和消費(fèi)者)鳄橘,加上神奇的cache line padding和內(nèi)存屏障對(duì)基本類型的強(qiáng)制修飾,就意味著沒(méi)有為偽共享和非預(yù)期的競(jìng)爭(zhēng)芒炼。
- 適當(dāng)?shù)牧?批量處理和資源競(jìng)爭(zhēng)點(diǎn)的分離瘫怜、合并策略。
吞吐量測(cè)試數(shù)據(jù)(每秒的數(shù)量)如下本刽。
環(huán)境:- CPU:Intel Core i7 860 @ 2.8 GHz without HT - JVM:Java 1.6.0_25 64-bit - OS:Windows 7
ABQ | Disruptor | |
---|---|---|
Unicast: 1P – 1C | 5,339,256 | 25,998,336 |
Pipeline: 1P – 3C | 2,128,918 | 16,806,157 |
Sequencer: 3P – 1C | 5,539,531 | 13,443,268 |
Multicast: 1P – 3C | 1,077,384 | 9,374,871 |
Diamond: 1P – 3C | 2,113,941 | 16,163,613 |
環(huán)境:- CPU:Intel Core i7-2720QM - JVM:Java 1.6.0_25 64-bit - OS:Ubuntu 11.04
ABQ | Disruptor | |
---|---|---|
Unicast: 1P – 1C | 4,057,453 | 22,381,778 |
Pipeline: 1P – 3C | 2,006,943 | 15,797,913 |
Sequencer: 3P – 1C | 2,054,118 | 14,540,519 |
Multicast: 1P – 3C | 260,433 | 10,860,121 |
Diamond: 1P – 3C | 2,082,124 | 15,235,197 |
依據(jù)并發(fā)競(jìng)爭(zhēng)的激烈程度的不同鲸湃,Disruptor比ArrayBlockingQueue吞吐量快4~8倍。
按照Pipeline: 1P – 3C的連接模式測(cè)試延遲子寓,生產(chǎn)者兩次寫入之間的延遲為1ms暗挑。
Thanks
表格及圖片引用:
- http://brokendreams.iteye.com/blog/2255720
- http://ifeve.com/dissecting-disruptor-whats-so-special/
- https://github.com/LMAX-Exchange/disruptor/wiki/Performance-Results
- https://lmax-exchange.github.io/disruptor/
- https://logging.apache.org/log4j/2.x/manual/async.html
- https://tech.meituan.com/2016/11/18/disruptor.html