- 原文地址:Algorithms Behind Modern Storage Systems
- 原文作者:Alex Petrov
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:LeopPro
- 校對者:zephyrJS FesonX
讀優化 B-Tree 和寫優化 LSM-Tree 的不同用途
作者:Alex Petrov
隨著應用程式處理的資料量不斷增長,擴充套件儲存變得愈發具有挑戰性。每個資料庫系統都有自己的方案。為了從這些方案中做出正確的選擇,瞭解它們是至關重要的。
每個應用程式在讀寫負載平衡、一致性、延遲和訪問模式方面各不相同。熟悉資料庫和底層儲存能幫助你進行架構決策、解釋系統行為、排除故障以及根據具體情況調優。
優化一個系統不可能做到面面俱到。我們當然希望有一個資料結構既能保證最佳的讀寫效能,又不需要任何儲存開銷,但顯然,這是不存在的。
本文深入討論了大多數現代資料庫中使用的兩種儲存系統設計 —— 讀優化 B-Tree [1] 和寫優化 LSM(log-structured merge)-Tree [5] —— 並描述了它們的用例和優缺權衡。
B-Tree
B-Tree 是一種流行的讀優化索引資料結構,是二叉樹的泛化。它有許多變種,並且被用於多種資料庫(包括 MySQL InnoDB [4]、PostgreSQL [7])甚至檔案系統(HFS+ [8]、HTrees ext4 [9])。B-Tree 中的 B 代表原始資料結構的作者 Bayer,或是他當時就職的公司 Boeing。
在搜尋二叉樹中,每個節點都有兩個孩子(稱為左右孩子)。左子樹的節點值小於當前節點值,右子樹反之。為了保持樹的深度最小,搜尋二叉樹必須是平衡的:當隨機順序的值被新增到樹中時,如果不加調整,終會導致樹的傾斜。
一種平衡二叉樹的方法是所謂的旋轉:重新排列節點,將較深子樹的父節點向下推到其子節點下方,並將該子節點拉上來,將其放在原父節點的位置。圖 1 是平衡二叉樹中的旋轉示例。在左側新增節點 2 後,二叉樹失去平衡。為了使該樹平衡,將其以節點 3 為軸旋轉(樹圍繞它旋轉)。然後節點 5(旋轉前是根節點和節點 3 的父節點)成為其子節點。旋轉完成後,左側子樹的深度減少 1,右側子樹的深度增加 1。樹的最大深度已經減小。
二叉樹是最有用的記憶體資料結構。然而由於平衡(保持所有子樹的深度最小)和低出度(每個節點最多兩個子節點),它們在磁碟上水土不服。B-Tree 允許每個節點儲存兩個以上的指標,並通過將節點大小與頁面大小(例如,4 KB)進行匹配來與塊裝置協同工作。今天的一些實現將使用更大的節點大小,跨越多個頁面。
B-Tree 有以下幾個性質:
• 有序。這允許順序掃描並簡化查詢。
• 自平衡。在插入和刪除時不需要平衡樹:當 B-Tree 節點已滿時,它被分成兩部分,並且當相鄰節點的利用率低於某個閾值時,合併這兩個節點。這也意味著各葉子節點與根節點的距離相等,並且在查詢過程中定位的步數是相同的。
• 對數級查詢時間複雜度。查詢時間是非常重要的,這使得 B-Tree 成為資料庫索引的理想選擇。
• 易變。插入、更新、刪除(包括因此導致的拆分和合並)過程在磁碟上進行。為了使就地更新成為可能,需要一定的空間開銷。B-Tree 可以作為聚集索引,實際資料儲存在葉子節點上,也可以作為非聚集索引,稱為一個堆檔案。
本文討論的 B+Tree [3] 是一種經常用於資料庫儲存的 B-Tree 現代變種。B+Tree 與原始 B-Tree [1] 的不同之處在於:(1)它採用額外連結的葉節點儲存值;(2)值不能儲存在內部節點上。
剖析 B-Tree
我們先來仔細看看 B-Tree 的結構,如圖 2 所示。B-Tree 的節點有幾種型別:根節點,內部節點和葉子節點。根節點(頂部)是沒有雙親的節點(即,它不是任何節點的子節點)。內部節點(中間)有雙親和孩子節點;他們將根節點和葉子節點連線起來。葉子節點(底部)持有資料並且沒有孩子節點。圖 2 描繪了分支因子為 4(4 個指標,內部節點中有 3 個鍵,葉上有 4 個鍵/值對)的 B-Tree。
B-Tree 的特性如下:
• 分支因子 —— 指向子節點的指標數(N)。除指標外,根節點和內部節點還持有 N-1 個鍵。
• 利用率 —— 節點當前持有的指向子節點的指標數量與可用最大值之比。例如,若某樹分支因子是 N,且其中某節點當前持有 N/2 個指標,則該節點利用率為 50%。
• 高度 —— B-Tree 的數量級,表示在查詢過程中必須經過多少指標。
樹中的每個非葉節點最多可持有 N 個鍵(索引條目),這些鍵將樹分為 N+1 個子樹,這些子樹可以通過相應的指標定位。項 Ki 中的指標 i 指向某子樹,該子樹中包含所有 Ki-1 <= K目標 < Ki(其中 K 是一組鍵)的索引項。首尾指標是特殊的,它們指向的子樹中所有的項都小於等於最左子節點的 K0 或大於最右子節點的 KN-1。葉子節點同時持有其同級前後節點的指標,形成兄弟節點間的雙向連結串列。所有節點中的鍵總是有序的。
查詢
進行查詢時,將從根節點開始搜尋,並經過內部節點遞迴向下到葉子節點層。在每層中,通過指向子節點的指標將搜尋範圍縮小到某子樹(包含搜尋目標值的子樹)。圖 3 展示了 B-Tree 的一次從根到葉的搜尋過程,指標在兩個鍵之間,其中一個大於(或等於)搜尋目標,另一個小於搜尋目標。進行點查詢時,搜尋將在定位到葉子節點後完成。進行範圍掃描時,遍歷所找到的葉子節點的鍵和值,然後遍歷範圍內的兄弟葉子節點。
在複雜度方面,B-Tree 保證查詢的時間複雜度為 log(n),因為查詢一個節點中的鍵使用二分查詢,如圖 4 所示。二進位制搜尋可以通俗的解釋為在字典中查詢以某字母開頭的單詞,字典中所有單詞都按字母順序排序。首先你翻開正好在字典中間的一頁。如果要查詢的單詞字母順序小於(在前面)當前頁,你繼續在字典的左半邊查詢;否則就繼續在右半邊查詢。你繼續像這樣將剩餘的頁碼範圍分為一半,選擇一邊,直到找到期望的字母。每一步都將搜尋範圍減半,因此查詢的時間複雜度為對數級。 B-Tree 節點上的鍵是有序的,且使用二分查詢演算法進行匹配,因此 B-Tree 的搜尋複雜度是對數級的。這也說明了保持樹的高利用率和統一訪問的重要性。
插入、更新、刪除
進行插入時,第一步是定位目標葉子節點。此過程使用前序搜尋演算法。在定位目標葉子節點後,鍵和值將被新增至該節點。如果該節點沒有足夠的可用空間,這種情況稱為溢位,則將葉子節點分割成兩部分。這是通過分配一個新的葉子節點,將一半元素移動到新節點並將一個指向這個新節點的指標新增到父節點來完成的。如果父節點沒有足夠的空間,則也會在父節點上進行分割。操作一直持續到根節點為止。當根節點溢位時,其內容在新分配的節點之間被分割,根節點本身被覆蓋以避免重定位。這也意味著樹(及其高度)總是通過分裂根節點而增長。
LSM-Tree
結構化日誌合併樹是一個不可變的基於磁碟的寫優化資料結構。它適用於寫入比查詢操作更頻繁的場景。LSM-Tree 已經獲得了更多的關注,因為它可以避免隨機插入,更新和刪除。
剖析 LSM-Tree
為了允許連續寫入,LSM-Tree 在記憶體中的表(通常使用支援查詢的時間複雜度為對數級的資料結構,例如二叉搜尋樹或跳躍表)中批量寫入和更新,當其大小達到閾值時將它寫在磁碟上(這個操作稱為重新整理)。檢索資料時需要搜尋樹所有磁碟中的部分,檢查記憶體中的表,合併它們的內容,然後再返回結果。圖 5 展示了 LSM-Tree 的結構:用於寫入的基於記憶體的表。只要記憶體表體積達到一定程度,記憶體表就會被寫入磁碟。進行讀取時,同時讀取磁碟和記憶體表,通過一個合併操作來整合資料。
有序序列表
因為 SSTable(有序序列表)的簡單性(易於寫入,搜尋和讀取)與合併效能(合併期間,掃描源 SSTable,合併結果的寫入是順序的),多數現代的 LSM-Tree 實現(例如 RocksDB 和 Apache Cassandra)都選用 SSTable 作為硬碟表。
SSTable 是一種基於硬碟的有序不可變的資料結構。從結構上來看,SSTable 可以分為兩部分:資料塊和索引塊,如圖 6 所示。資料塊包含以鍵為順序寫入的唯一鍵值對。索引塊包含對映到資料塊指標的鍵,指標指向實際記錄的位置。為了快速搜尋,索引一般使用優化的結構實現,例如 B-Tree 或用於點查詢的雜湊表。SSTable 中的每一個值都有一個時間戳與之對應。時間戳記錄了插入、更新(這兩者一般不做區分)和刪除時間。
SSTable 具有以下優點:
• 通過查詢主鍵索引可以實現快速的點查詢(例如,通過鍵尋找值)。
• 只需要順序讀取資料塊上的鍵值對就可以實現掃描(例如,遍歷制定範圍內的鍵值對)。
SSTable 代表一段時間內所有資料庫操作的快照,因為 SSTable 是通過對記憶體表的重新整理操作建立的,該表充當此時段內對資料庫狀態的緩衝區。
查詢
檢索資料需要搜尋硬碟上的所有 SSTable,檢查記憶體表,並且合併它們的內容後返回結果。要搜尋的資料可以儲存在多個 SSTable 中,因此合併步驟是必須的。
合併步驟也是確保刪除和更新正常工作所必需的。在 LSM-Tree 中,通過插入佔位符(通常稱為墓碑)來指定哪個鍵被標記為刪除。同樣的,更新操作只是提交一個帶較晚時間戳的記錄。在讀取期間,被標記刪除的記錄被跳過,不會返回給客戶端。更新操作與之類似:在具有相同鍵的兩個記錄中,只返回具有較晚時間戳的記錄。圖 7 展示了一次合併操作,用於對在不同表中儲存的同一個鍵的資料進行整合:如圖,Alex 記錄中時間戳是 100,隨後更新了新的電話,時間戳為 200;John 記錄被刪除。另外兩項沒有改變,因為它們沒有被覆蓋。
為了減少搜尋 SSTable ,防止為了查詢某個鍵而搜尋每個 SSTable,許多儲存系統採用一個被稱為布隆過濾器 [10] 的資料結構。這是一個概率資料結構,可用於測試某個元素是否屬於某集合。它有可能產生錯誤的肯定(即,判斷元素是集合的成員,但實際上並不是),但不能產生錯誤的否定(即,如果返回否定結果,則元素一定不是集合的成員)。換句話說,布隆過濾器用於判斷鍵“可能在 SSTable 中”或“絕對不在 SSTable 中”。在搜尋過程中,將會跳過布隆過濾器返回否定結果的 SSTable。
LSM-Tree 的維護
由於 SSTable 是不可變的,因此它們會按順序寫入,並且不存在用於修改的預留空白空間。這就意味著插入、更新或刪除操作將需要重寫整個檔案。所有修改資料庫狀態的操作都在記憶體表中“批處理”。隨著時間的推移,磁碟表的數量將增加(同一個鍵的資料位於幾個不同檔案,同一記錄有多個不同的版本,被刪除的冗餘記錄),讀取操作的開銷將變得越來越大。
為了降低讀取開銷,整合被刪除記錄佔用的空間並減少磁碟表的數量,LSM-Tree 需要一個壓縮操作,從磁碟讀取完整的 SSTable 併合並它們。由於 SSTable 是以鍵排序的,因此其壓縮工作和歸併排序類似,是非常高效的操作:從多個源有序序列中讀取記錄,進行合併後的輸出馬上追加到結果檔案中,則結果檔案也是有序的。歸併排序的一個優點是,即使合併記憶體吃不消的大檔案,它依舊可以高效地工作。結果表保留了原始 SSTable 的順序。
在此過程中,被合併的 SSTable 被丟棄並替換為其“壓縮”後的版本,如圖 8 所示。壓縮多個 SSTable 並將它們合併為一個。某些資料庫系統在邏輯層面上按大小把不同的表分為不同級別,分組到相同的“級別”,並在特定級別的表足夠多時開始合併操作。壓縮後,SSTable 的數量減少,提高查詢效率。
原子性與永續性
為了減少 I/O 操作並使它們順序執行,無論是 B-Tree 還是 LSM-Tree 都在實際更新之前,先在記憶體中進行批量操作。這意味著,在故障情況時,資料完整性、原子性(將一系列操作賦予原子性,將它們視為一個操作,要麼全部執行要麼全不執行)、永續性(當程式崩潰或電源失效時,可以確保資料已經到達永續性儲存裝置)得不到保證。
為了解決這個問題,大多數現代儲存系統採用 WAL(預寫日誌)。WAL 的核心思想是,所有資料庫狀態改變都先持久化進硬碟中的只追加日誌中。如果程式在工作中崩潰,將會重映日誌以確保沒有資料丟失且所有更改都滿足原子性。
在 B-Tree 中,使用 WAL 可以理解為僅在寫入操作被記錄後才將其寫入資料檔案。通常,B-Tree 儲存系統的日誌尺寸相對較小:只要將更改應用於持久儲存,它們就可以被棄用。WAL 還可以作為執行時操作的備份:任何未應用於資料頁的更改都可以根據日誌記錄重做。
在 LSM-Tree 中,WAL 用於儲存處於記憶體表但尚未完全重新整理到磁碟上的更改。只要記憶體表被重新整理完畢並置換,便可以在新建立的 SSTable 中進行讀取操作,則 WAL 中從記憶體表重新整理到硬碟上的那部分更改就可以丟棄了。
總結
B-Tree 和 LSM-Tree 資料結構最大的差異之一是它們優化的目的以及優化的效果。
我們來對比一下 B-Tree 和 LSM-Tree 之間的特性。總的來說,B-Tree 具有以下特性:
• 它是可變的,它允許通過一些空間開銷和更多的寫入路徑來進行就地更新,因而它不需要檔案重寫或多源合併。
• 它是讀優化的,這意味著它不需要從多個源資料中讀取(也不需要合併),因而簡化了讀取路徑。
• 寫操作可能引起級聯節點分裂,這使得寫操作開銷較高。
• 它針對分頁環境(塊儲存)進行了優化,杜絕了位元組定位操作。
• 碎片化, 由頻繁更新造成的碎片化可能需要額外的維護和塊重寫。然而對 B-Tree 的維護一般要比 LSM-Tree 要少。
• 併發訪問讀寫隔離,這涉及鎖存器與鎖鏈。
LSM-Tree 具有以下特性:
• 它是不可變的。SSTable 一旦被寫入硬碟就不會再改變。壓縮操作被用於整合佔用空間,刪除條目,合併在不同資料檔案中的同鍵資料。作為壓縮操作的一部分,在成功合併後,源 SSTable 將被棄用並刪除。這種不可變性給我們帶來了另一個有用的特性,重新整理後的表可以被併發訪問。
• 它是寫優化的,這意味著寫入操作將進入緩衝,隨後順序重新整理到硬碟上,可能支援基於硬碟的空間區域性性。
• 讀取操作可能需要訪問多個資料來源,因為在不同時間寫入的同一個鍵的資料有可能位於不同的資料檔案中。必須經過合併過程才能將記錄返回給客戶端。
• 需要維護 / 壓縮,因為緩衝中的寫入操作被重新整理到硬碟上。
評估儲存系統
開發儲存系統總要面對類似的挑戰,考慮類似的因素。決定優化方向會對結果產生很大影響。你可以在寫入過程中花費更多時間來佈局結構以實現更高效的讀取,為就地更新預留空間,也可以緩衝資料確保順序寫入以提高寫入速度。但是,一次完成這一切是不可能的。理想中的儲存系統應該具有最低的讀取成本,最低的寫入成本,並且沒有額外開銷。但實際上,資料結構只能在多個因素之間權衡。理解這些取捨是重要的。
來自哈佛 DASlab(資料系統實驗室)的研究人員總結了資料庫系統優化方向的關鍵三點:讀取開銷、更新開銷和記憶體開銷(或簡稱為 RUM)。對於資料結構、訪問方法、甚至適用於某些工作負載的選擇應該瞭解哪些引數對你的用例最為重要,因為演算法是針對特定用例量身定製的。
RUM 假說 [2] 為上述的兩種開銷設定了上限,同時為第三種設定了下限。例如,B-Tree 以提高寫入開銷、預留空間(同時也造成了記憶體開銷)為代價進行讀優化。LSM-Tree 以讀取時必須進行多硬碟表訪問的高讀取開銷換取低寫入開銷。在處於競爭三角形的三個引數中,一方面的改進可能就意味著另一方面的讓步。圖 9 對 RUM 假說進行了說明。
B-Tree 優化讀取效能:索引的佈局方式可以最小化遍歷樹的磁碟訪問需求。通過訪問一個索引檔案就可以定位資料。這是通過持續更新索引檔案來實現的,但這也增加了由於節點拆分和合並,重定位以及碎片、不平衡相關的維護造成的額外寫入開銷。為了平穩更新成本並減少分割次數,B-Tree 在所有級別的節點上都預留有額外的空間。這有助於在節點飽和之前延遲寫入開銷的增長。簡而言之,B-Tree 犧牲更新和記憶體效能以獲得更好的讀取效能。
LSM-Tree 優化寫入效能。無論是更新還是刪除都需要在磁碟上定位資料(B-Tree 也一樣),並且它通過在記憶體表中快取所有插入,更新和刪除操作來保證順序寫入。這是以較高的維護成本和壓縮需求(這是唯一的緩解不斷增長的讀取開銷和減少磁碟表的數量的方式)和更高的讀取成本(因為資料必須從多個源讀取併合並)為代價的。同時,LSM-Tree 通過不保留任何預留空間來減少記憶體開銷(不同於 B-Tree 節點,其平均利用率為 70%,包含就地更新所需的開銷),因為更高的利用率和最終檔案的不變性,LSM-Tree 支援塊壓縮。簡而言之,LSM-Tree 犧牲讀取效能,提高維護成本來獲得更好的寫入效能和更低的記憶體佔用。
有的資料結構可針對每個期望的屬性進行優化。使用自適應資料結構可以以更高維護成本獲得更好的讀取效能。新增有助於遍歷的後設資料(如分散層疊)將會影響寫入時間並佔用更多空間,但可以提高讀取效能。使用壓縮優化記憶體使用率(例如,Gorilla 壓縮 [6] 、delta 編碼等諸多演算法)會增加一些開銷,用於在寫入時壓縮資料並在讀取時解壓縮資料。有時候,你可以犧牲功能來提高效率。例如,堆檔案和雜湊索引由於檔案格式簡單,可以保證很好的效能和較小的空間開銷,而作為代價,它們不支援除點查詢以外的其他功能。你還可以通過使用近似資料結構(如布隆過濾器、HyperLogLog、Count-Min sketch 等)來為了空間與效率犧牲精度。
三種可變引數 —— 讀取,更新和記憶體開銷 —— 可以幫助你評估資料庫並深入瞭解最適合的工作負載。它們都非常直觀,將儲存系統按其分類很容易,猜測它是如何執行的,然後通過大量測試驗證你的假設。
當然,評估儲存系統時還有一些其他重要因素需要考慮,例如維護開銷,易用性,系統要求,頻繁增刪的適應性,訪問模式等。RUM 假說只是幫助發展直觀感覺並提供初始方向的一條經驗法則。瞭解你的工作部件是構建可擴充套件後端的第一步。
一些因素可能因實施而異,甚至兩個使用類似儲存設計原則的資料庫可能會有不同表現。資料庫是包含許多可插拔模組的複雜系統,是許多應用程式的重要組成部分。這些資訊將幫助你窺探資料庫的底層,並且瞭解底層資料結構和其內部行為之間的差異,從而決定哪個是最適合你的。
參考文獻
1. Comer, D. 1979. The ubiquitous B-tree. Computing Surveys 11(2); 121-137; citeseerx.ist.psu.edu/viewdoc/dow….
2. Data Systems Laboratory at Harvard. The RUM Conjecture; daslab.seas.harvard.edu/rum-conject….
3. Graefe, G. 2011. Modern B-tree techniques. Foundations and Trends in Databases 3(4): 203-402; citeseerx.ist.psu.edu/viewdoc/dow….
4. MySQL 5.7 Reference Manual. The physical structure of an InnoDB index; dev.mysql.com/doc/refman/….
5. O'Neil, P., Cheng, E., Gawlick, D., O'Neil, E. 1996. The log-structured merge-tree. Acta Informatica 33(4): 351-385; citeseerx.ist.psu.edu/viewdoc/dow….
6. Pelkonen, T., Franklin, S., Teller, J., Cavallaro, P., Huang, Q., Meza, J., Veeraraghavan, K. 2015. Gorilla: a fast, scalable, in-memory time series database. Proceedings of the VLDB Endowment 8(12): 1816-1827; www.vldb.org/pvldb/vol8/….
7. Suzuki, H. 2015-2018. The internals of PostreSQL; www.interdb.jp/pg/pgsql01.….
8. Apple HFS Plus Volume Format; developer.apple.com/legacy/libr…
9. Mathur, A., Cao, M., Bhattacharya, S., Dilger, A., Tomas, A., Vivier, L. (2007). The new ext4 filesystem: current status and future plans. Proceedings of the Linux Symposium. Ottawa, Canada: Red Hat.
10. Bloom, B. H. (1970), Space/time trade-offs in hash coding with allowable errors,Communications of the ACM, 13 (7): 422-426
相關文章
五分鐘法則:20 年後快閃記憶體將如何改寫遊戲規則
Goetz Graefe, Hewlett-Packard 實驗室
舊規則繼續發展,而快閃記憶體增加了兩條新規則。
queue.acm.org/detail.cfm?…
Disambiguating Databases
Rick Richardson
根據你的訪問模型構建資料庫。
queue.acm.org/detail.cfm?…
你做錯了!
Poul-Henning Kamp
你以為自己已經掌握了伺服器效能的藝術了麼?再想一想。
queue.acm.org/detail.cfm?…
Alex Petrov (coffeenco.de/, @ifesdjeen (GitHub) @ifesdjeen (Twitter)),一位 Apache Cassandra 貢獻者、儲存系統愛好者。在過去的幾年,他一直致力於資料庫,為各個公司建立分散式系統和資料處理管道。
本文英文原文 PDF 檔案:下載地址
Copyright © 2018 held by owner/author. Publication rights licensed to ACM.
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。