B站這個(gè)分布式KV存儲(chǔ)設(shè)計(jì),我一鍵三連了
一、背景
在B站的業(yè)務(wù)場(chǎng)景中,存在很多種不同模型的數(shù)據(jù),有些數(shù)據(jù)關(guān)系比較復(fù)雜像:賬號(hào)、稿件信息。有些數(shù)據(jù)關(guān)系比較簡(jiǎn)單,只需要簡(jiǎn)單的kv模型即可滿(mǎn)足。此外,又存在某些讀寫(xiě)吞吐比較高的業(yè)務(wù)場(chǎng)景,該場(chǎng)景早期的解決方案是通過(guò)MySQL來(lái)進(jìn)行數(shù)據(jù)的持久化存儲(chǔ),同時(shí)通過(guò)redis來(lái)提升訪(fǎng)問(wèn)的速度與吞吐。但是這種模式帶來(lái)了兩個(gè)問(wèn)題, 其一是存儲(chǔ)與緩存一致性的問(wèn)題, 該問(wèn)題在B站通過(guò)canal異步更新緩存的方式得以解決, 其二則是開(kāi)發(fā)的復(fù)雜度, 對(duì)于這樣一套存儲(chǔ)系統(tǒng),每個(gè)業(yè)務(wù)都需要額外維護(hù)一個(gè)任務(wù)腳本來(lái)消費(fèi)canal數(shù)據(jù)進(jìn)行緩存數(shù)據(jù)的更新。基于這種場(chǎng)景,業(yè)務(wù)需要的其實(shí)是一個(gè)介于Redis與MySQL之間的提供持久化高性能的kv存儲(chǔ)。此外對(duì)象存儲(chǔ)的元數(shù)據(jù),對(duì)數(shù)據(jù)的一致性、可靠性與擴(kuò)展性有著很高的要求。
基于此背景,我們對(duì)自研KV的定位從一開(kāi)始就是構(gòu)建一個(gè)高可靠、高可用、高性能、高拓展的系統(tǒng)。對(duì)于存儲(chǔ)系統(tǒng),核心是保證數(shù)據(jù)的可靠性,當(dāng)數(shù)據(jù)不可靠時(shí)提供再高的可用性也是沒(méi)用的。可靠性的一個(gè)核心因素就是數(shù)據(jù)的多副本容災(zāi),通過(guò)raft一致性協(xié)議保證多副本數(shù)據(jù)的一致性。
分布式系統(tǒng),如何對(duì)數(shù)據(jù)進(jìn)行分片放置,業(yè)界通常有兩種做法,一是基于hash進(jìn)行分區(qū),二是基于range進(jìn)行分區(qū),兩種方式各有優(yōu)缺點(diǎn)。hash分區(qū),可以有效防止熱點(diǎn)問(wèn)題,但是由于key是hash以后放置的,無(wú)法保證key的全局有序。range分區(qū),由于相鄰的數(shù)據(jù)都放在一起,因此可以保證數(shù)據(jù)的有序,但是同時(shí)也可能帶來(lái)寫(xiě)入熱點(diǎn)的問(wèn)題。基于B站的業(yè)務(wù)場(chǎng)景,我們同時(shí)支持了range分區(qū)和hash分區(qū),業(yè)務(wù)接入的時(shí)候可以根據(jù)業(yè)務(wù)特性進(jìn)行選擇。大部分場(chǎng)景,并不需要全局有序,所以默認(rèn)推薦hash分區(qū)的接入方式,比如觀看記錄、用戶(hù)動(dòng)態(tài)這些場(chǎng)景,只需要保證同一個(gè)用戶(hù)維度的數(shù)據(jù)有序即可,同一個(gè)用戶(hù)維度的數(shù)據(jù)可以通過(guò)hashtag的方式保證局部有序。
二、架構(gòu)設(shè)計(jì)
1、總體架構(gòu)
整個(gè)系統(tǒng)核心分為三個(gè)組件:
Metaserver用戶(hù)集群元信息的管理,包括對(duì)kv節(jié)點(diǎn)的健康監(jiān)測(cè)、故障轉(zhuǎn)移以及負(fù)載均衡。
Node為kv數(shù)據(jù)存儲(chǔ)節(jié)點(diǎn),用于實(shí)際存儲(chǔ)kv數(shù)據(jù),每個(gè)Node上保存數(shù)據(jù)的一個(gè)副本,不同Node之間的分片副本通過(guò)raft保證數(shù)據(jù)的一致性,并選出主節(jié)點(diǎn)對(duì)外提供讀寫(xiě),業(yè)務(wù)也可以根據(jù)對(duì)數(shù)據(jù)一致性的需求指定是否允許讀從節(jié)點(diǎn),在對(duì)數(shù)據(jù)一致性要求不高的場(chǎng)景時(shí),通過(guò)設(shè)置允許讀從節(jié)點(diǎn)可以提高可用性以及降低長(zhǎng)尾。
Client模塊為用戶(hù)訪(fǎng)問(wèn)入口,對(duì)外提供了兩種接入方式,一種是通過(guò)proxy模式的方式進(jìn)行接入,另一種是通過(guò)原生的SDK直接訪(fǎng)問(wèn),proxy本身也是封裝自c++的原生SDK。SDK從Metaserver獲取表的元數(shù)據(jù)分布信息,根據(jù)元數(shù)據(jù)信息決定將用戶(hù)請(qǐng)求具體發(fā)送到哪個(gè)對(duì)應(yīng)的Node節(jié)點(diǎn)。同時(shí)為了保證高可用,SDK還實(shí)現(xiàn)了重試機(jī)制以及backoff請(qǐng)求。
2、集群拓?fù)?/h3>
集群的拓?fù)浣Y(jié)構(gòu)包含了幾個(gè)概念,分別是Pool、Zone、Node、Table、Shard 與Replica。
- Pool 為資源池連通域,包含多個(gè)可用區(qū)。也可用于業(yè)務(wù)資源隔離域。
- Zone 為可用區(qū),同一個(gè)pool內(nèi)部的zone是網(wǎng)路聯(lián)通并且故障隔離的。通常為一個(gè)機(jī)房或者一個(gè)交換機(jī)
- Node 為實(shí)際的物理主機(jī)節(jié)點(diǎn),負(fù)責(zé)具體的數(shù)據(jù)存儲(chǔ)邏輯與數(shù)據(jù)持久化。
- Table 對(duì)應(yīng)到具體的業(yè)務(wù)表,類(lèi)似MySQL里的表。
- Shard 為邏輯分片,通過(guò)將table分為多個(gè)shard將數(shù)據(jù)打散分布。
- Replica 為shard的副本,同一個(gè)shard的不同副本不能分布在同一個(gè)zone,必須保證故障隔離。每一個(gè)replica包含一個(gè)engine,engine存儲(chǔ)全量的業(yè)務(wù)數(shù)據(jù)。engine的實(shí)現(xiàn)包含rocksdb和sparrowdb。其中sparrowdb是針對(duì)大value寫(xiě)放大的優(yōu)化實(shí)現(xiàn)。
三、核心特征
1、分區(qū)分裂
基于不同的業(yè)務(wù)場(chǎng)景,我們同時(shí)支持了range分區(qū)和hash分區(qū)。對(duì)于range場(chǎng)景,隨著用戶(hù)數(shù)據(jù)的增長(zhǎng),需要對(duì)分區(qū)數(shù)據(jù)進(jìn)行分裂遷移。對(duì)于hash分區(qū)的場(chǎng)景,使用上通常會(huì)根據(jù)業(yè)務(wù)的數(shù)據(jù)量做幾倍的冗余預(yù)估,然后創(chuàng)建合適的分片數(shù)。但是即便是幾倍的冗余預(yù)估,由于業(yè)務(wù)發(fā)展速度的不可預(yù)測(cè),也很容易出現(xiàn)實(shí)際使用遠(yuǎn)超預(yù)估的場(chǎng)景,從而導(dǎo)致單個(gè)數(shù)據(jù)分片過(guò)大。
之所以不在一開(kāi)始就創(chuàng)建足夠的分片數(shù)有兩個(gè)原因:其一,由于每一個(gè)replica都包含一個(gè)獨(dú)立的engine,過(guò)多的分片會(huì)導(dǎo)致數(shù)據(jù)文件過(guò)多,同時(shí)對(duì)于批量寫(xiě)入場(chǎng)景存在一定的寫(xiě)扇出放大。其二,每一個(gè)shard都是一組raftgroup,過(guò)多的raft心跳會(huì)對(duì)服務(wù)造成額外的開(kāi)銷(xiāo),這一點(diǎn)后續(xù)我們會(huì)考慮基于節(jié)點(diǎn)做心跳合并優(yōu)化減少集群心跳數(shù)。
為了滿(mǎn)足業(yè)務(wù)的需求場(chǎng)景,我們同時(shí)支持了range和hash兩種模式下的分裂。兩種模式分裂流程類(lèi)似,下面以hash為例進(jìn)行說(shuō)明。
hash模式下的分裂為直接根據(jù)當(dāng)前分片數(shù)進(jìn)行倍增。 分裂的流程主要涉及三個(gè)模塊的交互。
1)metaserver
分裂時(shí),metaserver會(huì)根據(jù)當(dāng)前分片數(shù)計(jì)算出目標(biāo)分片數(shù),并且下發(fā)創(chuàng)建replica指令到對(duì)應(yīng)的Node節(jié)點(diǎn),同時(shí)更新shard分布信息,唯一不同的是,處于分裂中的shard狀態(tài)為splitting。該狀態(tài)用于client流量請(qǐng)求路由識(shí)別。當(dāng)Node完成數(shù)據(jù)分裂以后上報(bào)metaserver,metaserver更新shard狀態(tài)為normal從而完成分裂。
2)Node
node收到分裂請(qǐng)求以后,會(huì)根據(jù)需要分裂的分片id在原地拉起創(chuàng)建一個(gè)新的分片。然后對(duì)舊分片的數(shù)據(jù)進(jìn)行checkpoint,同時(shí)記錄舊分片checkpoint對(duì)應(yīng)的logid。新分片創(chuàng)建完成后,會(huì)直接從舊分片的checkpoint進(jìn)行open,然后在異步復(fù)制logid之后的數(shù)據(jù)保證數(shù)據(jù)的一致性。新分片加載完checkpoint后,原來(lái)的舊分片會(huì)向raftgroup提交一條分裂完成日志,該日志處理流程與普通raft日志一致。分裂完成后上報(bào)分裂狀態(tài)到metaserver,同時(shí)舊分片開(kāi)始拒絕不再屬于自己分片的數(shù)據(jù)寫(xiě)入,client收到分片錯(cuò)誤以后會(huì)請(qǐng)求metaserver更新shard分布。
完成分裂以后的兩個(gè)分片擁有的兩倍冗余數(shù)據(jù),這些數(shù)據(jù)會(huì)在engine compaction的時(shí)候根據(jù)compaction_filter過(guò)濾進(jìn)行刪除。
3)Client
用戶(hù)請(qǐng)求時(shí),根據(jù)hash(key) % shard_cnt 獲取目標(biāo)分片。表分裂期間,該shard_cnt表示分裂完成后的最終分片數(shù)。以上圖3分片的分裂為例:
hash(key) = 4, 分裂前shard_cnt為3,因此該請(qǐng)求會(huì)被發(fā)送到shard1. 分裂期間,由于shard_cnt變?yōu)?,因此目標(biāo)分片應(yīng)該是shard4, 但是由于shard4為splitting,因此client會(huì)重新計(jì)算分片從而將請(qǐng)求繼續(xù)發(fā)送給shard1. 等到最終分裂完成后,shard4狀態(tài)變更為Normal,請(qǐng)求才會(huì)被發(fā)送到shard4.
分裂期間,如果Node返回分片信息錯(cuò)誤,那么client會(huì)請(qǐng)求metaserver更新分片分布信息。
2、binlog支持
類(lèi)似于MySQL的binlog,我們基于raftlog日志實(shí)現(xiàn)了kv的binlog. 業(yè)務(wù)可以根據(jù)binlog進(jìn)行實(shí)時(shí)的事件流訂閱,同時(shí)為了滿(mǎn)足事件流回溯的需求,我們還對(duì)binlog數(shù)據(jù)進(jìn)行冷備。通過(guò)將binlog冷備到對(duì)象存儲(chǔ),滿(mǎn)足了部分場(chǎng)景需要回溯較長(zhǎng)事件記錄的需求。
直接復(fù)用raftlog作為用戶(hù)行為的binlog,可以減少binlog產(chǎn)生的額外寫(xiě)放大,唯一需要處理的是過(guò)濾raft本身的配置變更信息。 learner通過(guò)實(shí)時(shí)監(jiān)聽(tīng)不斷拉取分片產(chǎn)生的binlog到本地并解析。 根據(jù)learner配置信息決定將數(shù)據(jù)同步到對(duì)應(yīng)的下游。 同時(shí)binlog數(shù)據(jù)還會(huì)被異步備份到對(duì)象存儲(chǔ),當(dāng)業(yè)務(wù)需要回溯較長(zhǎng)時(shí)間的事件流的時(shí)候,可以直接指定位置從S3拉取歷史binlog進(jìn)行解析。
3、多活
基于上述提到的binlog能力,我們還基于此實(shí)現(xiàn)了kv的多活。learner模塊會(huì)實(shí)時(shí)將用戶(hù)寫(xiě)入的數(shù)據(jù)同步到跨數(shù)據(jù)中心的其他kv集群。對(duì)于跨數(shù)據(jù)中心部署的業(yè)務(wù),業(yè)務(wù)可以選擇就近的kv集群進(jìn)行讀取訪(fǎng)問(wèn),降低訪(fǎng)問(wèn)延時(shí)。
kv的多活分為讀多活和寫(xiě)多活。對(duì)于讀多活,機(jī)房A的寫(xiě)入會(huì)被異步復(fù)制到機(jī)房B,機(jī)房B的服務(wù)可以直接讀取本機(jī)房的數(shù)據(jù),該模式下只有機(jī)房A的kv可以寫(xiě)入。對(duì)于寫(xiě)多活,kv在機(jī)房A B 都能同時(shí)提供寫(xiě)入并且進(jìn)行雙向同步,但是為了保證數(shù)據(jù)的一致性,需要業(yè)務(wù)上做數(shù)據(jù)的單元化寫(xiě)入,保證兩個(gè)機(jī)房不會(huì)同時(shí)修改同一條記錄。通過(guò)將用戶(hù)劃分單元,提供了寫(xiě)多活的能力。通過(guò)對(duì)binlog數(shù)據(jù)打標(biāo),解決了雙向同步時(shí)候的數(shù)據(jù)回環(huán)問(wèn)題。
4、bulk load
對(duì)于用戶(hù)畫(huà)像和特征引擎等場(chǎng)景,需要將離線(xiàn)生成的大量數(shù)據(jù)快速導(dǎo)入KV存儲(chǔ)系統(tǒng)提供用戶(hù)讀取訪(fǎng)問(wèn)。傳統(tǒng)的寫(xiě)入方式是根據(jù)生成的數(shù)據(jù)記錄一條條寫(xiě)入kv存儲(chǔ),這樣帶來(lái)兩個(gè)問(wèn)題。其一,大批量寫(xiě)入會(huì)對(duì)kv造成額外的負(fù)載與寫(xiě)入帶寬放大造成浪費(fèi)。其次,由于寫(xiě)入量巨大,每次導(dǎo)入需要花費(fèi)較長(zhǎng)的時(shí)間。為了減少寫(xiě)入放大以及導(dǎo)入提速,我們支持了bulk load的能力。離線(xiàn)平臺(tái)只需要根據(jù)kv的存儲(chǔ)格式離線(xiàn)生成對(duì)應(yīng)的SST文件,然后上傳到對(duì)象存儲(chǔ)服務(wù)。kv直接從對(duì)象存儲(chǔ)拉取SST文件到本地,然后直接加載SST文件即可對(duì)外提供讀服務(wù)。bulk load的另外一個(gè)好處是可以直接在生成SST后離線(xiàn)進(jìn)行compaction,將compaction的負(fù)載offload到離線(xiàn)的同時(shí)也降低了空間的放大。
5、kv存儲(chǔ)分離
由于LSM tree的寫(xiě)入特性,數(shù)據(jù)需要被不斷的compaction到更底層的level。在compaction時(shí),如果該key還有效,那么會(huì)被寫(xiě)入到更底層的level里,如果該key已經(jīng)被刪除,那么會(huì)判斷當(dāng)前l(fā)evel是否是最底層的,一條被刪除的key,會(huì)被標(biāo)記為刪除,直到被compaction到最底層level的時(shí)候才會(huì)被真正刪除。compaction的時(shí)候會(huì)帶來(lái)額外的寫(xiě)放大,尤其當(dāng)value比較大的時(shí)候,會(huì)造成巨大的帶寬浪費(fèi)。為了降低寫(xiě)放大,我們參考了Bitcask實(shí)現(xiàn)了kv分離的存儲(chǔ)引擎sparrowdb.
1)sparrowdb 介紹
用 戶(hù)寫(xiě)入的時(shí)候,value通過(guò)append only的方式寫(xiě)入data文件,然后更新索引信息,索引的value包含實(shí)際數(shù)據(jù)所在的data文件id,value大小以及position信息,同時(shí)data文件也會(huì)包含索引信息。 與原始的bitcask實(shí)現(xiàn)不一樣的是,我們將索引信息保存在 rocksdb。
更新寫(xiě)入的時(shí)候,只需要更新對(duì)應(yīng)的索引即可。compaction的時(shí)候,只需將索引寫(xiě)入底層的level,而無(wú)需進(jìn)行data的拷貝寫(xiě)入。對(duì)于已經(jīng)失效的data,通過(guò)后臺(tái)線(xiàn)程進(jìn)行檢查,當(dāng)發(fā)現(xiàn)data文件里的索引與rocksdb保存的索引不一致的時(shí)候,說(shuō)明該data已經(jīng)被刪除或更新,數(shù)據(jù)可以被回收淘汰。
使用kv存儲(chǔ)分離降低了寫(xiě)放大的問(wèn)題,但是由于kv分離存儲(chǔ),會(huì)導(dǎo)致讀的時(shí)候多了一次io,讀請(qǐng)求需要先根據(jù)key讀到索引信息,再根據(jù)索引信息去對(duì)應(yīng)的文件讀取data數(shù)據(jù)。為了降低讀訪(fǎng)問(wèn)的開(kāi)銷(xiāo),我們針對(duì)value比較小的數(shù)據(jù)進(jìn)行了inline,只有當(dāng)value超過(guò)一定閾值的時(shí)候才會(huì)被分離存儲(chǔ)到data文件。通過(guò)inline以及kv分離獲取讀性能與寫(xiě)放大之間的平衡。
6、負(fù)載均衡
在分布式系統(tǒng)中,負(fù)載均衡是繞不過(guò)去的問(wèn)題。一個(gè)好的負(fù)載均衡策略可以防止機(jī)器資源的空閑浪費(fèi)。同時(shí)通過(guò)負(fù)載均衡,可以防止流量?jī)A斜導(dǎo)致部分節(jié)點(diǎn)負(fù)載過(guò)高從而影響請(qǐng)求質(zhì)量。對(duì)于存儲(chǔ)系統(tǒng),負(fù)載均衡不僅涉及到磁盤(pán)的空間,也涉及到機(jī)器的內(nèi)存、cpu、磁盤(pán)io等。同時(shí)由于使用raft進(jìn)行主從選主,保證主節(jié)點(diǎn)盡可能的打散也是均衡需要考慮的問(wèn)題。
1)副本均衡
由于設(shè)計(jì)上我們會(huì)盡量保證每個(gè)副本的大小盡量相等,因此對(duì)于空間的負(fù)載其實(shí)可以等價(jià)為每塊磁盤(pán)的副本數(shù)。創(chuàng)建副本時(shí),會(huì)從可用的zone中尋找包含副本數(shù)最少的節(jié)點(diǎn)進(jìn)行創(chuàng)建。同時(shí)考慮到不同業(yè)務(wù)類(lèi)型的副本讀寫(xiě)吞吐可能不一樣導(dǎo)致CPU負(fù)載不一致,在挑選副本的時(shí)候會(huì)進(jìn)一步檢查當(dāng)前節(jié)點(diǎn)的負(fù)載情況,如果當(dāng)前節(jié)點(diǎn)負(fù)載超過(guò)閾值,則跳過(guò)該節(jié)點(diǎn)繼續(xù)選擇其他合適的節(jié)點(diǎn)。目前基于最少副本數(shù)以及負(fù)載校驗(yàn)基本可以做到集群內(nèi)部的節(jié)點(diǎn)負(fù)載均衡。
當(dāng)出現(xiàn)負(fù)載傾斜時(shí),則從負(fù)載較高的節(jié)點(diǎn)選擇副本進(jìn)行遷出,從集群中尋找負(fù)載最低的節(jié)點(diǎn)作為待遷入節(jié)點(diǎn)。當(dāng)出現(xiàn)節(jié)點(diǎn)故障下線(xiàn)以及新機(jī)器資源加入的時(shí)候,也是基于均值計(jì)算待遷出以及遷入節(jié)點(diǎn)進(jìn)行均衡。
2)主從均衡
雖然通過(guò)最少副本數(shù)策略保證了節(jié)點(diǎn)副本數(shù)的均衡,但是由于raft選主的性質(zhì),可能出現(xiàn)主節(jié)點(diǎn)都集中在部分少數(shù)節(jié)點(diǎn)的情況。由于只有主節(jié)點(diǎn)對(duì)外提供寫(xiě)入,主節(jié)點(diǎn)的傾斜也會(huì)導(dǎo)致負(fù)載的不均衡。為了保證主節(jié)點(diǎn)的均衡,Node節(jié)點(diǎn)會(huì)定期向metaserver上報(bào)當(dāng)前節(jié)點(diǎn)上副本的主從信息。
主從均衡基于表維度進(jìn)行操作。metaserver會(huì)根據(jù)表在Node的分布信息進(jìn)行副本數(shù)的計(jì)算。主副本的數(shù)量基于最樸素簡(jiǎn)單的數(shù)學(xué)期望進(jìn)行計(jì)算: 主副本期望值 = 節(jié)點(diǎn)副本數(shù) / 分片副本數(shù)。下面為一個(gè)簡(jiǎn)單的例子:
假設(shè)表a包含10個(gè)shard,每個(gè)shard 3個(gè)replica。在節(jié)點(diǎn)A、B、C、D的分布為 10、5、6、9. 那么A、B、C、D的主副本數(shù)期望值應(yīng)該為 3、1、2、3. 如果節(jié)點(diǎn)數(shù)實(shí)際的主副本數(shù)少于期望值,那么被放入待遷入?yún)^(qū),如果大于期望值,那么被放入待遷出區(qū)。同時(shí)通過(guò)添加誤差值來(lái)避免頻繁的遷入遷出。只要節(jié)點(diǎn)的實(shí)際主副本數(shù)處于 [x-δx,x+δx] 則表示主副本數(shù)處于穩(wěn)定期間,x、δx 分別表示期望值和誤差值。
需要注意的是,當(dāng)對(duì)raft進(jìn)行主從切換的時(shí)候,從節(jié)點(diǎn)需要追上所有已提交的日志以后才能成功選為主,如果有節(jié)點(diǎn)落后的時(shí)候進(jìn)行主從切換,那么可能導(dǎo)致由于追數(shù)據(jù)產(chǎn)生的一段時(shí)間無(wú)主的情況。因此在做主從切換的時(shí)候必須要檢查主從的日志復(fù)制狀態(tài),當(dāng)存在慢節(jié)點(diǎn)的時(shí)候禁止進(jìn)行切換。
7、故障檢測(cè)&修復(fù)
一個(gè)小概率的事件,隨著規(guī)模的變大,也會(huì)變成大概率的事件。分布式系統(tǒng)下,隨著集群規(guī)模的變大,機(jī)器的故障將變得愈發(fā)頻繁。因此如何對(duì)故障進(jìn)行自動(dòng)檢測(cè)容災(zāi)修復(fù)也是分布式系統(tǒng)的核心問(wèn)題。故障的容災(zāi)主要通過(guò)多副本raft來(lái)保證,那么如何進(jìn)行故障的自動(dòng)發(fā)現(xiàn)與修復(fù)呢。
1)健康監(jiān)測(cè)
metaserver會(huì)定期向node節(jié)點(diǎn)發(fā)送心跳檢查node的健康狀態(tài),如果node出現(xiàn)故障不可達(dá),那么metaserver會(huì)將node標(biāo)記為故障狀態(tài)并剔除,同時(shí)將node上原來(lái)的replica遷移到其他健康的節(jié)點(diǎn)。
為了防止部分node和metaserver之間部分網(wǎng)絡(luò)隔離的情況下node節(jié)點(diǎn)被誤剔除,我們添加了心跳轉(zhuǎn)發(fā)的功能。上圖中三個(gè)node節(jié)點(diǎn)對(duì)于客戶(hù)端都是正常的,但是node3由于網(wǎng)絡(luò)隔離與metaserver不可達(dá)了,如果metaserver此時(shí)直接剔除node3會(huì)造成節(jié)點(diǎn)無(wú)必要的剔除操作。通過(guò)node2轉(zhuǎn)發(fā)心跳探測(cè)node3的狀態(tài)避免了誤剔除操作。
除了對(duì)節(jié)點(diǎn)的狀態(tài)進(jìn)行檢測(cè)外,node節(jié)點(diǎn)本身還會(huì)檢查磁盤(pán)信息并進(jìn)行上報(bào),當(dāng)出現(xiàn)磁盤(pán)異常時(shí)上報(bào)異常磁盤(pán)信息并進(jìn)行踢盤(pán)。磁盤(pán)的異常主要通過(guò)dmesg日志進(jìn)行采集分析。
2)故障修復(fù)
當(dāng)出現(xiàn)磁盤(pán)節(jié)點(diǎn)故障時(shí),需要將原有故障設(shè)備的replica遷移到其他健康節(jié)點(diǎn),metaserver根據(jù)負(fù)載均衡策略選擇合適的node并創(chuàng)建新replica, 新創(chuàng)建的replica會(huì)被加入原有shard的raft group并從leader復(fù)制快照數(shù)據(jù),復(fù)制完快照以后成功加入raft group完成故障replica的修復(fù)。
故障的修復(fù)主要涉及快照的復(fù)制。每一個(gè)replica會(huì)定期創(chuàng)建快照刪除舊的raftlog,快照信息為完整的rocksdb checkpoint。通過(guò)快照進(jìn)行修復(fù)時(shí),只需要拷貝checkpoint下的所有文件即可。通過(guò)直接拷貝文件可以大幅減少快照修復(fù)的時(shí)間。需要注意的是快照拷貝也需要進(jìn)行io限速,防止文件拷貝影響在線(xiàn)io.
四、實(shí)踐經(jīng)驗(yàn)
1、rocksdb
1)過(guò)期數(shù)據(jù)淘汰
在很多業(yè)務(wù)場(chǎng)景中,業(yè)務(wù)的數(shù)據(jù)只需要存儲(chǔ)一段時(shí)間,過(guò)期后數(shù)據(jù)即可以自動(dòng)刪除清理,為了支持這個(gè)功能,我們通過(guò)在value上添加額外的ttl信息,并在compaction的時(shí)候通過(guò)compaction_filter進(jìn)行過(guò)期數(shù)據(jù)的淘汰。level之間的容量呈指數(shù)增長(zhǎng),因此rocksdb越底層能容納越多的數(shù)據(jù),隨著時(shí)間的推移,很多數(shù)據(jù)都會(huì)被移動(dòng)到底層,但是由于底層的容量比較大,很難觸發(fā)compaction,這就導(dǎo)致很多已經(jīng)過(guò)期的數(shù)據(jù)沒(méi)法被及時(shí)淘汰從而導(dǎo)致了空間放大。與此同時(shí),大量的過(guò)期數(shù)據(jù)也會(huì)對(duì)scan的性能造成影響。這個(gè)問(wèn)題可以通過(guò)設(shè)置periodic_compaction_seconds 來(lái)解決,通過(guò)設(shè)置周期性的compaction來(lái)觸發(fā)過(guò)期數(shù)據(jù)的回收。
2)scan慢查詢(xún)
除了上面提到的存在大批過(guò)期數(shù)據(jù)的時(shí)候可能導(dǎo)致的scan慢查詢(xún),如果業(yè)務(wù)存在大批量的刪除,也可能導(dǎo)致scan的時(shí)候出現(xiàn)慢查詢(xún)。因?yàn)閐elete對(duì)于rocksdb本質(zhì)也是一條append操作,delete寫(xiě)入會(huì)被添加刪除標(biāo)記,只有等到該記錄被compaction移動(dòng)到最底層后該標(biāo)記才會(huì)被真正刪除。帶來(lái)的一個(gè)問(wèn)題是如果用戶(hù)scan的數(shù)據(jù)區(qū)間剛好存在大量的delete標(biāo)記,那么iterator需要迭代過(guò)濾這些標(biāo)記直到找到有效數(shù)據(jù)從而導(dǎo)致慢查詢(xún)。該問(wèn)題可以通過(guò)添加CompactOnDeletionCollector 來(lái)解決。當(dāng)memtable flush或者sst compaction的時(shí)候,collector會(huì)統(tǒng)計(jì)當(dāng)前key被刪除的比例,通過(guò)設(shè)置合理的 deletion_trigger ,當(dāng)發(fā)現(xiàn)被delete的key數(shù)量超過(guò)閾值的時(shí)候主動(dòng)觸發(fā)compaction。
3)delay compaction
通過(guò)設(shè)置 CompactOnDeletionCollector 解決了delete導(dǎo)致的慢查詢(xún)問(wèn)題。但是對(duì)于某些業(yè)務(wù)場(chǎng)景,卻會(huì)到來(lái)嚴(yán)重的寫(xiě)放大。當(dāng)L0被compaction到L1時(shí)候,由于閾值超過(guò)deletion_trigger ,會(huì)導(dǎo)致L1被添加到compaction隊(duì)列,由于業(yè)務(wù)的數(shù)據(jù)特性,L1和L2存在大量重疊的數(shù)據(jù)區(qū)間,導(dǎo)致每次L1的compaction會(huì)同時(shí)帶上大量的L2文件造成巨大的寫(xiě)放大。為了解決這個(gè)問(wèn)題,我們對(duì)這種特性的業(yè)務(wù)數(shù)據(jù)禁用了CompactOnDeletionCollector 。通過(guò)設(shè)置表級(jí)別參數(shù)來(lái)控制表級(jí)別的compaction策略。后續(xù)會(huì)考慮優(yōu)化delete trigger的時(shí)機(jī),通過(guò)只在指定層級(jí)觸發(fā)來(lái)避免大量的io放大。
4)compaction限速
由于rocksdb的compaction會(huì)造成大量的io讀寫(xiě),如果不對(duì)compaction的io進(jìn)行限速,那么很可能影響到在線(xiàn)的寫(xiě)入。但是限速具體配置多少比較合適其實(shí)很難確定,配置大了影響在線(xiàn)業(yè)務(wù),配置小了又會(huì)導(dǎo)致低峰期帶寬浪費(fèi)。基于此rocksdb 在5.9以后為 NewGenericRateLimiter 添加了 auto_tuned 參數(shù),可以根據(jù)當(dāng)前負(fù)載自適應(yīng)調(diào)整限速。需要注意的是,該函數(shù)還有一個(gè)參數(shù) RateLimiter::Mode 用來(lái)限制操作類(lèi)型,默認(rèn)值為 kWritesOnly,通常情況該模式不會(huì)有問(wèn)題,但是如果業(yè)務(wù)存在大量被刪除的數(shù)據(jù),只限制寫(xiě)可能會(huì)導(dǎo)致compaction的時(shí)候造成大量的讀io。
5)關(guān)閉WAL
由于raft log本身已經(jīng)可以保證數(shù)據(jù)的可靠性,因此寫(xiě)入rocksdb的時(shí)候可以關(guān)閉wal減少磁盤(pán)io,節(jié)點(diǎn)重啟的時(shí)候根據(jù)rocksdb里保存的last_apply_id從raft log進(jìn)行狀態(tài)機(jī)回放即可。
2、Raft
1)降副本容災(zāi)
對(duì)于三副本的raft group,單副本故障并不會(huì)影響服務(wù)的可用性,即使是主節(jié)點(diǎn)故障了剩余的兩個(gè)節(jié)點(diǎn)也會(huì)快速選出主并對(duì)外提供讀寫(xiě)服務(wù)。但是考慮到極端情況,假設(shè)同時(shí)出現(xiàn)兩個(gè)副本故障呢?這時(shí)只剩一個(gè)副本無(wú)法完成選主服務(wù)將完全不可用。根據(jù)墨菲定律,可能發(fā)生的一定會(huì)發(fā)生。服務(wù)的可用性一方面是穩(wěn)定提供服務(wù)的能力,另一方面是故障時(shí)快速恢復(fù)的能力。那么假設(shè)出現(xiàn)這種故障的時(shí)候我們應(yīng)該如何快速恢復(fù)服務(wù)的可用呢。
如果通過(guò)創(chuàng)建新的副本進(jìn)行修復(fù),新副本需要等到完成快照拷貝以后才能加入raft group進(jìn)行選舉,期間服務(wù)還是不可用的。那么我們可以通過(guò)強(qiáng)制將分片降為單副本模式,此時(shí)剩余的單個(gè)健康副本可以獨(dú)自完成選主,后續(xù)再通過(guò)變更副本數(shù)的方式進(jìn)行修復(fù)。
2)RaftLog 聚合提交
對(duì)于寫(xiě)入吞吐非常高的場(chǎng)景,可以通過(guò)犧牲一定的延時(shí)來(lái)提升寫(xiě)入吞吐,通過(guò)log聚合來(lái)減少請(qǐng)求放大。對(duì)于SSD盤(pán),每一次寫(xiě)入都是4k刷盤(pán),value比較小的時(shí)候會(huì)造成磁盤(pán)帶寬的浪費(fèi)。我們?cè)O(shè)置了每5ms或者每聚合4k進(jìn)行批量提交。該參數(shù)可以根據(jù)業(yè)務(wù)場(chǎng)景進(jìn)行動(dòng)態(tài)配置修改。
3)異步刷盤(pán)
有些對(duì)于數(shù)據(jù)一致性要求不是非常高的場(chǎng)景,服務(wù)故障的時(shí)候允許部分?jǐn)?shù)據(jù)丟失。對(duì)于該場(chǎng)景,可以關(guān)閉fsync通過(guò)操作系統(tǒng)進(jìn)行異步刷盤(pán)。但是如果寫(xiě)入吞吐非常高導(dǎo)致page cache的大小超過(guò)了 vm.diry_ratio ,那么即便不是fsync也會(huì)導(dǎo)致io等待,該場(chǎng)景往往會(huì)導(dǎo)致io抖動(dòng)。為了避免內(nèi)核pdflush大量刷盤(pán)造成的io抖動(dòng),我們支持對(duì)raftlog進(jìn)行異步刷盤(pán)。
五、未來(lái)探討
- 透明多級(jí)存儲(chǔ),和緩存結(jié)合,自動(dòng)冷熱分離,通過(guò)將冷數(shù)據(jù)自動(dòng)搬遷到kv降低內(nèi)存使用成本。
- 新硬件場(chǎng)景接入,使用SPDK 進(jìn)行IO提速,使用PMEM進(jìn)行訪(fǎng)問(wèn)加速。