BookKeeper 工作原理
參考文檔:
https://medium.com/splunk-maas/apache-bookkeeper-internals-part-1-high-level-6dce62269125
https://medium.com/splunk-maas/apache-bookkeeper-internals-part-2-writes-359ffc17c497
https://medium.com/splunk-maas/apache-bookkeeper-internals-part-3-reads-31637b118bf
https://medium.com/splunk-maas/apache-bookkeeper-internals-part-4-back-pressure-7847bd6d1257
https://mp.weixin.qq.com/s/L0IPBZDEI31mOrvLIwaqAA
BookKeeper是一個(gè)高性能的追加寫(xiě)(append-only)存儲(chǔ)服務(wù),主要用在Pulsar中哄啄,單個(gè)BookKeeper節(jié)點(diǎn)也叫Bookie歇竟。本文試圖來(lái)解釋一些BookKeeper中的架構(gòu)势似。
BookKeeper總體架構(gòu)
BookKeeper是一個(gè)單進(jìn)程服務(wù)低矮,多個(gè)進(jìn)程提供對(duì)等的服務(wù)內(nèi)容婿滓。其總體架構(gòu)如下菜皂,
最上層是Netty燕侠,用來(lái)處理網(wǎng)絡(luò)請(qǐng)求IO棒掠。BookKeeper內(nèi)部主要有2塊Entry處理的服務(wù)孵构,Journal和DbLedgerStorage。Journal是BookKeeper的WAL烟很,DbLedgerStorage是處理Write Cache颈墅,并從后臺(tái)將Entry數(shù)據(jù)刷入到Entry Log文件中。
BookKeeper的線程模型
BookKeeper的線程模型如下圖雾袱,
不同顏色的SyncThread恤筛、DbStorageThread是處理后臺(tái)任務(wù)線程,其他是是處理實(shí)時(shí)任務(wù)的線程芹橡。介紹其中幾個(gè)線程毒坛,
- Netty Threadpool,Netty線程林说,主要處理網(wǎng)絡(luò)IO煎殷,消息解析;
- Long Pool Threadpool腿箩,長(zhǎng)輪詢線程豪直,當(dāng)寫(xiě)入線程發(fā)生相關(guān)寫(xiě)入的時(shí)候,觸發(fā)該線程(沒(méi)有理解這個(gè)線程的具體作用)珠移;
- Write Threadpool弓乙,寫(xiě)線程,處理寫(xiě)入請(qǐng)求的任務(wù)剑梳;
- Read Threadpool唆貌,讀線程,處理讀取請(qǐng)求的任務(wù)垢乙;
- High Priority Threadpool锨咙,高優(yōu)先級(jí)線程,對(duì)請(qǐng)求添加高優(yōu)先級(jí)標(biāo)簽追逮,主要是Pulsar的Fencing和恢復(fù)場(chǎng)景會(huì)用到酪刀,正常情況下不會(huì)用;
值得注意的是钮孵,write, read, long poll 和 high priority這4類(lèi)線程都是OrderedExecutor類(lèi)的實(shí)例骂倘,這些線程組成一個(gè)大的線程組來(lái)提供服務(wù),并與Ledger id綁定巴席。Netty根據(jù)Ledger id來(lái)分發(fā)請(qǐng)求到響應(yīng)的線程組進(jìn)行處理历涝。部分線程的默認(rèn)線程數(shù)量,
- serverNumIOThreads (Netty threads, defaults to 2xCPU threads)
- numAddWorkerThreads (defaults to 1)
- numReadWorkerThreads (defaults to 8)
- numLongPollWorkerThreads (defaults to 0 表示Long Poll讀入消息后就提交給讀線程)
- numHighPriorityWorkerThreads (defaults to 8)
- numJournalCallbackThreads (defaults to 1)
在處理Journal和DbLedger的時(shí)候,線程管理的整體結(jié)構(gòu)荧库,如下圖堰塌,
BookKeeper的存儲(chǔ)是分開(kāi)管理的,Journal和DbLedger存儲(chǔ)在不同的地方分衫,同時(shí)场刑,可以配置多個(gè)Journal和多個(gè)DbLedger(參數(shù)journalDirectories、ledgerDirectories)蚪战,最大可能利用存儲(chǔ)牵现。對(duì)每個(gè)Journal,專(zhuān)有的Journal線程組實(shí)例處理邀桑;對(duì)每個(gè)DbLedger瞎疼,也有專(zhuān)有的SingleDirectoryDbLedgerStorage線程組實(shí)例來(lái)處理,包括Write Cache概漱、Read Cache, Ledger Entry Log File丑慎、RockDB index file等。
例如瓤摧,對(duì)于一個(gè)讀請(qǐng)求竿裂,Netty根據(jù)Ledger id轉(zhuǎn)發(fā)到對(duì)應(yīng)的DbLedger實(shí)例進(jìn)行處理,如下照弥,
對(duì)于一個(gè)寫(xiě)請(qǐng)求腻异,Netty根據(jù)Ledger id轉(zhuǎn)發(fā)到對(duì)應(yīng)的Journal和DbLedger實(shí)例進(jìn)行處理,如下这揣,
BookKeeper寫(xiě)入請(qǐng)求分析
BookKeeper處理寫(xiě)入Entry請(qǐng)求的整體流程如下悔常,
寫(xiě)入線程
首先,Netty線程收到寫(xiě)入請(qǐng)求给赞,解析机打,并把請(qǐng)求轉(zhuǎn)給寫(xiě)入線程。寫(xiě)入線程通常只有1個(gè)片迅,因?yàn)橐瓿傻氖虑楹苌俨醒?xiě)入線程先把Entry寫(xiě)入Write Cache,然后再發(fā)起一個(gè)寫(xiě)入Journal的請(qǐng)求進(jìn)入Entry Log內(nèi)存隊(duì)列柑蛇,就完成了所有工作芥挣。整個(gè)過(guò)程都是內(nèi)存操作,消耗很少耻台,因此不需要很多線程空免。
Journal線程
Journal線程在Entry Log內(nèi)存隊(duì)列的另一端等待,當(dāng)有寫(xiě)入線程寫(xiě)入消息時(shí)盆耽,就把消息中的Entry寫(xiě)入到磁盤(pán)蹋砚。但Journal的寫(xiě)入并不是同步寫(xiě)(fsync)扼菠,因此只保證寫(xiě)入到了系統(tǒng)緩存中,Journal線程本身并不發(fā)起同步寫(xiě)的系統(tǒng)請(qǐng)求都弹。同時(shí)娇豫,Journal線程周期性的發(fā)起強(qiáng)制寫(xiě)入請(qǐng)求,并將請(qǐng)求寫(xiě)入Force Write內(nèi)存隊(duì)列畅厢。強(qiáng)制寫(xiě)入請(qǐng)求的發(fā)起時(shí)機(jī)有以下幾處,
- 達(dá)到預(yù)設(shè)的最大等待時(shí)間(配置journalMaxGroupWaitMSec氮昧,默認(rèn)2 ms)
- 達(dá)到累積寫(xiě)入字節(jié)大小上限(配置journalBufferedWritesThreshold框杜,默認(rèn)512Kb)
- 達(dá)到累積寫(xiě)入Entry條數(shù)上限(配置journalBufferedEntriesThreshold,默認(rèn)0袖肥,不啟用)
- 當(dāng)Entry Log內(nèi)存隊(duì)列消費(fèi)完咪辱,從不空到空(配置journalFlushWhenQueueEmpty,默認(rèn)false)
也就是說(shuō)椎组,默認(rèn)啟用前2個(gè)選項(xiàng)油狂。
注意:Journal線程只有1個(gè),不會(huì)出現(xiàn)多個(gè)線程同時(shí)寫(xiě)一個(gè)Entry Log文件的情況寸癌。
強(qiáng)制寫(xiě)線程
Force Write線程等待Force Write內(nèi)存隊(duì)列的消息专筷,收到請(qǐng)求后,就發(fā)起fsync系統(tǒng)調(diào)用蒸苇,強(qiáng)制將數(shù)據(jù)寫(xiě)入磁盤(pán)Journal文件磷蛹。當(dāng)數(shù)據(jù)持久化完成之后,調(diào)用Journal Callback線程溪烤。
注意:處理強(qiáng)制寫(xiě)請(qǐng)求的時(shí)候味咳,可能有新的Entry寫(xiě)入,實(shí)際刷盤(pán)的時(shí)候檬嘀,刷入的Entry可能比發(fā)起請(qǐng)求時(shí)的Entry要多槽驶。
Journal Callback線程
該線程是回調(diào)線程,發(fā)送響應(yīng)給客戶端鸳兽。寫(xiě)入Journal的步驟到此結(jié)束掂铐。
DbStorage線程
DbStorage線程主要處理,當(dāng)Write Cache寫(xiě)滿之后贸铜,將數(shù)據(jù)寫(xiě)入到Ledger中堡纬,一個(gè)DbLedgerStorage對(duì)應(yīng)一個(gè)DbStorage線程。DbStorage線程是一個(gè)很復(fù)雜的線程蒿秦,不僅要負(fù)責(zé)管理Write Cache烤镐,還需要負(fù)責(zé)Entry存儲(chǔ)寫(xiě)入。
Write Cache在內(nèi)存中有兩份棍鳖,但同時(shí)只有一個(gè)Write Cache是活躍的(Active Write Cache)炮叶,用于實(shí)時(shí)任務(wù)的寫(xiě)入碗旅;另一個(gè)Write Cache是寫(xiě)入存儲(chǔ)時(shí)候用的(Flushed Write Cache)。當(dāng)寫(xiě)入存儲(chǔ)完成的時(shí)候镜悉,就把Flushed Write Cache清空祟辟。當(dāng)寫(xiě)入線程發(fā)現(xiàn)Active Write Cache已滿的時(shí)候,就觸發(fā)DbStorage線程進(jìn)行寫(xiě)入存儲(chǔ)侣肄。如果當(dāng)時(shí)Flushed Write Cache是已經(jīng)清空的旧困,說(shuō)明之前的寫(xiě)入任務(wù)已經(jīng)完成,DbStorage線程交換兩個(gè)Write Cache稼锅,將空的Write Cache變?yōu)锳ctive Write Cache吼具,供寫(xiě)入線程使用;然后矩距,開(kāi)始執(zhí)行寫(xiě)入任務(wù)拗盒,將Flushed Write Cache寫(xiě)入存儲(chǔ)。
這是理想情況锥债,如果Active Write Cache已滿的時(shí)候陡蝇,F(xiàn)lushed Write Cache尚未清空,說(shuō)明之前的寫(xiě)入任務(wù)還沒(méi)有完成哮肚。此時(shí)登夫,不能交換2個(gè)Write Cache,寫(xiě)入線程會(huì)阻擋寫(xiě)入請(qǐng)求一小段時(shí)間绽左,等待寫(xiě)入任務(wù)完成悼嫉。這個(gè)時(shí)間參數(shù)是dbStorage_maxThrottleTimeMs,默認(rèn)10秒拼窥。直到Flushed Write Cache全部寫(xiě)入完成戏蔑,交換2個(gè)Write Cache,寫(xiě)入線程就被釋放鲁纠。
默認(rèn)情況下总棵,Write Cache的大小設(shè)置為可用直接內(nèi)存的25%(應(yīng)該就是機(jī)器內(nèi)存的25%),也可以通過(guò)參數(shù)dbStorage_writeCacheMaxSizeMb來(lái)設(shè)置改含。因?yàn)閃rite Cache有2份情龄,所以實(shí)際的Write Cache大小會(huì)翻倍。假定Write Cache設(shè)置為250MB捍壤,有2個(gè)Ledger目錄骤视,每個(gè)Ledger有2個(gè)Write Cache,總共就有4份Write Cache鹃觉,共計(jì)消耗內(nèi)存1GB专酗。
當(dāng)DbLedgerStorage存儲(chǔ)寫(xiě)入時(shí),會(huì)先按Ledger id和Entry id對(duì)所有Entry進(jìn)行排序盗扇,然后將Entry寫(xiě)入Entry Log文件祷肯,并將文件的偏移量寫(xiě)入Entry索引沉填,即RocksDB。
DbLedgerStorage存儲(chǔ)寫(xiě)入佑笋,不僅可以由DbStorage線程完成翼闹,也可以由Sync線程在產(chǎn)生檢查點(diǎn)時(shí)完成。
注意蒋纬,Entry Log文件中猎荠,每次寫(xiě)入的Entry是來(lái)自于多個(gè)Ledger的,同一個(gè)存儲(chǔ)中有多個(gè)Ledger的數(shù)據(jù)混雜在一起颠锉。經(jīng)過(guò)排序法牲,同一個(gè)Ledger的Entry會(huì)聚合在一起。在讀取的時(shí)候琼掠,當(dāng)前Entry的前后是同一個(gè)Ledger的概率高。如圖停撞,
Sync線程
Sync線程是一個(gè)守護(hù)線程瓷蛙,不在Journal線程池和DbLedgerStorage線程池中,主要負(fù)責(zé)定期生成檢查點(diǎn)(checkpoint)戈毒。檢查點(diǎn)要完成以下任務(wù)艰猬,
- 將Ledger數(shù)據(jù)刷入存儲(chǔ);
- 標(biāo)記刷入磁盤(pán)的Journal位置(日志標(biāo)記)埋市,并持久化冠桃,表示這個(gè)位置之前的Entry都已經(jīng)安全的寫(xiě)入存儲(chǔ)了。這個(gè)過(guò)程是通過(guò)寫(xiě)磁盤(pán)上一個(gè)單獨(dú)的日志標(biāo)記文件來(lái)完成的道宅;
- 清理不在需要的舊Journal文件食听;
寫(xiě)入瓶頸
通常情況下,BookKeeper的瓶頸都是磁盤(pán)IO造成的污茵,但Journal IO瓶頸和DbLedgerStorage IO瓶頸的現(xiàn)象是不一樣的樱报。
如果是Journal的瓶頸,會(huì)發(fā)現(xiàn)寫(xiě)入線程很平穩(wěn)的拒絕某些請(qǐng)求泞当,同時(shí)迹蛤,如果有火焰圖工具的話,可以看到寫(xiě)入線程非常繁忙襟士。如果是DbLedgerStorage的瓶頸盗飒,會(huì)發(fā)現(xiàn)寫(xiě)入線程拒絕所有寫(xiě)入請(qǐng)求,等待10秒(默認(rèn))后就開(kāi)始正常接收請(qǐng)求陋桂,然后發(fā)生擁堵逆趣,又開(kāi)始拒絕請(qǐng)求。
當(dāng)然章喉,也可能是CPU瓶頸汗贫,不太常見(jiàn)身坐,一般情況都是IO是瓶頸。即使發(fā)生了落包,也不難發(fā)現(xiàn)部蛇,CPU占用率很高,直接增加CPU資源處理Netty請(qǐng)求即可咐蝇。
BookKeeper讀取請(qǐng)求分析
BookKeeper的讀取請(qǐng)求涯鲁,主要是由DbLedgerStorage的getEntry(long ledgerId, long entryId)方法完成。架構(gòu)圖如下有序,
讀取的流程如下抹腿,
- 檢查Write Cache中是否有數(shù)據(jù),有則返回旭寿;
- 檢查Read Cache中是否有數(shù)據(jù)警绩,有則返回,無(wú)則說(shuō)明數(shù)據(jù)在磁盤(pán)上盅称;
- 從Entry索引(RocksDB)中獲取到Entry的位置(哪個(gè)文件的哪個(gè)偏移量)肩祥;
- 根據(jù)文件和偏移量,定位特定的Entry缩膝;
- 執(zhí)行預(yù)讀然旌荨;
- 將預(yù)讀取的Entry加載到Read Cache疾层;
- 返回當(dāng)前Entry将饺;
預(yù)讀取的假設(shè)是,讀取了當(dāng)前Entry痛黎,也很可能會(huì)讀取之后的Entry予弧,因此預(yù)先加載這些Entry到內(nèi)存,也因?yàn)閷?xiě)入的時(shí)候舅逸,相同Ledger的數(shù)據(jù)被排序放在了一起桌肴,因此,預(yù)讀取是磁盤(pán)的順序讀琉历,性能較好坠七。預(yù)讀取的邊界是,
- 達(dá)到單次預(yù)讀取數(shù)量上限旗笔,默認(rèn)1000彪置,Pulsar場(chǎng)景;
- 讀到當(dāng)前文件結(jié)束蝇恶;
- 讀到另一個(gè)Ledger的Entry拳魁;
Read Cache每個(gè)DbLedgerStorage上有1個(gè),默認(rèn)是可用直接內(nèi)存的25%撮弧。
關(guān)于讀取的一些其他問(wèn)題
Broker粘滯讀取潘懊,也就是說(shuō)姚糊,Broker如果在某個(gè)Bookie上讀取到了數(shù)據(jù),那么下次統(tǒng)一客戶端的讀取請(qǐng)求還是發(fā)送到同一個(gè)Bookie授舟。同樣是基于鄰近讀取的假設(shè)救恨。如果沒(méi)有粘滯的話,可能每個(gè)Bookie上都需要預(yù)讀取加載相同的數(shù)據(jù)释树。這也說(shuō)明肠槽,Broker在讀取Bookie數(shù)據(jù)的時(shí)候并不是對(duì)等對(duì)待每個(gè)Bookie的。Broker粘滯讀取需要由 Broker 來(lái)完成實(shí)現(xiàn)奢啥。
Read Cache有多個(gè)分段(Segment)秸仙,每個(gè)Segment的數(shù)據(jù)結(jié)構(gòu)是一個(gè)環(huán)形隊(duì)列(Ring Buffer)。內(nèi)存預(yù)先分配桩盲,新的Entry覆蓋舊的Entry寂纪,每個(gè)Cache有索引指向?qū)?yīng)位置,以方便查找赌结。
Read Cache緩存抖動(dòng)弊攘,主要出現(xiàn)在Read Cache大小不足的時(shí)候。假設(shè)Read Cache可以容納2000個(gè)Entry姑曙,讀請(qǐng)求先讀取Ledger A,預(yù)讀取1000個(gè)Entry在Cache中迈倍;又讀取Ledger B伤靠,預(yù)讀取1000個(gè)Entry在Cache中,此時(shí)Read Cache已滿啼染;再讀取Ledger C宴合,預(yù)讀取1000個(gè)Entry在Cache中,覆蓋掉Ledger A的1000個(gè)迹鹅;此時(shí)A又來(lái)讀取下一個(gè)Entry卦洽,緩存無(wú)法命中,繼續(xù)預(yù)讀刃迸铩阀蒂;如果之后,A弟蚀、B蚤霞、C依次讀取,那么一次Cache也無(wú)法命中义钉,性能急劇下降昧绣。造成這樣現(xiàn)象的原因是Read Cache大小不足,所有的緩存類(lèi)應(yīng)用都有類(lèi)似現(xiàn)象捶闸。解決方案有夜畴,增大Read Cache拖刃,或者降低預(yù)讀取的上限(如改為預(yù)讀取500條即可)。