業(yè)內(nèi)有公開資料的分布式關(guān)系型數(shù)據(jù)庫和分布式nosql刚夺,存算一體的架構(gòu)大多數(shù)都是基于共識算法或者異步復(fù)制的分布式日志侠姑,搭配rocksdb這類單機存儲引擎構(gòu)建的箩做,這些系統(tǒng)有著類似的架構(gòu)邦邦,但在許多細節(jié)上又不盡相同,甚至許多公司的nosql團隊會同時維護著好幾款架構(gòu)不同的產(chǎn)品鬼店。本文希望給出一個分布式存儲引擎的設(shè)計方案黔龟,能夠涵蓋市面上大多數(shù)分布式日志+單機存儲引擎架構(gòu)的存儲系統(tǒng)需求氏身,創(chuàng)造一個能夠同時支撐分布式kv观谦、表格桨菜、圖甚至關(guān)系型數(shù)據(jù)庫等存儲系統(tǒng)的高性能基座。
分布式存儲引擎仍然是以庫的形式提供泻红,使用方需要在分布式存儲引擎上定義數(shù)據(jù)模型和網(wǎng)絡(luò)協(xié)議等谊路。
為了便于理解缠劝,前兩章的單機存儲引擎部分以rocksdb為例,第四章會講相對于rocksdb的改進秉馏。
數(shù)據(jù)分片不在本文的討論范圍內(nèi)萝究,所以文中的都是以一個分片的多個副本為例锉罐。
共識算法和強一致
基于共識算法和單機存儲引擎實現(xiàn)一個強一致分布式存儲系統(tǒng)的方案已經(jīng)非常成熟了脓规,這里為了給后文做鋪墊簡單敘述一下:每個分片的多個副本中,選出一個master副本升酣,所有寫請求都編碼成操作日志态罪,由master提交复颈,經(jīng)過共識算法達成多數(shù)派耗啦,并且成功寫到master本地存儲引擎后,再向客戶端返回成功衅谷,這樣所有發(fā)到master的讀寫請求就是強一致的了似将。
下面再說一下跨區(qū)的部署模型,示例中的元數(shù)據(jù)部分不屬于分布式存儲引擎堵未,這里為了邏輯完整渗蟹,給出一種簡單的元數(shù)據(jù)管理方案赞辩,另外如TiDB的PD方案原理是一樣的辨嗽。
上圖的跨區(qū)部署架構(gòu)中召庞,etcd集群用來存儲元數(shù)據(jù),并且etcd的副本分布情況要與存儲集群一致忘古,從而保證機房發(fā)生故障時髓堪,etcd的多數(shù)派分布與存儲集群相同娘荡。scheduler是一個無狀態(tài)的調(diào)度組件炮沐,通過etcd搶主,選出一個工作的實例换薄,搶主成功的實例必然與etcd的多數(shù)派連通轻要,從而保證調(diào)度的有效性冲泥。
異步復(fù)制和最終一致-雙模raft
基于共識算法的分布式架構(gòu),能夠提供很高的數(shù)據(jù)安全性幸冻,并且能夠支持強一致,但代價是更高的寫入延遲革半,并且最少要有3副本流码,如果業(yè)務(wù)場景只需要最終一致漫试,并且能夠容忍節(jié)點故障導(dǎo)致丟失少量數(shù)據(jù),那么就可以用異步復(fù)制來代替共識算法外构,從而獲得更低的寫入延遲并可以只部署2副本审编。
下面將給出一種基于raft的方案歧匈。按照raft的設(shè)計件炉,日志只有在commit成功之后斟冕,才能提交到狀態(tài)機,已經(jīng)異步復(fù)制出去但沒commit的日志走净,對狀態(tài)機是不可見的孤里,對這里可以略做修改伏伯,增加一個實時狀態(tài)機,每個成員寫異步日志成功立刻提交到實時狀態(tài)機捌袜,日志commit成功后提交到commit狀態(tài)機说搅,從而支持commit和async兩種訪問模式:
- CommitWrite:原版raft的寫入模式,日志commit并在commit狀態(tài)機生效后才返回成功
- CommitRead:原版raft的讀取模式虏等,從commit狀態(tài)機讀數(shù)據(jù)
- AsyncWrite:raft的leader寫完異步日志后弄唧,立刻提交到本地的實時狀態(tài)機适肠,提交成功立刻返回成功
- AsyncRead:直接讀實時狀態(tài)機,允許讀到?jīng)]commit的數(shù)據(jù)
以下是具體方案:
先不考慮主從切換造成的日志沖突問題候引,把rocksdb作為實時狀態(tài)機,而它的歷史上的snapshot作為commit狀態(tài)機澄干。master每次commit的點不是根據(jù)日志復(fù)制情況動態(tài)決定的逛揩,而是在異步提交階段就先提前選好commit的點,commit點的日志寫入本地rocksdb后麸俘,立刻創(chuàng)建一個snapshot辩稽。當(dāng)commit點復(fù)制到多數(shù)節(jié)點后,本次commit就只到commit點从媚,哪怕commit點之后的內(nèi)容也已經(jīng)復(fù)制到多數(shù)節(jié)點逞泄,仍然放到后續(xù)commit中。commit成功后拜效,就將可讀的commit狀態(tài)機換成最近commit成功的點對應(yīng)的snapshot喷众,這樣從這個snapshot里讀到的數(shù)據(jù)就都是committed。
- 階段(a)紧憾,日志1是commit點侮腹,并且已經(jīng)復(fù)制到多數(shù)派成功commit,所以此時commit讀狀態(tài)機是snapshot_1
- 階段(b)稻励,日志2是普通日志父阻,雖然復(fù)制到多數(shù)派,但最后成功的commit點在它前面望抽,所以它不算commit成功加矛,如果它是CommitWrite寫入的,此時不能返回成功煤篙,但此時在S1或S2執(zhí)行AsyncRead斟览,可以讀到日志2。日志3是commit點辑奈,所以創(chuàng)建snapshot_3苛茂,但此時日志3還沒有在多數(shù)派成功commit,所以commit讀狀態(tài)機還是snapshot_1
- 階段(c)鸠窗,日志3成功復(fù)制到多數(shù)派并commit妓羊,所以snapshot_3替代snapshot_1成為commit讀狀態(tài)機
接下來我們要處理日志沖突問題,原版raft中沒有實時狀態(tài)機稍计,只需要找到最后一條index和term一致的日志躁绸,然后把后面的日志都替換掉就可以了,但在上文的方案中,這些日志都已經(jīng)提交到follower的實時狀態(tài)機了净刮,此時我們還需要將實時狀態(tài)機也回滾剥哑,具體方案如下:關(guān)閉rocksdb的WAL,只用分布式日志淹父,關(guān)閉自動flush株婴,改由外部主動觸發(fā)flush,每次flush前暑认,鎖住寫入困介,將已經(jīng)寫入的日志的index記到rocksdb里,然后flush穷吮,這樣就能保證rocksdb持久化的部分總是能夠精準(zhǔn)對應(yīng)index。leader和follower發(fā)生日志沖突時饥努,找到最后一條一致的日志捡鱼,同時重啟follower的rocksdb,重啟時丟棄memtable酷愧,重啟后從rocksdb記錄的index開始追日志驾诈,如果rocksdb記錄的index比最后一條一致的日志晚,那么就說明錯誤數(shù)據(jù)已經(jīng)持久化溶浴,為了保證最終一致性乍迄,這個follower只能丟掉全部本地數(shù)據(jù),從leader重新全同步一份士败,但這種情況發(fā)生的概率是非常低的闯两。
上述方案能夠同時支持commit和async兩種訪問模式,但為了高可用仍然需要最少3副本谅将,如果業(yè)務(wù)場景只需要async模式漾狼,是否有辦法只部署2副本呢?當(dāng)然是可以的饥臂,市面上有許多主從異步復(fù)制架構(gòu)的系統(tǒng)可以參考逊躁,但為了盡量復(fù)用已有的功能,我們繼續(xù)對前文的raft進行修改隅熙,讓它能夠支持2副本部署的純異步模式稽煤。要支持2副本系統(tǒng)的高可用,就要把“多數(shù)派”相關(guān)的行為全部去掉囚戚,raft和“多數(shù)派”相關(guān)的行為只有commit酵熙、選主和成員變更,前文的async模式完全不依賴commit行為驰坊,commit行為可以直接去掉绿店,剩下的選主和成員變更可做如下處理:純異步模式中,選主和成員信息由外部的中心調(diào)度模塊決定,所有成員強制同調(diào)度模塊指定的leader保持一致假勿,只要調(diào)度模塊最終能夠把leader信息通知到所有成員借嗽,那么系統(tǒng)就能夠達到最終一致。
多點寫入
在跨區(qū)部署的場景中转培,寫入的數(shù)據(jù)可能來自不同的區(qū)恶导,如果寫請求只能發(fā)往leader,那么就會有大量的跨區(qū)寫請求浸须,有些業(yè)務(wù)不希望承受跨區(qū)的延遲惨寿,希望就近寫入成功后立即返回,并且在寫入點能夠立即讀到寫入成功的數(shù)據(jù)删窒,之后存儲系統(tǒng)內(nèi)部自行達成最終一致裂垦。由于多點寫入時并沒有全局同步,所以剛寫完本地的操作是沒有全局的順序的肌索,全局同步時常用全局時鐘+CRDT的方案解決寫沖突蕉拢,但這類方案需要數(shù)據(jù)模型層面保證亂序執(zhí)行仍能達到最終一致,而本文的定位是分布式存儲引擎诚亚,所以要提供一種全局執(zhí)行順序最終一致的方案晕换,從而避免對上層數(shù)據(jù)模型的依賴。
具體方案如下:增加一層并行日志站宗,這部分日志有多串并行的日志闸准,每個寫入點對應(yīng)一串日志,每串日志序號嚴(yán)格依次遞增梢灭。全局有個leader副本夷家,發(fā)到leader的寫入仍然執(zhí)行前文的邏輯。除了leader外的每個寫入點維護一個臨時狀態(tài)機敏释,發(fā)到本寫入點的寫請求瘾英,寫完并行日志中自己的那串日志后,立刻提交到本地臨時狀態(tài)機颂暇,提交成功后返回寫成功缺谴。每個寫入點會將自己的并行日志異步復(fù)制給其它節(jié)點,其它節(jié)點收到新的并行日志后并不會立刻重放這些日志耳鸯,只有l(wèi)eader節(jié)點會主動重放收到的并行日志湿蛔,leader會按照前文的異步寫入流程,將重放的并行日志提交到全局日志中县爬,然后再提交到狀態(tài)機阳啥,其它節(jié)點通過復(fù)制全局日志重放并行日志,這樣就能保證全局執(zhí)行順序的一致了财喳。每個寫入點的臨時狀態(tài)機需要周期性清理察迟,具體過程如下:每個寫入點每過一個周期斩狱,在后臺創(chuàng)建一個新的臨時狀態(tài)機,從全局日志中自己的并行日志編號后開始重放本地自己的那串并行日志扎瓶,重放完成后鎖住寫入所踊,替換掉前臺的臨時狀態(tài)機,這樣已經(jīng)通過全局日志提交到底層狀態(tài)機的數(shù)據(jù)就從臨時狀態(tài)機清理掉了概荷。
上圖的例子中秕岛,S1是leader,通過寫入點S2寫入的數(shù)據(jù)有3條误证,這3條日志的前兩條通過并行日志復(fù)制到了S1继薛,S1把來自S2的2條日志異步提交到了全局日志。S2收到的全局日志中愈捅,提交的并行日志S2的最后一條編號是1遏考,此時重建臨時狀態(tài)機,只需要從S2_2開始重放蓝谨,重放完成后就可以把前臺的臨時狀態(tài)機1-3替換掉灌具。
最終一致的跨區(qū)部署
基于共識算法的強一致模型,一般要求讀主寫主像棘,部署方案已經(jīng)在本文第一章講過稽亏,但在異步模式下壶冒,主調(diào)方只要求最終一致缕题,所以不必把請求發(fā)到主副本,這就要讓客戶端能夠看到所有副本胖腾,并且自己選擇要訪問的副本烟零。在所有網(wǎng)路和節(jié)點都正常的情況下,每個客戶端都能連通所有副本咸作,但當(dāng)發(fā)生機房級網(wǎng)絡(luò)故障時锨阿,不同客戶端所能連通的副本是不同的,但如果每個客戶端各自做連通性檢測记罚,會額外耗費比較多資源墅诡,一種合理的做法是,每個機房部署一個region_scheduler和etcd集群桐智,用來做機房內(nèi)視角的視圖檢測和存儲末早、分發(fā),每個region_scheduler從全局etcd中訂閱集群配置说庭、master等信息然磷,并且以自己的視角檢測所有副本,并將連通性視圖存到同機房的etcd集群中刊驴,機房內(nèi)的客戶端只需要訂閱機房內(nèi)的etcd存儲的集群視圖姿搜,并根據(jù)視圖做路由就可以了寡润。而全局etcd可以根據(jù)機房故障的降級預(yù)案設(shè)定具體的跨區(qū)部署方案,全局etcd用來存儲集群配置舅柜、副本分布梭纹、leader等信息,全局scheduler負責(zé)選主业踢、數(shù)據(jù)遷移等工作栗柒。
分布式flush和compact
rocksdb是一個單機存儲引擎,在存算一體的分布式架構(gòu)中知举,每個存儲節(jié)點都要維護本地的rocksdb實例瞬沦,每個實例各自做flush和compact,這在多副本的場景下相當(dāng)于浪費了大量計算資源做重復(fù)的事情」臀現(xiàn)在單機網(wǎng)卡的性能增長遠遠高于cpu逛钻,故而都能想到,只用一份計算資源做flush和compact锰提,然后通過網(wǎng)絡(luò)將結(jié)果復(fù)制到所有副本∈锒唬現(xiàn)在rocksdb是無法通過接口支持這種方案的,想要實現(xiàn)必然要有侵入式的改造立肘,這里基于上文的分布式日志描繪一種方案边坤,能夠做到文件級別的最終一致。
切imutable_memtable谅年、flush茧痒、compact等會影響文件內(nèi)容的行為,所有元信息都先提交到分布式日志融蹂,這樣flush對應(yīng)的日志范圍旺订、flush和compact涉及的文件名等,都能全局保持一致超燃,然后每個副本根據(jù)自己的角色区拳,決定是自己根據(jù)元信息計算,還是直接去同步結(jié)果文件意乓。leader完成本地的計算后樱调,將結(jié)果提交到分布式日志,觸發(fā)新文件生效届良、垃圾回收笆凌。這里可以做一個進一步的修改,把imutable_memtable歸入L0伙窃,每個imutable_memtable對應(yīng)一個sst菩颖,L0不算持久化,flush行為由每個節(jié)點各自決定時機为障,不做全局同步晦闰,這樣每個節(jié)點能夠更靈活地控制自己的內(nèi)存放祟。
以上就是本文的全部內(nèi)容。