分布式存儲(chǔ)在B站的應(yīng)用實(shí)踐
業(yè)務(wù)高速發(fā)展,B站的存儲(chǔ)系統(tǒng)如何演進(jìn)以支撐指數(shù)增長(zhǎng)的流量洪峰?隨著流量進(jìn)一步暴增,如何設(shè)計(jì)一套穩(wěn)定可靠易拓展的系統(tǒng),來(lái)滿足未來(lái)進(jìn)一步增長(zhǎng)的業(yè)務(wù)訴求?同時(shí),面對(duì)更高的可用性訴求,KV 是如何通過(guò)異地多活為應(yīng)用提供更高的可用性保障?文章的最后,會(huì)介紹一些典型業(yè)務(wù)在KV存儲(chǔ)的應(yīng)用實(shí)踐。
全文將圍繞下面4點(diǎn)展開:
- 存儲(chǔ)演進(jìn)
- 設(shè)計(jì)實(shí)現(xiàn)
- 場(chǎng)景&問(wèn)題
- 總結(jié)思考
01 存儲(chǔ)演進(jìn)
首先介紹一下B站早期的存儲(chǔ)演進(jìn)。
?
針對(duì)不同的場(chǎng)景,早期的KV存儲(chǔ)包括Redix/Memcache,Redis+MySQL,HBASE。
但是隨著B站數(shù)據(jù)量的高速增長(zhǎng),這種存儲(chǔ)選型會(huì)面臨一些問(wèn)題:
- 首先,MySQL是單機(jī)存儲(chǔ),一些場(chǎng)景數(shù)據(jù)量已經(jīng)超過(guò) 10 T,單機(jī)無(wú)法放下。當(dāng)時(shí)也考慮了使用TiDB,TiDB是一種關(guān)系型數(shù)據(jù)庫(kù),對(duì)于播放歷史這種沒(méi)有強(qiáng)關(guān)系的數(shù)據(jù)并不適合。
- 其次,是Redis Cluster的規(guī)模瓶頸,因?yàn)閞edis采用的是Gossip協(xié)議來(lái)通信傳遞信息,集群規(guī)模越大,節(jié)點(diǎn)間的通信開銷越大,并且節(jié)點(diǎn)之間狀態(tài)不一致的存留時(shí)間也會(huì)越長(zhǎng),很難再進(jìn)行橫向擴(kuò)展。
- 另外,HBase存在嚴(yán)重長(zhǎng)尾和緩存內(nèi)存成本高的問(wèn)題。
基于這些問(wèn)題,我們對(duì)KV存儲(chǔ)提出了如下要求:
- 易拓展:100x橫向擴(kuò)容;
- 高性能:低延時(shí),高QPS;
- 高可用:長(zhǎng)尾穩(wěn)定,故障自愈;
- 低成本:對(duì)比緩存;
- 高可靠:數(shù)據(jù)不丟。
02 設(shè)計(jì)實(shí)現(xiàn)
接下來(lái)介紹我們是如何基于上述要求進(jìn)行具體實(shí)現(xiàn)的。
1. 總體架構(gòu)
總體架構(gòu)共分為三個(gè)部分Client,Node,Metaserver。Client是用戶接入端,確定了用戶的接入方式,用戶可以采用SDK的方式進(jìn)行接入。Metaserver主要是存儲(chǔ)表的元數(shù)據(jù)信息,表分為了哪些分片,這些分片位于哪些node之上。用戶在讀寫操作的時(shí)候,只需要put、get方法,無(wú)需關(guān)注分布式實(shí)現(xiàn)的技術(shù)細(xì)節(jié)。Node的核心點(diǎn)就是Replica,每一張表會(huì)有多個(gè)分片,而每個(gè)分片會(huì)有多個(gè)Replica副本,通過(guò)Raft實(shí)現(xiàn)副本之間的同步復(fù)制,保證高可用。
2. 集群拓?fù)?/span>
?
Pool:資源池。根據(jù)不同的業(yè)務(wù)劃分,分為在線資源池和離線資源池。
Zone:可用區(qū)。主要用于故障隔離,保證每個(gè)切片的副本分布在不同的zone。
Node:存儲(chǔ)節(jié)點(diǎn),可包含多個(gè)磁盤,存儲(chǔ)著Replica。
Shard:一張表數(shù)據(jù)量過(guò)大的時(shí)候可以拆分為多個(gè)Shard。拆分策略有Range,Hash。
3. Metaserver
資源管理:主要記錄集群的資源信息,包括有哪些資源池,可用區(qū),多少個(gè)節(jié)點(diǎn)。當(dāng)創(chuàng)建表的時(shí)候,每個(gè)分片都會(huì)記錄這樣的映射關(guān)系。
元數(shù)據(jù)分布:記錄分片位于哪臺(tái)節(jié)點(diǎn)之上。
健康檢查:注冊(cè)所有的node信息,檢查當(dāng)前node是否正常,是否有磁盤損壞?;谶@些信息可以做到故障自愈。
負(fù)載檢測(cè):記錄磁盤使用率,CPU使用率,內(nèi)存使用率。
負(fù)載均衡:設(shè)置閾值,當(dāng)達(dá)到閾值時(shí)會(huì)進(jìn)行數(shù)據(jù)的重新分配。
分裂管理:數(shù)據(jù)量增大時(shí),進(jìn)行橫向擴(kuò)展。
Raft選主:當(dāng)有一個(gè)Metaserver掛掉的時(shí)候,可進(jìn)行故障自愈。
Rocksdb:元數(shù)據(jù)信息持久化存儲(chǔ)。
4. Node
做為存儲(chǔ)模塊,主要包含后臺(tái)線程,RPC接入,抽象引擎層三個(gè)部分
?
① 后臺(tái)線程
Binlog管理,當(dāng)用戶進(jìn)行寫操作的時(shí)候,會(huì)記錄一條binlog日志,當(dāng)發(fā)生故障的時(shí)候可以對(duì)數(shù)據(jù)進(jìn)行恢復(fù)。因?yàn)楸镜氐拇鎯?chǔ)空間有限,所以Binlog管理會(huì)將一些冷數(shù)據(jù)存放在S3,熱門的數(shù)據(jù)存放在本地。數(shù)據(jù)回收功能主要是用來(lái)防止誤刪數(shù)據(jù)。當(dāng)用戶進(jìn)行刪除操作,并不會(huì)真正的把數(shù)據(jù)刪除,通常是設(shè)置一個(gè)時(shí)間,比如一天,一天之后數(shù)據(jù)才會(huì)被回收。如果是誤刪數(shù)據(jù),就可以使用數(shù)據(jù)回收模塊對(duì)數(shù)據(jù)進(jìn)行恢復(fù)。健康檢查會(huì)檢查節(jié)點(diǎn)的健康狀態(tài),比如磁盤信息,內(nèi)存是否異常,再上報(bào)給Metaserver。Compaction模塊主要是用來(lái)數(shù)據(jù)回收管理。存儲(chǔ)引擎Rocksdb,以LSM實(shí)現(xiàn),其特點(diǎn)在于寫入時(shí)是append only的形式。
RPC接入:
當(dāng)集群達(dá)到一定規(guī)模后,如果沒(méi)有自動(dòng)化運(yùn)維,那么人工運(yùn)維的成本是很高的。所以在RPC模塊加入了指標(biāo)監(jiān)控,包括QPS、吞吐量、延時(shí)時(shí)間等,當(dāng)出現(xiàn)問(wèn)題時(shí),會(huì)很方便排查。不同的業(yè)務(wù)的吞吐量是不同的,如何做到多用戶隔離?通過(guò)Quota管理,在業(yè)務(wù)接入的時(shí)候會(huì)申請(qǐng)配額,比如一張表申請(qǐng)了10K的QPS,當(dāng)超過(guò)這個(gè)值得時(shí)候,會(huì)對(duì)用戶進(jìn)行限流。不同的業(yè)務(wù)等級(jí),會(huì)進(jìn)行不同的Quota管理。
② 抽象引擎層
主要是為了應(yīng)對(duì)不同的業(yè)務(wù)場(chǎng)景。比如大value引擎,因?yàn)長(zhǎng)SM存在寫放大的問(wèn)題,如果數(shù)據(jù)的value特別大,頻繁的寫入會(huì)導(dǎo)致數(shù)據(jù)的有效寫入非常低。這些不同的引擎對(duì)于上層來(lái)說(shuō)是透明的,在運(yùn)行時(shí)通過(guò)選擇不同的參數(shù)就可以了。
5. 分裂-元數(shù)據(jù)更新
?
在KV存儲(chǔ)的時(shí)候,剛開始會(huì)根據(jù)業(yè)務(wù)規(guī)模劃分不同的分片,默認(rèn)情況下單個(gè)分片是24G的大小。隨著業(yè)務(wù)數(shù)據(jù)量的增長(zhǎng),單個(gè)分片的數(shù)據(jù)放不下,就會(huì)對(duì)數(shù)據(jù)進(jìn)行分裂。分裂的方式有兩種,rang和hash。這里我們以hash為例展開介紹:
假設(shè)一張表最開始設(shè)計(jì)了3個(gè)分片,當(dāng)數(shù)據(jù)4到來(lái),根據(jù)hash取余,應(yīng)該保存在分片1中。隨著數(shù)據(jù)的增長(zhǎng),3個(gè)分片放不下,則需要進(jìn)行分裂,3個(gè)分片會(huì)分裂成6個(gè)分片。這個(gè)時(shí)候數(shù)據(jù)4來(lái)訪問(wèn),根據(jù)Hash會(huì)分配到分片4,如果分片4正處于分裂狀態(tài),Metaserver會(huì)對(duì)訪問(wèn)進(jìn)行重定向,還是訪問(wèn)到原來(lái)的分片1。當(dāng)分片完成,狀態(tài)變?yōu)閚ormal,就可以正常接收訪問(wèn),這一過(guò)程,用戶是無(wú)感知的。
6. 分裂-數(shù)據(jù)均衡回收
?
首先需要先將數(shù)據(jù)分裂,可以理解為本地做一個(gè)checkpoint,Rocksdb的checkpoint相當(dāng)于是做了一個(gè)硬鏈接,通常1ms就可以完成數(shù)據(jù)的分裂。分裂完成后,Metaserver會(huì)同步更新元數(shù)據(jù)信息,比如0-100的數(shù)據(jù),分裂之后,分片1的50-100的數(shù)據(jù)其實(shí)是不需要的,就可以通過(guò)Compaction Filter對(duì)數(shù)據(jù)進(jìn)行回收。最后將分裂后的數(shù)據(jù)分配到不同的節(jié)點(diǎn)上。因?yàn)檎麄€(gè)過(guò)程都是對(duì)一批數(shù)據(jù)進(jìn)行操作,而不是像redis那樣主從復(fù)制的時(shí)候一條一條復(fù)制,得益于這樣的實(shí)現(xiàn),整個(gè)分裂過(guò)程都在毫秒級(jí)別。
7. 多活容災(zāi)
?
前面提到的分裂和Metaserver來(lái)保證高可用,對(duì)某些場(chǎng)景仍不能滿足需求。比如整個(gè)機(jī)房的集群掛掉,這在業(yè)界多是采用多活來(lái)解決。我們KV存儲(chǔ)的多活也是基于Binlog來(lái)實(shí)現(xiàn),比如在云立方的機(jī)房寫入一條數(shù)據(jù),會(huì)通過(guò)Binlog同步到嘉定的機(jī)房。假如位于嘉定的機(jī)房的存儲(chǔ)部分掛了以后,proxy模塊會(huì)自動(dòng)將流量切到云立方的機(jī)房進(jìn)行讀寫操作。最極端的情況,整個(gè)機(jī)房掛掉了,就會(huì)將所有的用戶訪問(wèn)集中到里一個(gè)機(jī)房,保證可用性。
03 場(chǎng)景&問(wèn)題
接下來(lái)介紹KV在B站應(yīng)用的典型場(chǎng)景以及遇到的問(wèn)題。
?
最典型的場(chǎng)景就是用戶畫像,比如推薦,就是通過(guò)用戶畫像來(lái)完成的。其他還有動(dòng)態(tài)、追番、對(duì)象存儲(chǔ)、彈幕等都是通過(guò)KV來(lái)存儲(chǔ)。
1. 定制優(yōu)化
?
基于抽象實(shí)現(xiàn),可以很方便地支持不同的業(yè)務(wù)場(chǎng)景,并對(duì)一些特定的業(yè)務(wù)場(chǎng)景進(jìn)行優(yōu)化。
Bulkload全量導(dǎo)入的場(chǎng)景主要是用于動(dòng)態(tài)推薦以及用戶畫像。用戶畫像主要是T+1的數(shù)據(jù),在沒(méi)有使用Bulkload以前,主要是通過(guò)Hive來(lái)逐條寫入,數(shù)據(jù)鏈路很長(zhǎng),每天全量導(dǎo)入10億條數(shù)據(jù)大概需要6、7個(gè)小時(shí)。使用Bulkload之后,只需要在hive離線平臺(tái)把數(shù)據(jù)構(gòu)建成一個(gè)rocksdb引擎,hive離線平臺(tái)再把數(shù)據(jù)上傳到對(duì)象存儲(chǔ)。上傳完成之后通知KV來(lái)進(jìn)行拉取,拉取完成后就可以進(jìn)行本地的Bulkload,時(shí)間可以縮短到10分鐘以內(nèi)。
另一個(gè)場(chǎng)景就是定長(zhǎng)list。大家可能發(fā)現(xiàn)你的播放歷史只有3000條,動(dòng)態(tài)也只有3000條。因?yàn)闅v史記錄是非常大的,不能無(wú)限存儲(chǔ)。最早是通過(guò)一個(gè)腳本,對(duì)歷史數(shù)據(jù)進(jìn)行刪除,為了解決這個(gè)問(wèn)題,我們開發(fā)了一個(gè)定制化引擎,保存一個(gè)定長(zhǎng)的list,用戶只需要往里面寫入,當(dāng)超過(guò)定長(zhǎng)的長(zhǎng)度時(shí),引擎會(huì)自動(dòng)刪除。
2. 面臨問(wèn)題——存儲(chǔ)引擎
前面提到的compaction,在實(shí)際使用的過(guò)程中,也碰到了一些問(wèn)題,主要是存儲(chǔ)引擎和raft方面的問(wèn)題。存儲(chǔ)引擎方面主要是Rocksdb的問(wèn)題。第一個(gè)就是數(shù)據(jù)淘汰,在數(shù)據(jù)寫入的時(shí)候,會(huì)通過(guò)不同的Compaction往下推。我們的播放歷史,會(huì)設(shè)置一個(gè)過(guò)期時(shí)間。超過(guò)了過(guò)期時(shí)間之后,假設(shè)數(shù)據(jù)現(xiàn)在位于L3層,在L3層沒(méi)滿的時(shí)候是不會(huì)觸發(fā)Compaction的,數(shù)據(jù)也不會(huì)被刪除。為了解決這個(gè)問(wèn)題,我們就設(shè)置了一個(gè)定期的Compaction,在Compaction的時(shí)候回去檢查這個(gè)Key是否過(guò)期,過(guò)期的話就會(huì)把這條數(shù)據(jù)刪除。
另一個(gè)問(wèn)題就是DEL導(dǎo)致SCAN慢查詢的問(wèn)題。因?yàn)長(zhǎng)SM進(jìn)行delete的時(shí)候要一條一條地掃,有很多key。比如20-40之間的key被刪掉了,但是LSM刪除數(shù)據(jù)的時(shí)候不會(huì)真正地進(jìn)行物理刪除,而是做一個(gè)delete的標(biāo)識(shí)。刪除之后做SCAN,會(huì)讀到很多的臟數(shù)據(jù),要把這些臟數(shù)據(jù)過(guò)濾掉,當(dāng)delete非常多的時(shí)候,會(huì)導(dǎo)致SCAN非常慢。為了解決這個(gè)問(wèn)題,主要用了兩個(gè)方案。第一個(gè)就是設(shè)置刪除閾值,超過(guò)閾值的時(shí)候,會(huì)強(qiáng)制觸發(fā)Compaction,把這些delete標(biāo)識(shí)的數(shù)據(jù)刪除掉。但是這樣也會(huì)產(chǎn)生寫放大的問(wèn)題,比如有L1層的數(shù)據(jù)進(jìn)行了刪除,刪除的時(shí)候會(huì)觸發(fā)一個(gè)Compaction,L1的文件會(huì)帶上一整層的L2文件進(jìn)行Compaction,這樣會(huì)帶來(lái)非常大的寫放大的問(wèn)題。為了解決寫放大,我們加入了一個(gè)延時(shí)刪除,在SCAN的時(shí)候,會(huì)統(tǒng)計(jì)一個(gè)指標(biāo),記錄當(dāng)前刪除的數(shù)據(jù)占所有數(shù)據(jù)的比例,根據(jù)這個(gè)反饋值去觸發(fā)Compaction。
第三個(gè)是大Value寫入放大的問(wèn)題,目前業(yè)內(nèi)的解決辦法都是通過(guò)KV存儲(chǔ)分離來(lái)實(shí)現(xiàn)的。我們也是這樣解決的。
3. 面臨問(wèn)題——Raft
?
Raft層面的問(wèn)題有兩個(gè):
首先,我們的Raft是三副本,在一個(gè)副本掛掉的情況下,另外兩個(gè)副本可以提供服務(wù)。但是在極端情況下,超過(guò)半數(shù)的副本掛掉,雖然概率很低,但是我們還是做了一些操作,在故障發(fā)生的時(shí)候,縮短系統(tǒng)恢復(fù)的時(shí)間。我們采用的方法就是降副本,比如三個(gè)副本掛了兩個(gè),會(huì)通過(guò)后臺(tái)的一個(gè)腳本將集群自動(dòng)降為單副本模式,這樣依然可以正常提供服務(wù)。同時(shí)會(huì)在后臺(tái)啟動(dòng)一個(gè)進(jìn)程對(duì)副本進(jìn)行恢復(fù),恢復(fù)完成后重新設(shè)置為多副本模式,大大縮短了故障恢復(fù)時(shí)間。
另一個(gè)是日志刷盤問(wèn)題。比如點(diǎn)贊、動(dòng)態(tài)的場(chǎng)景,value其實(shí)非常小,但是吞吐量非常高,這種場(chǎng)景會(huì)帶來(lái)很嚴(yán)重的寫放大問(wèn)題。我們用磁盤,默認(rèn)都是4k寫盤,如果每次的value都是幾十個(gè)字節(jié),這樣會(huì)造成很大的磁盤浪費(fèi)?;谶@樣的問(wèn)題,我們會(huì)做一個(gè)聚合刷盤,首先會(huì)設(shè)置一個(gè)閾值,當(dāng)寫入多少條,或者寫入量超過(guò)多少k,進(jìn)行批量刷盤,這個(gè)批量刷盤可以使吞吐量提升2~3倍。
04 總結(jié)思考
?
1. 應(yīng)用
應(yīng)用方面,我們會(huì)做KV與緩存的融合。因?yàn)闃I(yè)務(wù)開發(fā)不太了解KV與緩存資源的情況,融合之后就不需要再去考慮是使用KV還是緩存。
另一個(gè)應(yīng)用方面的改進(jìn)是支持Sentinel模式,進(jìn)一步降低副本成本。
2. 運(yùn)維
運(yùn)維方面,一個(gè)問(wèn)題就是慢節(jié)點(diǎn)檢測(cè),我們可以檢測(cè)到故障節(jié)點(diǎn),但是慢節(jié)點(diǎn)怎么檢測(cè)呢,目前在業(yè)界也是一個(gè)難題,也是我們今后要努力的方向。
另一個(gè)問(wèn)題就是自動(dòng)剔盤均衡,磁盤發(fā)生故障后,目前的方法是第二天看一些報(bào)警事項(xiàng),再人工操作一下。我們希望做成一個(gè)自動(dòng)化機(jī)制。
3. 系統(tǒng)
系統(tǒng)層面就是SPDK、DPDK方面的性能優(yōu)化,通過(guò)這些優(yōu)化,進(jìn)一步提升KV進(jìn)程的吞吐。