自研磁碟型特徵儲存引擎RDB在雲音樂的實踐

雲音樂技術團隊發表於2022-03-21

本文作者:奇濤,來自資料智慧部-實時計算組,主要負責雲音樂演算法特徵儲存相關工作。

業務背景

雲音樂推薦和搜尋業務有大量的演算法特徵資料,需要以key-value的形式儲存,提供線上的讀寫服務。這些特徵主要從大資料平臺上spark或者flink的任務產出,比如歌曲的特徵、使用者的特徵等。它們的特點是資料量大,每天定時全量更新或者實時增量更新,而且對查詢的效能要求高。這些演算法特徵資料,有的儲存在redis/tair記憶體型儲存系統中,也有的儲存在myrocks/hbase磁碟型儲存系統中。

為了減小接入多種不同儲存系統帶來的成本,並且可以針對演算法特徵kv資料的儲存特點定製化開發,我們在tair分散式儲存框架下引入rocksdb引擎,用以低成本地支援資料量較大的演算法特徵kv資料場景的線上儲存。

下面先簡要介紹tair引入rocksdb的方案,再介紹我們在演算法特徵kv儲存上的實踐。為區分tair框架下以memcache為引擎的記憶體型儲存和以rocksdb為引擎的磁碟型儲存,我們將兩者分別稱為MDB和RDB。

RDB介紹

tair作為分散式儲存框架,分為ConfigServer和DataServer兩部分。DataServer由多個節點組成,負責資料的實際儲存。所有kv資料根據對key計算hash值劃分到若干桶(bucket),每個桶的資料可以儲存多個副本到不同的DataServer節點上,具體對映規則由ConfigServer構建的路由表決定。

tair分散式儲存框架

ConfigServer維護所有DataServer節點的狀態,如果有節點增加或者減少,則發起資料遷移,並構建新的路由表。DataServer支援不同的底層儲存引擎,底層引擎需要實現kv資料的基本操作put/get/delete,以及資料全量掃描scan介面。Client通過ConfigServer提供的路由表,向實際要請求的DataServer節點讀寫資料。讀寫資料均請求對應桶的master節點,如果是寫資料由DataServer內部完成資料的主從複製。

rocksdb儲存引擎原理

而rocksdb為開源的kv儲存引擎,原理為lsm(log structured merge),lsm為很多sst檔案組成的分層結構。每個sst檔案包含一定數量的kv資料,並附帶相應的後設資料資訊,而且sst檔案中的kv都是按key排序的。通過分層的方式,定期將各個level的資料做合併(compaction),刪除無效資料。新寫入的資料放在level0,level0的規模達到閾值後compaction到level1,依次類推。每一層的所有sst檔案也保持整體有序且不重疊(level0除外),查詢時從上往下在各level中檢索。

在tair中引入rocksdb時,我們設計了每條kv資料的儲存格式如下。

RDB中的kv格式

儲存到rocksdb中的key,由bucket_id+area_id+原始key拼接而成。其中area_id指的是業務表id,不同的資料表有不同的area。bucket_id的作用是為了資料遷移時方便按桶依次遷移,因為rocksdb的資料是有序儲存的,相同桶的資料聚集在一起可以通過字首掃描提高效率。area_id的作用是為了區分不同的業務表,避免key有重疊。其實,對於資料量大的表,我們在rocksdb中會儲存到單獨的column family中,這樣同時也能避免key重疊。

儲存到rocksdb中的value,由meta+原始value拼接而成。其中meta儲存了kv的修改時間、過期時間等資訊,因為rocksdb中的資料可以在compaction的時候判斷是否丟棄,通過自定義CompactionFilter可以實現過期資料的刪除。

bulkload批量導資料

bulkload方案

演算法特徵資料有很多的場景都是每天在大資料平臺離線計算出最新的全量資料,再匯入kv儲存引擎,這些資料表的規模經常在100GB以上,條數在1億條以上。基礎版本的RDB只能通過呼叫put介面逐條寫入,這樣會導致需要有很多併發的任務來通過put方式匯入全量資料,佔用大資料平臺的計算資源。

