資料庫儲存與索引技術(三)LSM樹實現案例

OceanBase資料庫發表於2023-03-16

1678242452

1. MemTable

OceanBase 資料庫的記憶體儲存引擎 MemTable 由 BTree 和 Hashtable 組成。在插入/更新/刪除資料時,首先記錄Redo日誌,並透過RPC進行Paxos協議複製到其他多數副本,然後再將資料寫入記憶體塊,在 HashTable 和 BTree 中儲存的均為指向對應資料的指標。副本透過接收Master節點傳送過來的Redo日誌進行重做回放將資料到自身的MemTable。

1678242472

MemTable在記憶體中維護了歷史版本的事務,每⼀⾏將歷史事務針對該⾏的操作按照時間順序組織成⾏操作鏈,新事務提交時會往⾏操作鏈尾部追加新的⾏操作。如果⾏操作鏈儲存的歷史事務過多,將影響讀取效能,此時需要觸發記憶體compaction操作,融合這些歷史事務以⽣成新的⾏操作鏈。

兩種索引結構各自有優缺點,單獨無法很好的完成資料庫的點查和範圍查詢的需求,所以OB將兩者結合起來使用,唯 一的代價就是在多執行緒併發環境下資料寫入需要對兩種資料結構都進行操作,實現相對複雜。

資料結構 優點 缺點
HashTable · 插入一行資料的時候,需要先檢查此行資料是否已經存在,當且僅當資料不存在時才能插入,檢查衝突時,Hashtable 要比 BTree 快。
· 事務在插入或更新一行資料的時候,需要找到此行並對其進行上鎖,防止其它事務修改此行,OceanBase 資料庫的行鎖放在 ObMvccRow 中,需要先找到它,才能上鎖。
不適合對範圍查詢使用HashTable。
BTree 範圍查詢時,由於 BTree node 中的資料都是有序的,因此只需要搜尋區域性的資料就可以了。 單行的查詢,也需要進行大量的 rowkey 比較,從根結點找到葉子結點,而 rowkey 比較效能是較差的,因此理論上效能比 HashTable 慢很多。

2. SSTable

當 MemTable 的大小達到某個閾值後,OceanBase 資料庫會將 MemTable 中的資料轉存於磁碟上,轉儲後的結構稱之為 SSTable。OceanBase的SSTable是不定長的,其內部會被切分為2MB一個的定長資料塊,OB稱之為宏塊(Macro Block)。宏塊是OB寫資料到磁碟的基本IO單位。

宏塊內部會被劃分為多個16KB左右的變長資料塊,OB稱之為微塊(Micro Block)。微塊中會儲存若干資料行(Row),微塊是OB資料檔案讀取的基本單位。微塊在構造時會寫入連續的資料行,到16KB左右,然後再透過通用壓縮演算法(LZ4, ZSTD等),然後再加入到宏塊當中。

宏塊是定長的,大小為固定的 2MB,長度不可以被調整;微塊預設大小為16KB,經通用壓縮後是變長的。OB在進行 IO 讀取時,會按照 4KB 來做 IO 對齊,因此一次 IO 讀的長度並不一定與微塊長度完全一致。微塊長度可以被修改,透過以下的語句可以對於不同的表設定不同的微塊長度:

ALTER TABLE mytest SET block_size = 131072;

一般來說微塊長度越大,資料的壓縮比會越高,但相應的一次 IO 讀的代價也會越大;微塊長度越小,資料的壓縮比會越低,但相應的一次 IO 讀的代價會更小。

1678242571

3. 資料壓縮

3.1. 傳統關聯式資料庫資料壓縮實現

傳統關係型資料庫理論和架構在20世紀70,80年代被確定下來,直到2010年的移動網際網路時代之前,其架構都沒有大的改變。早期資料庫系統,CPU算力較弱,而資料壓縮是非常耗費CPU的操作,因此傳統基於頁面行存的關聯式資料庫中一開始都沒有為資料壓縮排行頂層設計。到網際網路時代面對海量資料,傳統的關係型資料庫也開始考慮增加壓縮特性,降低使用者的儲存成本。

傳統的關係型資料庫的壓縮,主要有兩個方向,一個是針對OLTP,即事務處理的workload提供的方案。另外一種是針對OLAP,即資料分析型的workload提供的方案。

