- 原文地址:Algorithms Behind Modern Storage Systems
- 原文做者:Alex Petrov
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:LeopPro
- 校對者:zephyrJS FesonX
隨着應用程序處理的數據量不斷增加,擴展存儲變得愈發具備挑戰性。每一個數據庫系統都有本身的方案。爲了從這些方案中作出正確的選擇,瞭解它們是相當重要的。html
每一個應用程序在讀寫負載平衡、一致性、延遲和訪問模式方面各不相同。熟悉數據庫和底層存儲能幫助你進行架構決策、解釋系統行爲、排除故障以及根據具體狀況調優。前端
優化一個系統不可能作到面面俱到。咱們固然但願有一個數據結構既能保證最佳的讀寫性能,又不須要任何存儲開銷,但顯然,這是不存在的。mysql
本文深刻討論了大多數現代數據庫中使用的兩種存儲系統設計 —— 讀優化 B-Tree [1] 和寫優化 LSM(log-structured merge)-Tree [5] —— 並描述了它們的用例和優缺權衡。android
B-Tree 是一種流行的讀優化索引數據結構,是二叉樹的泛化。它有許多變種,而且被用於多種數據庫(包括 MySQL InnoDB [4]、PostgreSQL [7])甚至文件系統(HFS+ [8]、HTrees ext4 [9])。B-Tree 中的 B 表明原始數據結構的做者 Bayer,或是他當時就任的公司 Boeing。ios
在搜索二叉樹中,每一個節點都有兩個孩子(稱爲左右孩子)。左子樹的節點值小於當前節點值,右子樹反之。爲了保持樹的深度最小,搜索二叉樹必須是平衡的:當隨機順序的值被添加到樹中時,若是不加調整,終會致使樹的傾斜。git
一種平衡二叉樹的方法是所謂的旋轉:從新排列節點,將較深子樹的父節點向下推到其子節點下方,並將該子節點拉上來,將其放在原父節點的位置。圖 1 是平衡二叉樹中的旋轉示例。在左側添加節點 2 後,二叉樹失去平衡。爲了使該樹平衡,將其以節點 3 爲軸旋轉(樹圍繞它旋轉)。而後節點 5(旋轉前是根節點和節點 3 的父節點)成爲其子節點。旋轉完成後,左側子樹的深度減小 1,右側子樹的深度增長 1。樹的最大深度已經減少。github
二叉樹是最有用的內存數據結構。然而因爲平衡(保持全部子樹的深度最小)和低出度(每一個節點最多兩個子節點),它們在磁盤上水土不服。B-Tree 容許每一個節點存儲兩個以上的指針,並經過將節點大小與頁面大小(例如,4 KB)進行匹配來與塊設備協同工做。今天的一些實現將使用更大的節點大小,跨越多個頁面。算法
B-Tree 有如下幾個性質:sql
• 有序。這容許順序掃描並簡化查找。數據庫
• 自平衡。在插入和刪除時不須要平衡樹:當 B-Tree 節點已滿時,它被分紅兩部分,而且當相鄰節點的利用率低於某個閾值時,合併這兩個節點。這也意味着各葉子節點與根節點的距離相等,而且在查找過程當中定位的步數是相同的。
• 對數級查找時間複雜度。查找時間是很是重要的,這使得 B-Tree 成爲數據庫索引的理想選擇。
• 易變。插入、更新、刪除(包括所以致使的拆分和合並)過程在磁盤上進行。爲了使就地更新成爲可能,須要必定的空間開銷。B-Tree 能夠做爲彙集索引,實際數據存儲在葉子節點上,也能夠做爲非彙集索引,稱爲一個堆文件。
本文討論的 B+Tree [3] 是一種常常用於數據庫存儲的 B-Tree 現代變種。B+Tree 與原始 B-Tree [1] 的不一樣之處在於:(1)它採用額外連接的葉節點存儲值;(2)值不能存儲在內部節點上。
咱們先來仔細看看 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 在內存中的表(一般使用支持查找的時間複雜度爲對數級的數據結構,例如二叉搜索樹或跳躍表)中批量寫入和更新,當其大小達到閾值時將它寫在磁盤上(這個操做稱爲刷新)。檢索數據時須要搜索樹全部磁盤中的部分,檢查內存中的表,合併它們的內容,而後再返回結果。圖 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。
因爲 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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。