LSM Tree-Based存儲引擎的compaction策略(feat. RocksDB)

前言

這篇從半個月前就開始寫,斷斷續續寫到現在,終于能發了(被簡書吞了好幾次),不容易。

最近筆者正在補習與RocksDB底層相關的細節,因為:

  • 次要原因——當前所有Flink實時任務的狀態后端都是RocksDB;
  • 主要原因——將來會利用TiDB搭建HTAP服務。TiDB與我們現有的MySQL可以無縫銜接,并且它的基礎正是RocksDB。

RocksDB與筆者多次講過的HBase一樣,都屬于基于LSM樹的存儲引擎,只不過前者偏向嵌入式用途,更輕量級而已。看官可以先食用這篇文章獲得關于LSM樹的前置知識。

下面先粗略看看RocksDB的讀寫流程,其實與HBase是很像的。

RocksDB讀寫簡介

直接畫圖說明。這張圖取自Flink PMC大佬Stefan Richter在Flink Forward 2018演講的PPT,筆者重畫了一下。

RocksDB的寫緩存(即LSM樹的最低一級)名為memtable,對應HBase的MemStore;讀緩存名為block cache,對應HBase的同名組件。

執行寫操作時,先同時寫memtable與預寫日志WAL。memtable寫滿后會自動轉換成不可變的(immutable)memtable,并flush到磁盤,形成L0級sstable文件。sstable即有序字符串表(sorted string table),其內部存儲的數據是按key來排序的,后文將其簡稱為SST。

執行讀操作時,會首先讀取內存中的數據(根據局部性原理,剛寫入的數據很有可能被馬上讀取),即active memtable→immutable memtable→block cache。如果內存無法命中,就會遍歷L0層sstable來查找。如果仍未命中,就通過二分查找法在L1層及以上的sstable來定位對應的key。

隨著sstable的不斷寫入,系統打開的文件就會越來越多,并且對于同一個key積累的數據改變(更新、刪除)操作也就越多。由于sstable是不可變的,為了減少文件數并及時清理無效數據,就要進行compaction操作,將多個key區間有重合的sstable進行合并。本文暫無法給出"compaction"這個詞的翻譯,個人認為把它翻譯成“壓縮”(compression?)或者“合并”(merge?)都是片面的。

通過上面的簡介,我們會更加認識到,LSM樹是一種以讀性能作為trade-off換取寫性能的結構,并且RocksDB中的flush和compaction操作正是LSM思想的核心。下面來介紹LSM-based存儲中通用的兩種compaction策略,即size-tiered compaction和leveled compaction。

通用compaction策略

size-tiered compaction與空間放大

size-tiered compaction的思路非常直接:每層允許的SST文件最大數量都有個相同的閾值,隨著memtable不斷flush成SST,某層的SST數達到閾值時,就把該層所有SST全部合并成一個大的新SST,并放到較高一層去。下圖是閾值為4的示例。

https://www.scylladb.com/2018/01/17/compaction-series-space-amplification/

size-tiered compaction的優點是簡單且易于實現,并且SST數目少,定位到文件的速度快。當然,單個SST的大小有可能會很大,較高的層級出現數百GB甚至TB級別的SST文件都是常見的。它的缺點是空間放大比較嚴重,下面詳細說說。

所謂空間放大(space amplification),就是指存儲引擎中的數據實際占用的磁盤空間比數據的真正大小偏多的情況。例如,數據的真正大小是10MB,但實際存儲時耗掉了25MB空間,那么空間放大因子(space amplification factor)就是2.5。

為什么會出現空間放大呢?很顯然,LSM-based存儲引擎中數據的增刪改都不是in-place的,而是需要等待compaction執行到對應的key才算完。也就是說,一個key可能會同時對應多個value(刪除標記算作特殊的value),而只有一個value是真正有效的,其余那些就算做空間放大。另外,在compaction過程中,原始數據在執行完成之前是不能刪除的(防止出現意外無法恢復),所以同一份被compaction的數據最多可能膨脹成原來的兩倍,這也算作空間放大的范疇。

下面用Cassandra的size-tiered compaction策略舉兩個例子,以方便理解。每層SST個數的閾值仍然采用默認值4。

  • 以約3MB/s的速度持續插入新數據(保證unique key),時間與磁盤占用的曲線圖如下。