針對OLTP的workload提供的方案主要是基於頁面的通用壓縮。OLTP型的workload,通常需要對行內的資料進行更改,一般來說,採用行儲存效能較好。這種負載下,一個頁面內的行可能會被上層應用反覆修改,因此行與行之間的資料壓縮基本難以進行,主流廠商一般都是才用了基於頁面的整體通用壓縮。以MySQL的InnoDB引擎為例,一個頁面未壓縮前,在磁碟上佔用16KB的空間,壓縮後只有11KB,那麼按照4KB進行IO對齊的話,壓縮後的資料佔用12KB的磁碟空間。InnoDB的這個頁面在檔案中仍然佔用16KB的空間,但是利用作業系統的稀疏檔案(Sparse File)特性,作業系統實際只為這個頁面申請了12KB的磁碟空間,因此可以節約4KB的空間。

1678242595

這種壓縮方式也會帶來很多問題:

  • 如果一個頁面被修改很頻繁的話,反覆的讀出和寫入資料的過程中需要進行壓縮/解壓,對CPU的開銷也不可忽略。
  • 如果頁面被修改並壓縮後,其大小比原來增加了,如原來壓縮後是12KB,現在壓縮後是13KB。作業系統需要為新增加的資料分配磁碟空間,此時分配的磁碟空間通常都不是和之前的12KB的空間連續的。這樣對這個頁面的讀取,IO是不連續的,效能會變差。

總結起來就是,傳統資料庫的基於行存頁面的壓縮,通常情況下,只能幫助使用者節約一部分儲存空間,並不會帶來效能的提升。如果運用不當,效能有可能還會下降。

OLAP型負載的資料壓縮,通常資料都是隻讀的,沒有了資料更新的限制,資料壓縮做的非常激進。在頁面內的資料可以不按行存,而是按照列存。相同列的資料放在一起,可以做非常多的壓縮,如常量編碼,字典編碼,Delta編碼,RLE編碼等等。這些資料編碼可以有效減少頁面的儲存空間,同時不會對資料查詢有任何的影響。由於OB也採用了類似的壓縮方案,因此這裡的列存壓縮方案,會放到OB的壓縮實現中去講。

傳統的基於頁面儲存的關聯式資料庫還有一個問題就是,OLAP和OLTP兩種負載的不可調和性。要想OLAP處理能力強,資料一般都是行存的;要想OLTP處理能力強,資料需要是列存的。如果要想兩者都要(小孩才做選擇,成年人全都要),那麼必須要部署兩套異構的儲存(儘管資料庫服務程式可能是一個/一套),一套專門用於OLTP,一套用於OLAP,二者之間還需要透過資料鏈路進行同步。這無疑顯著增加了硬體和運維管理的成本。

3.2. LSM樹資料壓縮優勢綜述

基於LSM的儲存引擎,其在磁碟上的資料,在下一次合併之前,都是隻讀而不會被修改的。透過BigTable和LevelDB引入的SSTable,SSTable內部再分Block儲存的概念目前已經是LSM樹實現的標配。這些只讀的Block,類比傳統資料庫的頁的話,是非常好的壓縮目標。相比OLTP資料庫的頁面,SSTable的Block壓縮有以下幾個優勢:

  • 資料只讀,不會因為反覆修改而帶來反覆的壓縮/解壓的CPU消耗,也不會因為反覆修改帶來的資料膨脹導致的IO不連續。
  • 資料內聚,一個Block內通常有幾十乃至更多的行,可以在Block寫入的時候一次性的針對這些行做列式壓縮,可以得到更好的壓縮比,而且可以加速後續的查詢。

業內基於LSM的儲存其實也看到了這些壓縮優勢並有了相當的實現,如Facebook提出的RCFile。但是這類應用通常也都是Hadoop等批處理場景,實際在OLTP資料庫場景運用這種壓縮方案的,據瞭解就Spanner和OceanBase。

1678242657

3.3. OB資料壓縮實現

