從零開(kāi)始開(kāi)發(fā)一個(gè)單機(jī)存儲(chǔ)引擎
1.VDL Logstore概述
如何設(shè)計(jì)存儲(chǔ)引擎麻敌,使得讀寫接口的性能足夠高,如何保證在機(jī)器宕機(jī)時(shí)掂摔,存儲(chǔ)引擎能夠?qū)⒁汛鎯?chǔ)的數(shù)據(jù)恢復(fù)到一個(gè)一致性狀態(tài)术羔。如何測(cè)試存儲(chǔ)引擎的正確性?本文將著重介紹一下VDL系統(tǒng)的日志存儲(chǔ)引擎--Logstore的架構(gòu)設(shè)計(jì)與核心流程實(shí)現(xiàn)乙漓,及為了保證Logstore的正確性级历,我們做了哪些工作;為了進(jìn)一步提高Logstore的讀寫性能叭披,我們又做了哪些工作寥殖。希望通過(guò)這篇文章,給大家介紹一下設(shè)計(jì)和開(kāi)發(fā)一個(gè)存儲(chǔ)引擎的『前世今生』涩蜘。
1.1 Logstore提供的功能
VDL中有兩種日志形態(tài)嚼贡,一種是raft日志(以下稱為raft log),由raft算法產(chǎn)生和使用皱坛,另一種是用戶形態(tài)的Log(以下稱為user log)编曼,由用戶產(chǎn)生和使用。Logstore作為VDL日志存儲(chǔ)引擎剩辟,同時(shí)存儲(chǔ)著VDL的raft log 和user log掐场。Logstore在設(shè)計(jì)中,將兩種Log形態(tài)組合成一個(gè)Log Entry贩猎。只是通過(guò)不同的頭部信息來(lái)區(qū)分熊户。Logstore需要同時(shí)提供兩種不同形態(tài)的Log操作接口,主要有以下幾類:
- 讀取吭服,根據(jù)索引信息嚷堡,讀取對(duì)應(yīng)的Log。
- 寫入艇棕,將用戶產(chǎn)生的Log蝌戒,封裝成相應(yīng)的user Log和Raft Log寫入到Logstore中。
- 刪除沼琉,刪除用戶不再使用的Log北苟,以文件為粒度,從最開(kāi)始位置往后刪除打瘪。
- 轉(zhuǎn)換友鼻,由Raft Log獲取對(duì)應(yīng)的user Log傻昙。
- 截?cái)啵財(cái)嘁徊糠諰og彩扔,主要是為了支持raft lib中刪除未達(dá)成一致的Log的功能妆档。
2.Logstore的架構(gòu)設(shè)計(jì)
2.1系統(tǒng)架構(gòu)
Logstore由數(shù)據(jù)文件和索引文件組成,同時(shí)Logstore還會(huì)在內(nèi)存中緩存最新的一段Log Entry虫碉,用于Raft lib能夠快速地從內(nèi)存中讀取到最近Raft log贾惦,同時(shí)用戶也能夠快速讀取到最新存儲(chǔ)到Logstore中的user log。Logstore的組成如下圖所示:
- segment: 用于存儲(chǔ)log的文件蔗衡,大小固定(默認(rèn)是512MB)纤虽。Segment文件從前到后代表著log的順序,Logstore通過(guò)追加的方式不斷將Log Entry寫入到segment中绞惦。Logstore只追加Log Entry到最后的Segment文件中,對(duì)于整個(gè)Logstore只有最后一個(gè)segment可讀可寫洋措,其他Segment文件只讀济蝉。由于Segment文件大小固定,我們采用mmap函數(shù)方式對(duì)segment文件進(jìn)行讀寫菠发。
- index: 用于存儲(chǔ)對(duì)應(yīng)的segment中的log entry的元信息王滤,例如:log entry在segment文件中的偏移,raft log index等滓鸠。每個(gè)索引項(xiàng)大小固定雁乡。用于加速查找raft log和user log。
- MemCache: 緩存最后一段log entry數(shù)據(jù)糜俗,保證VDL能夠從內(nèi)存中讀取最新的一段log entry數(shù)據(jù)踱稍。
segment由一條一條的raft log entry組成,raft log的data部分存放的是user log悠抹。每個(gè)segment文件對(duì)應(yīng)一個(gè)index文件珠月,index file由index entry組成,index 文件中的索引項(xiàng)紀(jì)錄了對(duì)應(yīng)raft log的位置和大小等信息楔敌。示意圖如下所示:
3. Logstore的核心流程實(shí)現(xiàn)
3.1 讀數(shù)據(jù)流程
Logstore讀數(shù)據(jù)分為兩種情況:
Read in MemCache,MemCache的元數(shù)據(jù)記錄了緩存的Log范圍信息啤挎,當(dāng)讀取范圍剛好落在MemCache內(nèi)時(shí),則Logstore直接從MemCache中讀取Log并返回卵凑。
Read in Segment,當(dāng)上層讀取的Log范圍未完全落在MemCache中時(shí)庆聘,則會(huì)從segment文件中讀取。Logstore記錄了每個(gè)segment的Log范圍元數(shù)據(jù)信息勺卢,先通過(guò)segment范圍元數(shù)據(jù)信息伙判,定位到讀取的開(kāi)始segment,然后在通過(guò)索引來(lái)定位具體的文件偏移值漫。例如澳腹,讀取raft index 為10010-10019這段范圍的raft log,segment范圍如下圖所示:
根據(jù)segment的Log范圍元數(shù)據(jù)信息织盼,我們可以知道此次讀取范圍開(kāi)始位置和結(jié)束位置都在segment_2中,由于Raft log entry的長(zhǎng)度是不固定的酱塔,如何定位讀取開(kāi)始位置和結(jié)束位置的文件偏移呢沥邻?這時(shí)候就需要用到索引項(xiàng),在Logstore中每個(gè)Log entry對(duì)應(yīng)的索引項(xiàng)大小是固定的羊娃,索引項(xiàng)紀(jì)錄了該raft log entry在segment文件內(nèi)的文件偏移唐全。segment_2對(duì)應(yīng)的index文件第一個(gè)索引項(xiàng)紀(jì)錄的是raft index為10001的raft log entry索引項(xiàng),所以需要在index文件中超找raft log index范圍是:10010-10019蕊玷,就非常簡(jiǎn)單了邮利。直接讀取index 文件的第10到第19范圍的索引項(xiàng),然后根據(jù)索引項(xiàng)內(nèi)的文件偏移到segment上讀取raft log垃帅。大概的流程如下圖所示:
3.2 寫數(shù)據(jù)流程
raft算法要求寫入的raft log必須強(qiáng)制落盤后延届,才能返回成功。通過(guò)將log entry批量異步寫入segment文件贸诚,并調(diào)用sync_file_range函數(shù)強(qiáng)制刷盤方庭。為了提升寫入segment性能,segment文件創(chuàng)建時(shí)就預(yù)分配了512MB的磁盤空間酱固,這種預(yù)分配文件空間的方式有助于提升寫性能械念。將索引信息寫入index文件是異步寫完后就返回。同步寫segment运悲,異步寫index的方式降低了raft log寫耗時(shí)龄减,但又不影響raft算法的正確性。因?yàn)閞aft算法是以segment中的數(shù)據(jù)作為參考標(biāo)準(zhǔn)的班眯。
Logstore寫入流程如下圖所示:
3.3 數(shù)據(jù)恢復(fù)流程
Logstore必須要考慮到在VDL系統(tǒng)異常退出時(shí)希停,存儲(chǔ)的數(shù)據(jù)有可能出現(xiàn)不一致。例如在Logstore寫數(shù)據(jù)過(guò)程中鳖敷,機(jī)器突然宕機(jī)脖苏。這時(shí)候就有可能只寫入了部分?jǐn)?shù)據(jù),在設(shè)計(jì)Logstore時(shí)就必須考慮到如何支持?jǐn)?shù)據(jù)恢復(fù)操作定踱,保證寫入Logstore的數(shù)據(jù)的一致性棍潘。
在Logstore中,只有最后一個(gè)segment文件可能出現(xiàn)數(shù)據(jù)不一致的可能崖媚。因?yàn)長(zhǎng)ogstore在寫滿一個(gè)segment文件后亦歉,會(huì)創(chuàng)建一個(gè)新的segment文件。在創(chuàng)建新的segment文件之前畅哑,Logstore通過(guò)sync系統(tǒng)調(diào)用讓最后的segment對(duì)應(yīng)的index文件內(nèi)容強(qiáng)制刷盤肴楷,并且最后一個(gè)segment文件寫入本身就是同步寫。通過(guò)這種機(jī)制保證了只有最后一個(gè)segment寫入的數(shù)據(jù)存在部分寫的可能荠呐。而在這之前的segment文件和index文件內(nèi)容都是完整的赛蔫。
有了上面的保證砂客,數(shù)據(jù)恢復(fù)我們只需要考慮最后一個(gè)segment及其index文件中的數(shù)據(jù)是否完整。Logstore通過(guò)一個(gè)標(biāo)識(shí)文件來(lái)標(biāo)識(shí)系統(tǒng)是否正常退出呵恢,如果文件存在且里面的標(biāo)記為正常退出鞠值,Logstore就走正常啟動(dòng)流程,否則渗钉,轉(zhuǎn)入數(shù)據(jù)恢復(fù)流程彤恶,Logstore數(shù)據(jù)恢復(fù)流程,主要操作如下圖所示:
4.Logstore的測(cè)試
為保證Logstore的正確性鳄橘,我們對(duì)Logstore對(duì)外提供的接口函數(shù)及內(nèi)部調(diào)用的核心函數(shù)都做了單元測(cè)試声离,通過(guò)gitlab+jenkins持續(xù)集成的方式,保證每次提交都會(huì)觸發(fā)腳本將所有的單元測(cè)試重新運(yùn)行一次瘫怜,如果新增代碼或改動(dòng)代碼术徊,導(dǎo)致單元測(cè)試失敗,我們可以立刻發(fā)現(xiàn)宝磨。通過(guò)這種持續(xù)集成的方式弧关,我們可以保證每次代碼提交的質(zhì)量。
僅僅有單元測(cè)試還是不夠的唤锉,因?yàn)槲覀儫o(wú)法預(yù)測(cè)Logstore某個(gè)接口函數(shù)異常,對(duì)整個(gè)VDL系統(tǒng)造成什么影響别瞭。所以窿祥,我們還對(duì)Logstore進(jìn)行了異常測(cè)試,通過(guò)一個(gè)自研工具FIU蝙寨,對(duì)Logstore中特定的函數(shù)注入各種異常條件晒衩,測(cè)試Logstore的在異常情況下,對(duì)系統(tǒng)的影響墙歪。我們?cè)贚ogstore相關(guān)代碼中插入固定的異常代碼听系,然后通過(guò)FIU來(lái)觸發(fā)相應(yīng)的異常點(diǎn)。這樣就可以讓Logstore走入指定的異常邏輯代碼虹菲。異常注入測(cè)試主要分為兩類:
- 增加讀或?qū)懷舆t靠胜,Logstore向上層提供讀寫raft log和user log等操作。例如毕源,讀取raft log增加3s的延遲浪漠、寫入user log增加1s-3s的隨機(jī)延遲。我們測(cè)試在這類異常場(chǎng)景下霎褐,對(duì)上層VDL會(huì)造成什么影響址愿,結(jié)果是否跟我們的預(yù)期一致。
- 部分寫問(wèn)題冻璃,機(jī)器突然宕機(jī)响谓,有可能導(dǎo)致Logstore部分寫操作损合。也就是segment有可能只寫入了部分?jǐn)?shù)據(jù),或者index文件只寫入了部分?jǐn)?shù)據(jù)娘纷。同樣嫁审,我們也是在寫入segment文件邏輯和index文件邏輯中增加異常點(diǎn),利用FIU觸發(fā)指定的異常邏輯失驶。這樣就可以測(cè)試到在Logstore出現(xiàn)部分寫時(shí)土居,Logstore的數(shù)據(jù)恢復(fù)流程是否能夠正常工作,是否符合預(yù)期嬉探。
有了這類異常測(cè)試擦耀,我們可以提前去模擬線上有可能出現(xiàn)的異常場(chǎng)景,并修復(fù)可能存在的未知缺陷涩堤。保證VDL上線后更加穩(wěn)定眷蜓、可靠。并且添加異常各類異常測(cè)試用例是一個(gè)持續(xù)的過(guò)程胎围,伴隨著VDL系統(tǒng)開(kāi)發(fā)和演進(jìn)的全過(guò)程吁系。
5.Logstore的性能優(yōu)化
為保證Logstore具有高性能的讀寫,在設(shè)計(jì)階段就考慮到了白魂。比如通過(guò)文件空間預(yù)分配來(lái)提升寫性能汽纤,通過(guò)mmap方式讀日志數(shù)據(jù),提升讀性能福荸。在代碼開(kāi)發(fā)完成后蕴坪,結(jié)合go pprof和火焰圖來(lái)定位Logstore的性能開(kāi)銷較大的系統(tǒng)調(diào)用或代碼段,并做相應(yīng)優(yōu)化敬锐。性能優(yōu)化方面的工作背传,比較有意義的幾點(diǎn),可以分享一下:
- 批量寫數(shù)據(jù)台夺,不管是寫segment還是寫index文件径玖,都是將數(shù)據(jù)先組合在一個(gè)內(nèi)存空間中,然后批量寫入到磁盤颤介。減少IO調(diào)用帶來(lái)的開(kāi)銷梳星。
- index文件異步刷盤,在前面的設(shè)計(jì)中买窟,我們談到在segment rolling操作中丰泊,需要將index文件同步刷盤后,再創(chuàng)建新的segment文件始绍。通過(guò)持續(xù)觀察發(fā)現(xiàn)瞳购,每次index文件刷盤都要消耗4ms-8ms的時(shí)間。寫入操作如果需要segment rolling時(shí)亏推,這次的寫入延遲額外會(huì)增加4ms-8ms学赛。Logstore的寫入就會(huì)出現(xiàn)抖動(dòng)年堆。經(jīng)過(guò)分析,我們可以發(fā)現(xiàn)index文件同步刷盤所做的操作就是將index文件對(duì)應(yīng)的內(nèi)存臟頁(yè)更新到磁盤盏浇。如果我們能夠減少segment rolling操作時(shí)index文件對(duì)應(yīng)的內(nèi)存臟頁(yè)數(shù)量变丧。就可以縮短index刷盤的耗時(shí)。我們采用的方式是每次寫index文件時(shí)绢掰,再調(diào)用sync_file_range操作異步將index文件數(shù)據(jù)刷盤痒蓬,這樣就可以分?jǐn)傋詈笠淮嗡⒈P的壓力。經(jīng)過(guò)優(yōu)化后的index文件刷盤操作耗時(shí)縮短到200us-300us滴劲。使得整個(gè)Lostore的寫入耗時(shí)更加平滑攻晒。
在核心函數(shù)調(diào)用中Logstore記錄相關(guān)metric信息,在Logstore上線后班挖,通過(guò)日志收集系統(tǒng)鲁捏,收集metric信息到influxdb,然后通過(guò)grafana展示出來(lái)萧芙。有了grafana的直觀展示给梅,我們可以監(jiān)控到耗時(shí)比較長(zhǎng)的系統(tǒng)調(diào)用,并做針對(duì)性地優(yōu)化双揪。目前關(guān)鍵的讀取和寫入操作都達(dá)到了預(yù)期的性能目標(biāo)动羽。
6.總結(jié)
本文介紹了Logstore在設(shè)計(jì)、開(kāi)發(fā)渔期、測(cè)試和性能優(yōu)化等方面曹质,我們所做的工作。希望能夠給讀者在設(shè)計(jì)和開(kāi)發(fā)分布式存儲(chǔ)系統(tǒng)時(shí)擎场,提供一定的參考思路。在后續(xù)演進(jìn)中几莽,我們希望結(jié)合業(yè)務(wù)場(chǎng)景迅办,對(duì)數(shù)據(jù)做冷熱分離,進(jìn)一步降低生產(chǎn)系統(tǒng)的成本章蚣。到時(shí)候有新的心得體會(huì)站欺,我們繼續(xù)給大家分享。