而且,因為資料put寫入RDB的順序是無序的,這樣會導致rocksdb在compaction的時候io壓力較大,因為需要對大量的kv做完排序後重新生成整體有序的sst檔案。這也就是rocksdb的寫放大問題,rocksdb真實寫資料的量會放大幾十倍,磁碟io壓力會導致讀請求的響應時間波動。

針對此問題,我們借鑑了hbase的bulkload機制來提高匯入效率。匯入大規模的離線特徵時,先通過spark將原始資料排序並轉換為rocksdb內部的資料格式檔案sst,再通過排程程式依次將sst檔案載入(rocksdb提供ingest機制)到RDB叢集中相應資料節點。

bulkload方案

這個過程中有兩個點提升了導資料的效率,一是通過檔案大批量載入資料,而不是呼叫put介面寫入單條/多條資料。二是在Spark轉換資料時已經做了排序,減少了rocksdb內部的資料合併(compaction)。

通過一份線上的真實演算法特徵資料,我們對比了bulkload方式和逐條put方式匯入的效能,bulkload方式在io壓力、讀rt、compaction量上均明顯好於put方式,約3倍提升。場景:已有全量資料3.8TB(2副本共7.6TB),匯入2.1億條增量資料300GB(2副本共600GB),匯入時間均控制在100分鐘左右,讀qps為1.2w/s。

io-util對比(bulkload vs put)

平均讀rt對比(bulkoad vs put)

通過rocksdb內部日誌對比兩者compaction情況,bulkload共85GB(10:00到13:00),put共273GB(13:00到16:00),約1:3.2。

10:00 Cumulative compaction: 1375.15 GB write, 6.43 MB/s write, 1374.81 GB read, 6.43 MB/s read, 23267.8 seconds 
13:00 Cumulative compaction: 1460.62 GB write, 6.29 MB/s write, 1460.29 GB read, 6.29 MB/s read, 24320.8 seconds 
16:00 Cumulative compaction: 1733.60 GB write, 7.16 MB/s write, 1733.31 GB read, 7.16 MB/s read, 27675.0 seconds

雙版本導資料

在bulkload的基礎上,對於每次通過全量導資料覆蓋更新的場景,我們通過雙版本的機制,進一步減少了bulkload導資料時的磁碟io。一份資料對應2個版本(areaid),即對應到rocksdb中的2個column family。導資料和讀資料的版本錯開,並輪流切換。導資料前先清空無效版本的資料,這樣完全避免了rocksdb中的資料合併(compaction)。

雙版本的機制使用了儲存代理層的多版本功能,具體方案和細節這裡不作介紹。通過這種方式,導資料期間查詢資料的rt波動更小。下圖為同一份資料在RDB叢集與冷熱叢集(hBase+redis)讀rt的監控對比。

雙版本bulkload效果對比

key-value分離儲存

kv分離方案

rocksdb通過compaction合併無效的資料,並保證每個level的資料都是有序的。compaction過程會引起寫放大問題。對於長value,寫放大問題更嚴重,因為value會被頻繁的讀寫。對於長value的寫放大問題,業內已經有針對SSD儲存的kv分離方案了《WiscKey: Separating Keys from Values in SSD-conscious Storage》[1]。即將value單獨存放在blob檔案中,lsm中只儲存value在blob檔案中的位置索引(fileno+offset+size)。

在RDB中,我們引入了tidb開源的kv分離外掛,它對rocksdb的程式碼入侵較小,且有一套無效資料回收的GC機制。GC的方式是在每次compaction時更新每個blob檔案有多少資料量的value被有效引用,如果一個blob檔案的有效資料比例低於某個閾值(預設0.5),則重寫有效資料到新檔案,並刪除原檔案。

kv分離原理

通過對比,對於長value,kv分離在隨機寫資料和bulkload導資料場景下均有不同程度的效能提升,但代價是更多的磁碟空間佔用。隨機寫資料由於本身寫放大問題嚴重,kv分離後讀rt能下降90%。bulkload導資料kv分離後讀rt也能下降50%以上。並且,我們測得kv分離有效果的value長度閾值約在0.5KB~0.7KB之間,線上部署時配置預設閾值為1KB,超過此長度的value會被分離存放在blob檔案中。