我們前面提到,OB的SSTable的格式,是分為SSTable,Macro Block, Micro Block三級。OB的開源3.x版本只實現了Micro Block內的行儲存,內部版本中實現了Micro Block內的列儲存。OB在Micro Block內基於列儲存進行高效的編碼壓縮,然後在整體以Micro Block為單位進行通用演算法壓縮。按照OB早期的文章的宣稱,客戶在進行MySQL資料遷移的時候,經驗公式是按照6:1進行容量規劃的,即OB按照列式編碼壓縮+通用壓縮之後的資料,只有同等MySQL的1/6(該結論未做深入考證,不確定是否包含OB三副本,以及MySQL資料是否啟用壓縮,僅供參考)。

3.3.1. 通用壓縮

我們現在能見到的,壓縮解壓代價相對可控的通用壓縮演算法,基本都是基於Abraham Lempel與Jacob Ziv在1977/1978論文中發表的LZ77/LZ78演算法變種得來,如LZW,LZ4,Snappy,ZSTD等。LZ系的通用壓縮演算法本質是一種在位元組流上的基於滑動視窗的字典壓縮演算法。演算法會動態的在最近一段資料流中尋找重複出現的位元組片段,然後進行壓縮。

1678242695

OB在Micro Block粒度是支援選擇通用壓縮演算法來對整個Micro Block進行通用壓縮的,使用者可以透過表的屬性來進行設定。通用壓縮雖然也能帶來一定的資料壓縮比,但是這是有效能損失的。OB的Micro Block在壓縮前是16KB,是一次讀IO讀取的資料的大小。當OB的Micro Block被通用壓縮之後,OB一次IO的大小仍然是16KB。壓縮後的資料還需要解壓之後才能使用。即是是當前最快的通用壓縮演算法LZ4,其解壓速度也只有約5GB/S,只有記憶體資料複製(memcpy)的35%左右。

Compressor Ratio Compression Decompression
memcpy 1.000 13700 MB/s 13700 MB/s
LZ4 default (v1.9.0) 2.101 780 MB/s 4970 MB/s
LZO 2.09 2.108 670 MB/s 860 MB/s
Snappy 1.1.4 2.091 565 MB/s 1950 MB/s
Zstandard 1.4.0 -1 2.883 515 MB/s 1380 MB/s
LZ4 HC -9 (v1.9.0) 2.721 41 MB/s 4900 MB/s

況且,通用的資料壓縮演算法並不是為資料庫專門設計的,這些壓縮演算法將輸入看成是一個連續的位元組流,並不對資料的pattern做任何先驗假設。但實際上資料庫中存放的是結構化資料,是有明確的schema的,每行資料每個欄位都有明確的型別和大小。利用這些資訊,採用一定的資料編碼技術,就可以實現比直接使用通用壓縮演算法好得多的壓縮效果。這些資料編碼技術,不但能提高資料的壓縮比,還能基本不降低資料的查詢效率。某些查詢情況下,甚至還能加速查詢。

3.3.2. 編碼壓縮

