外部存儲
數據庫管理系統DBMS
是現代應用中不可或缺的一部分,其中一個重要原因是其隱藏了外存管理的細節,並為應用層提供了高效、易用的數據檢索Retrieval
與持久化Persistence
功能。
外存具有容量大、成本低、斷電非易失等優點,但同時也存在尋址慢、訪問粒度粗的問題:
- 內存尋址速度快(ns 級),尋址單位小(byte)
- 外存尋址速度慢(ms 級),尋址單位大(≥4kb)
數據庫的讀寫性能取決於外存訪問效率,而優化外存訪問的手段有:
- 減少外存訪問次數:借助寫緩沖
Buffer
、讀緩存Cache
的方式,將熱點數據臨時存儲在內存,避免頻繁的外存訪問 - 避免隨機尋址:使用預寫日志
WAL
對寫操作進行優化,將隨機寫操作簡化為順序的追加操作 - 單次讀取盡可能多的數據:使用高密度的外存索引
Index
來組織數據,通過有序性提高檢索效率
預寫日志
預寫日志系統 WALWrite-Ahead Logging
是一種用於提高數據庫寫性能的常見手段,被廣泛應用於持久化數據庫中。
數據庫中的狀態可以分為兩部分:
- WAL 日志:所有對數據庫的變更都先寫入這個日志,並在事務提交時進行持久化,防止已提交數據丟失,已提交的日志數據會被定期清理
- DB 文件:包含所有已經交的數據、索引信息,數據長期存在不會消失
WAL 的核心思想是 日志先行 :
- 寫數據時,變更操作首先追加到 WAL 日志末尾,WAL 會將數據順序刷到磁盤(提交成功)。異步線程會消費 WAL 中的變更消息(類似於隊列),將應用變更到 DB 文件中並重置 WAL
- 讀數據時,需要同時讀取 WAL 與 DB 中的數據,並將兩者合並生成最新的記錄
由於追加 WAL 是順序的,可以將隨機的磁盤IO轉換為順序的磁盤IO,減少磁盤巡道時間,從而能夠更有效地提升了磁盤的吞吐量。
數據庫重啟過程中會檢查 WAL 日志,任何尚未附加到 DB 數據頁的記錄都將從日志記錄中重放,每次提交事務時不再需要(為了保證數據安全)把數據頁沖刷到磁盤,有效地提升了事務吞吐量。
WAL 只允許在尾部追加數據 Append-Only
,不允許修改日志記錄。這種不變性 Immutability
有利於並發控制:刪改數據只能通過追加新的日志實現,因此修改前無需對數據加鎖,直接在日志末尾追加新的記錄即可。
然而 WAL 的體積也不可能無限增長,系統需要周期性周期性的清理無用的日志記錄,減少文件碎片,釋放磁盤空間。
索引
索引是一種附加的數據結構,以犧牲空間和寫入速度為代價,換取更快的檢索速度。最常用的索引結構莫過於 Hash 與 Tree:
Hash
- 維護方便,單個 key 的隨機查找速度極快,一般都是常量級的
O(1)
- 無法支持范圍查找,隨着記錄的增長,哈希沖突率上升,導致查找速度下降
- 整個索引需要保證能夠放入內存,否則就無法發揮其速度優勢
Tree
- 支持范圍查找,查找速度穩定,二叉平衡樹可以保證
O(log2n)
- 維護成本較高,插入數據時需要重新平衡樹,每個節點的需要額外的指針存儲空間
- 大數據量的情況下查找性能比較穩定,具有多種變種算法可以適配各種應用場景
由於數據庫需要管理海量的數據,因此 Tree 便成為外存索引的不二之選。
下面介紹其中最具代表性兩類索引結構:B-Tree 與 LSM-Tree
B-Tree
最基礎的 Tree 莫過於二叉查找樹。其查找數據的方式,就是從根節點開始逐層向下遍歷,直到找到目標節點。但是當數據量比較大的時候,會有以下問題:
- 節點之間的地址不連續,每次在節點之間的跳轉訪問時,都要進行尋址,訪問效率不高
- 最壞情況下的訪問效率取決於樹的高度,當數據量大時,即便是平衡樹,其高度也很可觀
B-Tree 是一種用於處理海量數據的平衡多路查找樹,其主要改進是對二叉樹中間節點進行了合並,通過平衡算法和分叉因子 b
,可以將樹高度控制在logbn
的級別,對外存訪問更為友好:
- 每個節點包含盡可能多的數據,可以一次讀出大量的數據,減少對外存的訪問次數
- 有效地降低了整棵樹的高度,在大數據量的情況下能夠保證較少的訪問次數
這意味着:只需要很少的磁盤 IO,就能夠對大量的數據進行高效的查找操作。
B-Tree 在作為外存索引使用時:
- 根節點會常駐內存,其余節點存儲在磁盤上,從而能夠減少一次磁盤 IO
- 按照頁來組織數據,每個節點大小需對應一個完整的頁(磁盤IO的基本單位是物理塊
block
,操作系統使用邏輯頁page
管理應用程序的地址映射) - 為了保證數據的安全性,在對索引數據進行修改前要先寫 WAL,因此每次寫操作會造成至少兩次磁盤寫(忽略寫緩存)
- 寫入的 Key 如果是隨機或不連續的,可能會造成索引節點的多次分裂,影響寫入的效率(寫放大效應)
- 在多次修改、刪除操作之后,索引文件中會產生比較多的空洞,造成磁盤空間的浪費,並且會影響讀性能(需要定期重建索引)
B+Tree 是對 B-Tree 的進一步改進:將 Key 與 Value 進行分離,非葉節點只保存 Key,所有 Value 下沉到葉子節點。
每個中間節點可以容納更多的 Key,進一步提高了中間節點的密度,在相同的數據量下,樹的高度要比 B-Tree 更低。
LSM-Tree
LSM-Tree 的全稱是 Log-Structured Merge Tree
,相較於一種索引結構,其本質更接近於一整套完整的索引維護機制:
LSM-Tree 大致可以分為兩部分:
Memtable
: 常駐內存的 KV 查找樹(可用 SkipList 替代) + 無序的 WAL 文件SSTable (Sorted String Table)
: 一組存儲在磁盤的不可變文件(稀疏索引部分可選),存儲有序的鍵值對
寫入流程
1. 同步寫 Memtable
先將數據寫入 WAL 文件,然后修改內存中的 AVL,因此最優情況下,每次寫操作只有一次磁盤 I/O。
刪除操作並不會直接刪除磁盤中的內容,而是將刪除標記(tombstone)寫入 Memtable。當 Memtable 增大到一定程度后,則會轉換為 Immutable Memtable
並產生一個新的 Memtable 接受寫操作。
2. 異步寫 SSTable
后台會啟動一個合並線程,當 Immutable Memtable
達到一定數量,合並線程會將其寫入磁盤(Flush),生成 Level 0 的 SSTable 文件。
當 Level N 的 SSTable 文件數量到達閾值之后,會進行合並壓縮(Compaction)操作,在 Level N+1 生成新的 SSTable 文件。
SSTable 分為多層,單個文件的大小通常是上一層的 10 倍,每層可以同時包含多個 sst 文件,每個文件由多個 block 組成,其大小約為 32K,是磁盤 IO 的基本單位。
第 Level i (i > 0)
層的 SSTable 滿足:
- 第 i 層所有文件均由 i - 1 層的 SSTable 合並排序而來,可以通過設定閾值(文件個數...)來控制合並的行為
- 文件之間是有序的,且每個文件的 key 集合不會與其他文件有交集(Level 0 的 SSTable 除外)
讀取流程
首先中 Memtable 中進行查找,如果找不到,則按 Level 0、Level 1、... 的方式逐層向下遍歷.
一個 Key 可能同時存在於多層 SSTable 中,這種情況下以層數最小的記錄為准
為了提高熱點數據的讀取效率,提供了 sstable block cache 的功能,用於緩存讀取數據。
某些不存在的 Key 可能會導致較深的無用查找,通過使用 BloomFIlter
對 Key 進行過濾可以規避這一問題。
放大效應
- 寫放大效應:一次寫操作,實際所需的磁盤 IO 次數不止一次
- 讀放大效應:一次讀操作,實際所需的磁盤 IO 次數不止一次
對於讀寫負載較高的數據庫,性能瓶頸很有可能是磁盤的讀寫頻率。在這種情況下,讀寫放大會顯著影響性能:
在磁盤帶寬一定的情況下,放大效應越明顯,每次對數據庫的讀寫操作造成磁盤IO越多,每秒鍾能處理的數據庫操作次數越小
寫放大
Write | B-Tree | LSM-Tree |
---|---|---|
最優 |
|
|
最壞 |
|
|
LST-Tree 平均只需要寫一次磁盤,即寫 WAL, 在少數情況下,一次寫入也有可能造成多次寫磁盤操作。
讀放大
Read | B-Tree | LSM-Tree |
---|---|---|
最優 |
|
|
最壞 |
|
|
LST-Tree 由於引入了 SSTable 格式,最壞情況下讀取次數不可控。
對比
LSM-Tree 有着更小的寫放大效應,B-Tree 有着更小的讀放大效應。
LSM-Tree 能夠承載更高的寫入吞吐量,B-Tree 在隨機讀的情況下能夠提供更穩定的性能保障。
LSM-Tree 本身就是一種對讀寫的 trade-off,用更大的讀放大效應換取更小的寫放大效應。
更進一步的,LSM-Tree 可以通過調整合並策略Merge Policy
在讀寫放大之間進行權衡。
總結
優點 | 缺點 | |
---|---|---|
B-Tree |
|
|
LSM-Tree |
|
|