下圖是我們測試的一個場景,value平均長度5.3KB,全量資料800GB(1.6億條),bulkload匯入更新資料,並隨機讀資料。不做kv分離時,平均讀rt為1.02ms,做了kv分離後,平均讀rt為0.44ms,降低57%。

kv分離讀rt對比

序列append

在kv分離這個機制的基礎上,我們也在探索進一步的創新:實現blob檔案中value的原地更新。

這個想法的來源是這樣的:有些演算法特徵是以序列形式的value儲存的,比如使用者的歷史行為,更新的方式是向長序列中追加(append)一個短序列。按照原有的kv方式,我們需要先獲取原序列,再append更新資料形成新的序列,最後寫到RDB。這個過程多餘了大量資料的讀寫。針對這個問題,我們研發了序列append更新的介面。

如果只是簡單的在RDB內部做序列的讀取->追加->寫入操作,仍然會存在大量的磁碟讀寫。於是我們做了一個改造:提前在kv分離後的blob檔案中對每個value預留一部分空間(類似於STL中的vector的記憶體分配),序列append時直接寫入到blob檔案中value的尾部。如果這一過程無法進行(如預留空間不夠),則仍舊執行讀取->追加->寫入的操作。

序列append更新的儲存格式如下:

序列append更新儲存

序列append更新的詳細流程如下:

序列append更新流程

目前RDB的序列append功能已經上線,效果也非常明顯。一個實際的演算法特徵儲存場景,原來每次更新資料量幾TB,耗時10小時,現在每次更新資料量幾GB,耗時1小時。

ProtoBuf欄位更新

序列append的方案證實是可行的,於是我們探索進一步的擴充套件:支援更多通用的“部分更新”介面,如add/incr等。

雲音樂的演算法特徵kv資料,value基本上是以ProtoBuf(簡稱PB)的格式儲存的,我們演算法工程團隊在2020年也自研了支援PB格式欄位級更新的記憶體型儲存引擎(在MDB上擴充套件為PDB),後續也會有專門對PDB的詳細介紹文章,這裡不做具體介紹。PDB的原理是通過引擎層對PB的編解碼,支援對指定編號的欄位做更新操作,如incr/update/add,也包括更復雜的reapted欄位的去重和排序等。這樣原來要在應用層實現的讀取->解碼->更新->編碼->寫入的過程,現在只要呼叫pb_upate介面即可完成。PDB已線上上廣泛應用,因此我們希望能把這一套PB更新功能擴充套件到磁碟型特徵儲存引擎RDB上。

目前這一塊我們已經開發完成,正在做更多的測試。方案是複用PDB的PB更新邏輯,改造rocksdb程式碼實現kv分離後的value原地修改,避免頻繁的compaction帶來多餘的磁碟讀寫。上線後的效果待後續再同步。

改造後的rocksdb儲存格式如下:

改造後的rocksdb儲存

RDB中PB更新的詳細流程如下:

RDB中PB更新流程

總結思考

經過一年多的時間,在基礎版本的RDB上,我們根據演算法特徵資料儲存的特點,定製化研發了以上一些新特性。目前RDB線上叢集已經具備一定的規模,儲存資料條數百億級,資料量十TB級,QPS峰值達百萬每秒。

對於RDB的自研特性,我們的思考是這樣的:底層核心為改造後的rocksdb(帶kv分離),在此之上定製化研發新的應用場景,包括離線特徵bulkload、實時特徵snapshot、PB欄位更新協議等。

當然,RDB也存在一些不足之處。比如,RDB採用的tair框架按key通過hash分割槽,相比於通過range分割槽,在掃描一個範圍的資料時支援得就不好。另外,目前RDB支援的資料結構和操作介面也比較簡單,我們後續也將根據特徵儲存的業務需要,研發支援更多的功能,比如計算查詢一個時間序列視窗的統計值(sum/avg/max等)。我們也會結合內部特徵平臺Feature Store的演進,構建一套完整的面向機器學習的特徵儲存服務。

參考資料

[1]. Arpaci-Dusseau R H, Arpaci-Dusseau R H, Arpaci-Dusseau R H, et al. WiscKey: Separating Keys from Values in SSD-Conscious Storage[J]. Acm Transactions on Storage, 2017, 13(1):5.

本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們staff.musicrecruit@service.ne...

相關文章