OB中的Micro Block正是在列存的基礎上,結合了資料schema,進行了一系列的高效的資料編碼壓縮。其主要的資料編碼壓縮演算法有以下幾類。

  • 字典編碼:所有資料編碼技術中最常用、也是最有效的方法。字典編碼的思想很簡單,就是把重複性較高的資料進行去重,把去重後的資料建立字典,而把原來存放資料的地方存成指向特定字典下標的引用。資料的訪問邏輯是非常直接的,沒有解碼過程。特別要注意,字典中的各資料是按型別序排序的,這樣做有兩個好處:一是有序的資料更有利於壓縮;二是有序的資料意味著我們在做謂詞計算時,可以將謂詞邏輯直接下壓到字典,並透過二分邏輯完成快速迭代,這點在我們支援複雜計算時,將發揮非常重要的作用。
  • RLE編碼:適用於連續相等的資料,比如形如1,1,1,2,2,...之類的,我們可以把這些連續相等的資料去重,並只保留其起始行號,RLE編碼在資料庫中主要用於處理有序的資料(比如索引的字首)。
  • 常量編碼:針對近似常量化的資料,編碼識別一個最常見的資料作為常量,而只記錄所有不等於這個常量的異常值和它們的行號,常量編碼在實際業務資料中非常有用,後者往往存在著一些業務增加、但是並不使用的欄位,這些欄位同樣佔據了空間,並呈現出常量化(預設值)的資料分佈。
  • 差值編碼(數值欄位):適用於在一個小值域範圍內分佈的整數型資料,透過計算區間內的MIN、MAX,將資料減去MIN後,用更小的位寬進行編碼。比較常見的資料是時間型別,連續生成的時間型別資料通常有非常好的區域性性(比如差值在幾秒鐘以內)。
  • 差值編碼(字元欄位):適用於定長的、有確定業務規則的字元型資料,透過構造公共串,將資料中只記錄差異部分。這類資料在業務中非常常見,通常用於主鍵,比如訂單號、使用者賬號等。
  • 字首編碼:適用於字首相同的字元型資料,透過構造公共字首,將資料中只記錄差異的字尾。其它一些資料庫,比如SQL Server、HBase中也經常使用,這類資料在業務中也很常見,比如部分主鍵。前面提到的資料編碼都是列內編碼,即只考慮同一列內各欄位的資料相似性,除此之外,OceanBase還實現了一組列間的資料編碼,也就是考慮同一行的不同列間資料的相似性。
  • 列間編碼(數值欄位):適用於兩列近似相等的整數型資料,這樣,其中的一列只需要儲存和另一列的差異值。這種列間相等的資料編碼有它很強的適用場景,比如我們知道業務在資料表中通常都會記錄生成和最近一次更新的時間戳,如果某些流水資料從不會發生更新,那麼這兩個欄位就存在很強的列間相等關係。
  • 列間編碼(字元欄位):適用於兩列存在子串關係的字元型資料,也就是一列是另一列的子串(包括字首、字尾、相等關係)。實際業務中許多長字元型欄位並不是由使用者隨意生成的,而是基於一定的業務規則,將多個基礎欄位拼接而成,這時候這些基礎欄位就和前者存在子串關係。

1678242781

OB所採用的這些編碼壓縮演算法,解碼過程非常簡單,解碼特定一行的資料,並不需要依賴其它行,這種近似O(1)複雜度的設計提高了資料投影的效能,使得其能夠被用於線上系統。

雖然這些編碼壓縮演算法解碼簡單,但是編碼過程卻並不那麼trivial。儘管OB知道每張表每一列資料的屬性,但是資料的分佈OB卻並沒那麼的瞭解,如某一列雖然是64位整數,但是OB事前並不一定知道它所有值都是一個有限的列舉型別,只有等到需要Compact壓縮一個Micro Block的時候才能知道。為了減輕DBA的心智負擔,OB並不希望把每一列的編碼壓縮演算法都由DBA來指定,而希望是OB自己根據當前資料的分佈來智慧選擇。這就涉及到一個比較大的問題,如何智慧的為每一列來選擇最優的編碼壓縮演算法。

OB的做法是如果上一個Micro Block某一列透過計算選擇了某種編碼演算法,那麼如果資料分佈穩定的話,下一個Micro Block的相同列大機率最優的編碼演算法跟上一個Block一樣。OB會優先嚐試透過這個編碼演算法對列進行壓縮,如果壓縮後的結果(壓縮比)證明這個選擇正確,那麼就會採用這個編碼演算法來壓縮當前列;否則會退回重新選擇合適的編碼演算法。

OB的公開資料表示,這塊還有較大的最佳化空間。而且業內目前也有一些新穎的想法,如透過機器學習來預測資料的最優編碼壓縮演算法。相信後續OB的新版本中會有持續改進和提升。

3.4. TiDB資料壓縮實現

TiDB的儲存層TiKV是基礎RocksDB的,前文我們也提到,RocksDB是在LevelDB的基礎上改進來的,而原始的LevelDB只實現了基於Block的整體通用壓縮,並未在Block內採用列存及資料編碼壓縮。因此採用RocksDB作為底層儲存的TiDB,也未使用Block內的列存及資料編碼壓縮。

TiDB為了應對OLAP類需求,專門另外涉及並實現了一套儲存格式,採用了列存的方式,此處不展開討論,有興趣的同學可以參考文件(PS: TiDB不愧是開源標杆,文件非常完善,隨便翻翻也能學習到不少東西)。部署時,需要部署專門的從庫來接收主庫的資料同步,然後改為列存儲存以應對OLAP查詢需求。

1678242810

4. 快取策略