圖中清晰可見有不少毛刺,這就是compaction過程造成的空間放大。注意在2000s~2500s之間還有一個很高的尖峰,原數據量為6GB,但在一瞬間增長到了12GB,說明Cassandra在做大SST之間的compaction,大SST的缺陷就顯現出來了。盡管這只是暫時的,但是也要求我們必須預留出很多不必要的空閑空間,增加成本。

  • 重復寫入一個400萬條數據的集合(約1.2GB大,保證unique key),共重復寫入15次來模擬數據更新,時間與磁盤占用的曲線圖如下。

這種情況更厲害,最高會占用多達9.3GB磁盤空間,放大因子為7.75。雖然中途也會觸發compaction,但是最低只能壓縮到3.5GB左右,仍然有近3倍的放大。這是因為重復key過多,就算每層compaction過后消除了本層的空間放大,但key重復的數據仍然存在于較低層中,始終有冗余。只有手動觸發了full compaction(即圖中2500秒過后的最后一小段),才能完全消除空間放大,但我們也知道full compaction是極耗費性能的。

接下來介紹leveled compaction,看看它是否能解決size-tiered compaction的空間放大問題。

leveled compaction與寫放大

leveled compaction的思路是:對于L1層及以上的數據,將size-tiered compaction中原本的大SST拆開,成為多個key互不相交的小SST的序列,這樣的序列叫做“run”。L0層是從memtable flush過來的新SST,該層各個SST的key是可以相交的,并且其數量閾值單獨控制(如4)。從L1層開始,每層都包含恰好一個run,并且run內包含的數據量閾值呈指數增長。

下圖是假設從L1層開始,每個小SST的大小都相同(在實際操作中不會強制要求這點),且數據量閾值按10倍增長的示例。即L1最多可以有10個SST,L2最多可以有100個,以此類推。

https://www.scylladb.com/2018/01/31/compaction-series-leveled-compaction/

隨著SST不斷寫入,L1的數據量會超過閾值。這時就會選擇L1中的至少一個SST,將其數據合并到L2層與其key有交集的那些文件中,并從L1刪除這些數據。仍然以上圖為例,一個L1層SST的key區間大致能夠對應到10個L2層的SST,所以一次compaction會影響到11個文件。該次compaction完成后,L2的數據量又有可能超過閾值,進而觸發L2到L3的compaction,如此往復,就可以完成Ln層到Ln+1層的compaction了。

可見,leveled compaction與size-tiered compaction相比,每次做compaction時不必再選取一層內所有的數據,并且每層中SST的key區間都是不相交的,重復key減少了,所以很大程度上緩解了空間放大的問題。重復一遍上一節做的兩個實驗,曲線圖分別如下。

持續寫入實驗,尖峰消失了。

持續更新實驗,磁盤占用量的峰值大幅降低,從原來的9.3GB縮減到了不到4GB。

但是魚與熊掌不可兼得,空間放大并不是唯一掣肘的因素。仍然以size-tiered compaction的第一個實驗為例,寫入的總數據量約為9GB大,但是查看磁盤的實際寫入量,會發現寫入了50個G的數據。這就叫寫放大(write amplification)問題。

寫放大又是怎么產生的呢?下面的圖能夠說明。

可見,這是由compaction的本質決定的:同一份數據會不斷地隨著compaction過程向更高的層級重復寫入,有多少層就會寫多少次。但是,我們的leveled compaction的寫放大要嚴重得多,同等條件下實際寫入量會達到110GB,是size-tiered compaction的兩倍有余。這是因為Ln層SST在合并到Ln+1層時是一對多的,故重復寫入的次數會更多。在極端情況下,我們甚至可以觀測到數十倍的寫放大。

寫放大會帶來兩個風險:一是更多的磁盤帶寬耗費在了無意義的寫操作上,會影響讀操作的效率;二是對于閃存存儲(SSD),會造成存儲介質的壽命更快消耗,因為閃存顆粒的擦寫次數是有限制的。在實際使用時,必須權衡好空間放大、寫放大、讀放大三者的優先級。

RocksDB的混合compaction策略

由于上述兩種compaction策略都有各自的優缺點,所以RocksDB在L1層及以上采用leveled compaction,而在L0層采用size-tiered compaction。下面分別來看看。

leveled compaction

當L0層的文件數目達到level0_file_num_compaction_trigger閾值時,就會觸發L0層SST合并到L1。

L1層及以后的compaction過程完全符合前文所述的leveled compaction邏輯,如下圖所示,很容易理解。

多個compaction過程是可以并行進行的,如下圖所示。最大并行數由max_background_compactions參數來指定。

