數據庫存儲引擎是一個有歷史的技術,通過數十年的發展,已經出現不少優秀成熟的產品。阿里巴巴 X-Engine 團隊撰寫的論文 "X-Engine: An Optimized Storage Engine for Large-scale E-Commerce Transaction Processing",詳細講述了團隊在數據庫存儲引擎上所作的原創性工做,今年早些時候已經被 SIGMOD'19 Industrial Track 接收(SIGMOD 是數據庫領域最重要也是最有影響力的會議之一)。本文將對這篇論文作一個前導性分析。算法
X-Engine 是阿里數據庫產品事業部自研的 OLTP 數據庫存儲引擎,做爲自研數據庫POLARDB X 的存儲引擎,已經普遍應用在阿里集團內部諸多業務系統中,其中包括交易歷史庫,釘釘歷史庫等核心應用,爲業務大幅縮減了成本,同時也做爲雙十一大促的關鍵數據庫技術,挺過了數百倍平時流量的衝擊。數據庫
數據庫存儲引擎是一個有歷史的技術,通過數十年的發展,已經出現不少優秀成熟的產品。各式存儲引擎已經在索引組織,緩存管理,事務處理,查詢優化方方面面都作過細緻的研究。即使如此,這個領域的演進仍在持續,每一年都會涌現不少的新技術。緩存
近年來,LSM (Log-Structured Merge-Tree)結構受到愈來愈多的關注,雖然這個技術自己出現不少年了,不算什麼新事物,不過早先在 KV 存儲系統中被應用的更多一些,近年開始在數據庫存儲引擎領域嶄露頭角,RocksDB 便是典型表明。數據結構
LSM 之因此變得流行,一是由於其簡單,二是特色鮮明。寫入模型是簡單的追加,不會更新既有的數據,數據組織爲簡單的邏輯排序,由此帶來的特色是寫強而讀弱,持久化數據只讀的特色便於壓縮。可是大多數數據庫的應用場景其實都是讀多寫少的,直接使用 LSM 結構未必合適,想要另闢蹊徑,須得揚長闢短。架構
X-Engine 使用了 LSM 做爲基礎架構,目標是做爲一個通用的高性能低成本存儲引擎,追求讀寫性能更爲均衡,所以在其上作了大量的改進,主要圍繞幾個方向進行:併發
X-Engine 的總體架構以下圖,根據數據冷熱進行分層代替 LSM 自己的持久化數據分層,熱數據層和數據更新使用內存存儲,利用了大量內存數據庫的技術(Lock-Free index structure/append only)提升事務處理的性能,設計了一套事務處理流水線處理機制,把事務處理的幾個階段並行起來,提高吞吐。而訪問頻度低的冷(溫)數據逐漸淘汰或是合併到持久化的存儲層次中,結合當前豐富的存儲設備層次體系(NVM/SSD/HDD)進行存儲。app
咱們對性能影響比較大的 compaction 過程作了大量優化,主要是拆分數據存儲粒度,利用數據更新熱點較爲集中的特徵,儘量的在合併過程當中複用數據,精細化控制 LSM 的形狀,減小 I/O 和計算代價,並同時極大的減小了合併過程當中的空間放大。同時使用更細粒度的訪問控制和緩存機制,優化讀的性能。異步
既然 X-Engine 是以 LSM 爲基礎架構的,因此一切還要從 LSM 自己提及。性能
一條數據在 LSM 結構中的旅程,從寫入 WAL(Write Ahead Log) 開始,而後進入MemTable,這是 Ta 整個生命週期的第一處落腳點。隨後,flush 操做將 Ta 刻在更穩固的介質上,compaction 操做將Ta帶往更深遠的去處,或是在途中丟棄,取決於 Ta 的繼任者什麼時候到來。優化
LSM 的本質是,全部寫入操做並不作原地更新,而是以追加的方式寫入內存。每次寫到必定程度,即凍結爲一層(Level),寫入持久化存儲。全部寫入的行,都以主鍵(Key)排序好後存放,不管是在內存中,仍是持久化存儲中。在內存中即爲一個排序的內存數據結構(Skiplist, B-Tree, etc.),在持久化存儲也做爲一個只讀的全排序持久化存儲結構。
普通的存儲系統若要支持事務處理,尤爲是ACI,須要加入一個時間維度,藉此爲每一個事務構造出一個不受併發干擾的獨立視域。存儲引擎會對每一個事務定序並賦予一個全局單調遞增的事務版本號(SN),每一個事務中的記錄會存儲這個 SN 以判斷獨立事務之間的可見性,從而實現事務的隔離機制。
若是 LSM 存儲結構持續寫入,不作其餘的動做,那麼最終會成爲以下結構:
注意這裏每一層的 SN 範圍標識了事務寫入的前後順序,已經持久化的數據再也不會被修改。每一層數據按 Key 排序,層與層之間的 Key range 會交疊。
這種結構對於寫入是很是友好的,只要追加到最新的內存表中即完成,爲實現 crash recovery,只需記錄 WAL(Redo Log),由於新數據不會覆蓋舊版本,追加記錄會造成自然的多版本結構。
能夠想見,如此累積凍結的持久化層次愈來愈多,會對查詢會產生不利的影響,對同一個 key 不一樣事務提交產生的多版本記錄會散落在各個層次中,不一樣的 key 也會散落在不一樣層次中,讀操做諸如順序掃描便須要查找各個層併合併產生最終結果。
LSM 引入了一個 compaction 的操做解決這個問題,這個操做不斷的把相鄰層次的數據合併,並寫入這個更低層次。而合併的過程實際上就是把要合併的相鄰兩層(或是多層)數據讀出來,按 key 排序,相同的 key 若是有多個版本,只保留新(比當前正在執行的活躍事務中最小版本號新)的版本,丟掉舊版本數據,而後寫入新的層。能夠想見這個操做很是耗費資源。
LSM compaction 操做,有幾種做用,一是爲了丟棄再也不被使用的舊版本數據,二是爲了控制 LSM 層次形狀,通常的 LSM 形狀都是層次越低,數據量越大(倍數關係),這樣放置的目的主要是爲了提高讀性能。
通常來說,任何存儲系統的數據訪問都有局部性,大量的訪問都集中在少部分數據上,這也是緩存系統能有效工做的基本前提,在 LSM 存儲結構中,若是咱們把訪問頻率高的數據儘量放在較高的層次上,保持這部分數據量規模,能夠存放在快速存儲設備中(好比 NVM,DRAM),而把訪問頻率低的數據放在較低層次中,使用廉價慢速存儲設備存儲。這就是 X-Engine 的根據冷熱分層概念。
要達到這種效果,核心問題是如何挑選合適的數據合併到更低的層次,這是compaction 調度策略首先要解決的問題,根據冷熱分層的邏輯,就是優先合併冷數據(訪問頻率相對低)。
識別冷數據有不少方法,對於不一樣的業務不盡然相同,對於不少流水型業務(如交易,日誌系統),新近寫入的數據會有更多的機率被讀到,冷熱按寫入時間順序便可區分,也有不少應用的訪問特徵跟寫入的時間不必定有關係,這個就要根據實際的訪問頻率去識別冷數據或是熱數據。
除了數據熱度之外,挑選合併數據還有其餘一些維度,會對讀性能產生影響,好比數據的更新頻率,大量的多版本數據在查詢的時候會浪費更多的I/O和CPU,所以須要優先進行合併以減小記錄的版本數量,X-Engine 綜合考慮了各類策略造成本身的compaction 調度機制。
上面是 LSM 宏觀邏輯結構,若是具體來論讀寫操做和 compaction 如何進行,就須要探討每一層的數據組織方式, 每一個 LSM 變種的實現各不相同。
X-Engine 的 memtable 使用了 Locked-free SkipList. 求的是簡單,並且併發讀寫的性能都比較高。固然有更高效的數據結構,或者同時使用多種索引技術。這個部分X-Engine 沒有作過多優化,緣由在事務處理的邏輯比較複雜,寫入內存表尚未成爲其瓶頸。
持久化層如何組織更顯高效,這就須要討論每層的細微結構。
數據組織
簡單來講,X-Engine 的每層都劃分紅固定大小的 Extent,存放每一個層次中的數據的一個連續片斷(Key Range). 爲了快速定位 Extent,爲每層 Extents 創建了一套索引(Meta Index),全部這些索引,加上全部的 memory tables(active/immutable)一塊兒組成了一個元數據樹(Metadata Tree),root 節點爲"Metadata Snapshot", 這個樹結構相似於 B-Tree,固然不盡相同。
須要注意的是,X-Engine 中除了當前的正在寫入的 active memtable 之外,其餘結構都是隻讀的,不會被修改。給定某個時間點, 好比 LSN=1000, 上圖中的 "Metadata Snapshot1" 引用到的結構即包含了(LSN=1000)時刻的全部的數據的快照(這也是爲何這個結構被稱爲 Snapshot 的緣由)。
即使是 Metadata 結構自己,也是一旦生成就不會修改。全部的讀都是以這個" Snapshot "結構爲入口,這個是 X-Engine 實現 SI 隔離級別的基礎。以前講過隨着數據寫入,累積數據越多,須要對 memtable 凍結,flush, 以及層與層的compaction. 這些操做都會修改每層的數據存儲結構,全部這些操做,都是用 copy-on-write 來實現,方法就是每次都將修改(switch/flush/compaction)產生的結果寫入新的 Extent,而後依次生成新的"Meta Index"結構,乃至新的"Metadata Snapshot",以一次 compaction 操做爲例:
能夠看到"Metadata Snapshot 2"相對於"Metadata Snapshot 1"並無太多的變化,僅僅修改了發生變動的一些葉子節點以及索引節點。這個技術很有些相似"B-trees, Shadowing, and Clones",若是你讀過那篇論文,會對理解這個過程有所幫助。
事務處理
得益於 LSM 輕量化寫機制,寫入操做當然是其明顯的優點,可是事務處理遠不僅是把更新的數據寫入系統那麼簡單,這裏要保證 ACID,涉及到一整套複雜的流程。X-Engine 將整個事務處理過程分爲兩個階段:讀寫階段和提交階段。
讀寫階段須要校驗事務的寫寫衝突,讀寫衝突,判斷事務是否能夠執行或回滾重試,或是等鎖。若是事務衝突校驗經過,則把修改的全部數據寫入"Transaction Buffer", 提交階段包括寫 WAL,寫內存表,以及提交併返回給用戶結果的整個過程,這裏面既有 I/O 操做(寫日誌,返回消息),也有 CPU 操做(拷貝日誌,寫內存表)。
爲了提升事務處理吞吐,系統內會有大量事務併發執行,單個I/O操做比較昂貴,大部分存儲引擎會傾向於彙集一批事務一塊兒提交,稱爲"Group Commit",可以合併I/O操做,可是一組事務提交的過程當中,仍是有大量等待過程的,好比寫入日誌到磁盤過程當中,除了等待落盤無所事事。
X-Engine 爲了進一步提高事務處理的吞吐,採用了一種流水線的技術:把提交階段分爲四個獨立的更細的階段:拷貝日誌到緩衝區(Log Buffer),日誌落盤(Log Flush),寫內存表(Write memtable),提交返回(Commit)。咱們的事務提交線程到了處理階段,均可以自由選擇執行流水線中任意一個階段,這樣每一個階段均可以並行起來,只要流水線任務的大小劃分得當,就能充分並行起來,流水線處於接近滿載狀態。
另外,利用的是事務處理的線程,而非後臺線程,每一個線程在執行的時候,要麼選擇了流水線中的一個階段幹活,要麼逛了一圈發現無事可作,乾脆回去接收更多的請求,這裏沒有等待,也無需切換,充分的調動了每一個線程的能力。
讀操做
LSM 在處理多版本數據的方式是新版本數據記錄會追加在老版本數據後面,從物理上看,一條記錄不一樣的版本可能存放在不一樣的層,在查詢的時候須要找到合適的版本(根據事務的隔離級別定義的可見性規則),通常查詢都是查找最新的數據,老是由新的層次(最新寫入)往老的層次方向找。
對於單條記錄的查找而言,一旦找到即可終止,若是記錄還在比較靠上的層次,好比memtable,很快便返回;若是記錄不幸已經落入了很低的層次(多是很隨機的讀),那就得經歷逐層查找的漫漫旅途,也許 bloomfilter 能夠跳過某些層次加快這個旅程,但畢竟仍是有更多的I/O操做。
X-Engine 針對單記錄查詢引入了 Row Cache,在全部持久化的層次的數據之上作了一個緩存,在 memtable 中沒有命中的單行查詢,在 Row Cache 之中也會被捕獲。Row Cache 須要保證緩存了全部持久化層次中最新版本的記錄,而這個記錄是可能發生變化的,好比每次 flush 將只讀的 memtable 寫入持久化層次時,就須要恰當的更新 Row Cache 中的緩存記錄,這個操做比較微妙,須要當心的設計。
範圍掃描的操做就沒這麼幸運了。由於無法肯定一個範圍的key在哪一個層次中有數據,也許是每層都有,只能掃描全部的層次作合併以後才能返回最終的結果。X- Engine 一樣採用了一系列的手段:好比 Surf(SIGMOD'18 best paper)提供 range scan filter 減小掃描層數;還有異步 I/O 與預取對大範圍掃描也有顯著的提高。
讀操做中最核心的是緩存設計,Row Cache 來應付單行查詢,Block Cache 負責Row Cache miss 的漏網之魚,也用來應付 scan;因爲 LSM 的 compaction 操做會一次大批量更新大量的 Data Block,致使 Block Cache 中大量數據短期內失效,帶來性能的急劇抖動。
X-Engine 一樣作了不少的處理:
X-Engine 中的緩存比較多樣,memtable 也可算作其中一種。以有限的內存,如何恰當的分配給每一種緩存,才能實現價值最大化,是一個還未被妥善解決的問題,X-Engine 也在探索當中。
固然,LSM 對讀帶來的也並不是全是壞處,除了 memtable 之外的只讀的結構,在讀取路徑上能夠作到徹底無鎖( memtable 也可設計成讀無鎖)。
Compaction
compaction 操做是比較重的。須要把相鄰層次交叉的 key range 數據讀出來,合併,而後寫到新的位置。這是爲前面簡單的寫入操做不得不付出的代價。X-Engine 爲優化這個操做從新設計了存儲結構。
如前所述,X-Engine 將每一層的數據劃分爲固定大小的" Extent",一個 Extent 至關於一個小的完整的 SSTable, 存儲了一個層次中的一個連續片斷,其中又會被進一步劃分一個個連續的更小的片斷"Data Block",至關於傳統數據庫中的"Page",只不過是只讀的,並且是不定長的。
回看數據組織一節中"合併操做對元數據的改變", 對比"Metadata Snapshot2"和"Metadata Snapshot1"的區別,能夠發現 Extent 的設計意圖。是的,每次修改對結構的調整並非所有來過,而是隻須要修改少部分有交疊的數據,以及涉及到的"Meta Index"節點。
兩個"Metadata Snapshot"結構實際上共用了大量的數據結構。這個被稱爲數據複用技術(Data Reuse),而 Extent 大小正是影響數據複用率的關鍵,Extent 做爲一個完整的被複用的物理結構,須要儘量的小,這樣與其餘 Extent 數據交叉點會變少,但又不能很是小,不然須要索引過多,管理成本太大。
X-Engine 中 compaction 的數據複用是很是完全的,假設選取兩個相鄰層次(Level1, Level2)中的交叉的 Key Range 所涵蓋的 Extents 進行合併,合併算法會逐行進行掃描,只要發現任意的"物理結構"(包括 Data Block 和 Extent )與其餘層中的數據沒有交疊,則能夠進行復用。只不過,Extent的複用能夠修改 Meta Index,而 Data Block 的複用只能拷貝,即使如此也能夠節省大量的 CPU.
一個典型的數據複用在 compaction 中的過程能夠參考下圖:
能夠看出,對於數據複用的過程是在逐行迭代的過程當中完成的,不過這種精細的數據複用帶來另外一個反作用,即數據的碎片化,因此在實際操做的過程當中也須要根據實際狀況進行折中。
數據複用不只給 compaction 操做自己帶來了好處,下降操做過程當中的 I/O 與 CPU消耗,更對系統的綜合性能產生了一系列的影響。好比 compaction 過程當中數據不用徹底重寫,大大減小了寫入空間放大;更由於大部分數據保持原樣,數據緩存不會由於數據更新而失效,減小合併過程當中因緩存失效帶來的讀性能抖動。
實際上,優化 compaction 的過程只是 X-Engine 工做的一部分,還有更重要的,就是優化 compaction 調度的策略,選什麼樣的 Extent,定義 compaction 任務的粒度,執行的優先級,都會對整個系統性能產生影響,惋惜並不存在什麼完美的策略,X-Engine 積累了一些經驗,定義了不少規則,而探索如何合理的調度策略是將來一個重要方向。
X-Engine 是阿里雲智能事業羣-數據庫產品事業部的重要核心技術之一。
做爲兼容 MySQL 的數據庫 POLARDB X 的存儲引擎,以前是在服務阿里集團業務中逐漸打磨成熟,今年下半年,咱們將在阿里雲平臺上推出 MySQL(X-Engine) 的RDS 公有云服務,爲阿里雲上的公有云客戶提供低成本高性能的數據庫服務。
原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。