由於很多資料儲存於 SSTable,為了加速查詢,OB需要對資料進行快取。OceanBase 資料庫並不需要對快取的大小進行設定,類似於 Linux 對於 page cache 的控制策略,OceanBase 資料庫會盡量使用租戶的記憶體,直到租戶的記憶體達到一定閾值後,才會觸發對 Cache 的淘汰。同時 OceanBase 資料庫也是一個多租戶系統,對於每一個租戶都會有各自的 Cache,但 OceanBase 資料庫會對所有租戶的快取進行統一管理。

4.1. Row Cache

傳統關係型資料庫並不對單獨的某行進行快取,OB基於LSM的儲存引擎卻實現了行級快取。OB的Row Cache 快取具體的資料行,在進行 Get/MultiGet 查詢時,可能會將對應查到的資料行放入 Row Cache,這樣在進行熱點行的查詢時,就可以極大地提升查詢效能。

4.2. Block Cache

傳統的關聯式資料庫中,快取是以IO頁面為單位進行快取的。在LSM為儲存引擎的資料庫中,IO頁面為單位的快取策略也需要隨著LSM結構來進行適配。我們之前提到過,OB的IO讀的最小單位是微塊,OB的Block Cache是以微塊(Micro Block)為單位的,其快取的資料是解壓縮後的微塊資料,大小是變長的。

另外,資料查詢的時候,如果沒有在Row Cache或者MemTable中查詢到資料話,我們需要去Block Cache或者磁碟中查詢資料。查詢資料的第一步是需要先確定資料在哪個微塊,這也需要我們將一部分微塊的索引快取在記憶體中。

OB的Block Index Cache即快取微塊的索引,類似於 BTree 的中間層,在資料結構上和 Block Cache 有一些區別,由於中間層通常不大,Block Index Cache 的命中率通常都比較高。

4.3. Filter Cache

LSM樹是基於排序的的儲存結構,這類結構通常在進行點查詢的時候,對不存在的資料的查詢和確認是不高效的。不存在的資料,通常我們在記憶體中也無法快取,記憶體查詢也就無法命中,只有透過IO去讀取資料所在範圍的SSTable(微塊)來確認資料是否存在。這樣對於資料的存在性查詢將變得非常低效。因此各種LSM樹的實現中都引入了Blook Filter來進行存在性過濾,OB也在宏塊(Macro Block)層引入了Bloom Filter用於資料存在性過濾。

1678242835

Bloom Filter是一種只能插入元素,無法刪除元素的資料結構。在傳統的關聯式資料庫中,IO頁面(Page)隨時可能會被應用修改,一行資料隨時都可能被刪除,因此無法高效的使用Bloom Filter來作為空資料過濾器。而基於LSM樹儲存引擎則不同,其磁碟上的資料,在下一次合併(Compaction)之前,都是不可變的。Compaction間隔時間通常都是以小時為單位,這種場景非常適合Bloom Filter的使用。Bloom Filter也是一種可以非常容易透過空間來調整查詢錯誤率的資料結構,應用可以輕易的透過調整每個元素所佔用的空間(Row per bits),來調整查詢的錯誤率(即資料不在SSTable中,但是透過Bloom Filter查詢被確認存在的機率)。

OB中的Bloom Filter的構建策略是按需構建,當一個宏塊上的空查次數超過某個閾值時,就會自動構建 Bloom Filter,並將 Bloom Filter 放入 Cache。

5. 合併策略

前面我們已經講過合併是LSM樹類儲存系統中最複雜,也是對系統效能/穩定性影響最大的一個過程。因此,OB實現的LSM樹在資料合併上也是進行了非常細緻的涉及,提供了多種合併策略,力求合併過程平滑可控。OceanBase的合併策略,根據表上是否有DDL操作,如加列,減列,修改壓縮演算法等,會才去不同的策略。當表上無DDL操作時,OceanBase主要採取增量合併策略;反之則會採用漸進合併或者全量合併策略。

5.1. 增量合併

增量合併的情況下,我們知道表的Schema沒有修改,因此可以在合併的時候,在不同粒度上看資料是否可以重用。OB的Macro Block劃分為2M一個,這個是一個相對較小的尺寸。當表相對較大的時候,如一張100G的表,系統裡會有102400個Macro Block。資料更新的時候,這些Macro Block中很多有可能都沒有被修改過,因此可以直接重用,這種方式稱之為增量合併。