前面說過,leveled compaction策略中每一層的數據量是有閾值的,那么在RocksDB中這個閾值該如何確定呢?需要分兩種情況來討論。

  • 參數level_compaction_dynamic_level_bytes為false
    這種情況下,L1層的大小閾值直接由參數max_bytes_for_level_base決定,單位是字節。各層的大小閾值會滿足如下的遞推關系:

target_size(Lk+1) = target_size(Lk) * max_bytes_for_level_multiplier * max_bytes_for_level_multiplier_additional[k]

其中,max_bytes_for_level_multiplier是固定的倍數因子,max_bytes_for_level_multiplier_additional[k]是第k層對應的可變倍數因子。舉個例子,假設max_bytes_for_level_base = 314572800,max_bytes_for_level_multiplier = 10,所有max_bytes_for_level_multiplier_additional[k]都為1,那么就會形成如下圖所示的各層閾值。

可見,這與上文講leveled compaction時的示例是一個意思。

  • 參數level_compaction_dynamic_level_bytes為true
    這種情況比較特殊。最高一層的大小不設閾值限制,亦即target_size(Ln)就是Ln層的實際大小,而更低層的大小閾值會滿足如下的倒推關系:

target_size(Lk-1) = target_size(Lk) / max_bytes_for_level_multiplier

可見,max_bytes_for_level_multiplier的作用從乘法因子變成了除法因子。特別地,如果出現了target_size(Lk) < max_bytes_for_level_base / max_bytes_for_level_multiplier的情況,那么這一層及比它低的層就都不會再存儲任何數據。

舉個例子,假設現在有7層(包括L0),L6層已經存儲了276GB的數據,并且max_bytes_for_level_base = 1073741824,max_bytes_for_level_multiplier = 10,那么就會形成如下圖所示的各層閾值,亦即L5~L1的閾值分別是27.6GB、2.76GB、0.276GB、0、0。

可見,有90%的數據都落在了最高一層,9%的數據落在了次高一層。由于每個run包含的key都是不重復的,所以這種情況比上一種更能減少空間放大。

universal compaction

universal compaction是RocksDB中size-tiered compaction的別名,專門用于L0層的compaction,因為L0層的SST的key區間是幾乎肯定有重合的。

前文已經說過,當L0層的文件數目達到level0_file_num_compaction_trigger閾值時,就會觸發L0層SST合并到L1。universal compaction還會檢查以下條件。

  • 空間放大比例
    假設L0層現有的SST文件為(R1, R1, R2, ..., Rn),其中R1是最新寫入的SST,Rn是較舊的SST。所謂空間放大比例,就是指R1~Rn-1文件的總大小除以Rn的大小,如果這個比值比max_size_amplification_percent / 100要大,那么就會將L0層所有SST做compaction。

  • 相鄰文件大小比例
    有一個參數size_ratio用于控制相鄰文件大小比例的閾值。如果size(R2) / size(R1)的比值小于1 + size_ratio / 100,就表示R1和R2兩個SST可以做compaction。接下來繼續檢查size(R3) / size(R1 + R2)是否小于1 + size_ratio / 100,若仍滿足,就將R3也加入待compaction的SST里來。如此往復,直到不再滿足上述比例條件為止。

當然,如果上述兩個條件都沒能觸發compaction,該策略就會線性地從R1開始合并,直到L0層的文件數目小于level0_file_num_compaction_trigger閾值。

The End

還是寫的很亂,但就這樣吧。

困了。明天加班,民那晚安。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容

  • 最近看了一篇 Paper,Dostoevsky: Better Space-Time Trade-Offs for...
    siddontang閱讀 8,310評論 1 21
  • 最近看到一篇 Paper,Auto-tuning RocksDB,頓時兩眼放光。RocksDB 以配置多,難優化而...
    siddontang閱讀 15,519評論 1 21
  • 最近項目中用到這個nb的玩意,所以就花時間研究了下,同時整理下助自己記憶。這個猛虎上山的logo就是rocksdb...
    小東_16d3閱讀 9,173評論 3 10
  • 動態地給一個對象增加一些額外的職責,就增加對象功能來說,裝飾模式比生成子類實現更為靈活。 類型 結構型 簡介 裝飾...
    lyu571閱讀 559評論 0 2
  • 我是一名陰陽先生,雖然沒有逆天改命的本事,但也有一點驅邪治病的能力,所以在這村子里面,我的名氣也是頗高。 今天我一...
    小小情緒vat閱讀 491評論 0 0