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