相對於全量合併的把所有的宏塊的重寫一邊而言,增量合併只重寫發生了修改的宏塊。增量合併極大地減少了合併的工作量,也是 OceanBase 資料庫目前預設的合併演算法。

更進一步地,對於宏塊內部的微塊,很多情況下也並不是所有的微塊都會被修改。當發現宏塊有行被修改過時,在處理每一個微塊時,會先判斷這個微塊是否有行被修改過,如果沒有,只需要把這個微塊的資料直接複製到新的宏塊上,這樣沒被修改過的微塊就省去了解析行、選擇編碼規則、對行進行編碼以及計算列 Checksum 等操作。微塊級增量合併進一步減少了合併的時間。

5.2. 漸進合併

在執行某些 DDL 操作時,例如執行表的加列、減列、修改壓縮演算法等操作後,資料需要完全重寫一遍才能在磁碟上生效。OceanBase 資料庫並不會立即對資料執行重寫操作,而是將重寫動作延遲到合併時進行。基於增量合併的方式,無法完成對未修改資料的重寫,為此 OceanBase 資料庫引入了“漸進合併”,即把資料的重寫分散到多次合併中去做,在一次合併中只進行部分資料的重寫。

使用者可以透過引數指定漸進合併的輪次,如:

ALTER TABLE mytest SET progressive_merge_num=60;

表示將mytest表的漸進輪次設定為60,在DDL操作完成之後的60次漸進合併過程中,每一次合併會重寫60分之一的資料,在60輪合併過後,資料就被整體重寫了一遍。

當未對錶的progressive_merge_num進行設定時,其預設值為0,目前語義為在執行需要重寫資料的DDL操作之後,做漸進輪次為100的漸進合併。當表的progressive_merge_num被設定為1時,表示強制走全量合併。

5.3. 全量合併

OB的全量合併和 HBase 與 Rocksdb 的 Major Compaction 過程是類似的。顧名思義,在全量合併過程中,會把當前的基線資料都讀取出來,和增量資料合併後,再寫到磁碟上去作為新的基線資料。在這個過程中,會把所有資料都重寫一遍。全量合併會極大的耗費磁碟 IO 和空間,如非必要或者 DBA 強制指定,OceanBase 資料庫一般不會主動做全量合併。

OceanBase 資料庫發起的全量合併一般發生在列型別修改等 DDL 操作之後。DDL 變更是實時生效的,不阻塞讀寫,也不會影響到多副本間的 Paxos 同步,將對儲存資料的變更延後到合併的時候來做,這時就需要將所有資料重寫一遍。

5.4. 輪轉合併

一般來說合並會在業務低峰期進行,但並不是所有業務都有業務低峰期。在合併期間,會消耗比較多的 CPU 和 IO,此時如果有大量業務請求,勢必會對業務造成影響。為了規避合併對業務的影響。藉助 OceanBase 資料庫的多副本分散式架構,引入了輪轉合併的機制。

一般配置下,OceanBase 資料庫會同時有 3 個資料副本,當一個資料副本在進行合併時,可以將這個副本上的查詢流量切到其他沒在合併的叢集上面,這樣業務的查詢就不受每日合併的影響。等這個副本合併完成後,再將查詢流量切回來,繼續做其他副本的合併,這一機制稱之為輪轉合併。

為了避免流量切過去後,Cache 較冷造成的 RT 波動,在流量切換之前,OceanBase 資料庫還會做 Cache 的預熱,透過引數可以控制預熱的時間。

6. 總結

到此,《資料庫儲存與索引技術》系列的內容就完結了。儲存和索引技術作為資料庫底層的重要基礎,掌握其相關知識有助於我們進一步提高對資料庫底層原理的認識。希望透過本系列三篇文章的介紹,能夠讓大家對資料庫底層技術以及目前國內主流的分散式資料庫底層實現有所瞭解,在大家以後的工作中能夠有所幫助!

參考文獻

1. Oceanbase文件系列

• OB資料庫儲存架構:

• OB資料儲存管理:

• OB資料庫壓縮特性:

• OB儲存引擎詳解:

• OB部落格文章全系列:

2. 其他

• LZ4 Benchmark :


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69909943/viewspace-2940069/,如需轉載,請註明出處,否則將追究法律責任。

相關文章