https://zhuanlan.zhihu.com/p/436485359
作者簡介:沈煉,螞蟻集團技術風險部資料庫高階專家
畢業於東南大學,2014年以來從事 OceanBase 在螞蟻的架構工作,目前職責包括螞蟻 OceanBase 高可用體系建設 和 OceanBaseKV 在螞蟻的架構及研發工作,對標業界的“自治資料庫”和“多模型資料庫”,致力於讓 OceanBase 走得更穩、更遠、更快。在螞蟻 OceanBase 體系中,沈煉先後負責 螞蟻核心鏈路上 OceanBase 、 OceanBase 高可用體系建設、 OceanBaseKV 架構及研發。沈煉對網際網路金融場景、資料庫核心(關係型及 NoSQL )、資料庫 PAAS 、高可用體系、安全體系 等方面有深入的理解,將會推出系列文章介紹 OceanBase 的儲存引擎、高可用核心、高可用體系及理念、 OceanBase 多模型資料庫等
本文將為大家詳細講解 OceanBase 儲存引擎,回答關於 OceanBase 資料庫的以下相關提問:
- OceanBase 是否依賴其他開源 KV 資料庫(例如: LevelDB 、RocksDB )?
- OceanBase 底層引擎是什麼?是 KV 嗎 ?
- OceanBase 記憶體結構是 B+Tree 還是 LSMTree ?
- OceanBase 如何實現高效能服務?
背景
目前業界資料庫儲存引擎主要分為兩種:
- update-in-place :原地更新,較常見於傳統關係型資料庫( MySQL 、Oracle )採用的 B+Tree 結構。優點:更新記錄時對原有記錄進行覆蓋寫。有較好的資料區域性性,對掃描比較友好。缺點:是引入大量的隨機寫,同時還有一定的併發問題;並且與業務流量疊加,對業務流量有一定的影響;
- log-structure storage:日誌更新,例如 LevelDB 、RocksDB 、HBase 、BigTable 等採用的 LSMTree 結構。優點:日誌更新無鎖,不會引入併發問題,能夠保證高效寫入,並且沒有空間碎片。缺點:是讀路徑變長,例如 LSMTree 結構在掃描時需要讀取 memtable 、L0層及其餘層的資料,並進行歸併,需要透過非同步 compaction 進行GC以及均衡各個層級的資料來避免過多的讀放大。
為追求極致的資料庫效能,scan 操作需要良好的空間區域性性,get/put 操作需要高效的索引來定位,version/gc/compaction 會提升讀操作的效能但可能影響整體效能,目前的儲存引擎都存在著一定的侷限性。
為此 OceanBase 選擇完全自主實現儲存引擎,沒有藉助於任何其他已有的開源方案。
從架構上看,OceanBase 的儲存引擎可分為兩層:
①底層的分散式引擎實現了線性擴充套件、Paxos 複製、分散式事務等分散式特性從而達到持續可用;
②上層的單機引擎融合了傳統關聯式資料庫以及記憶體資料庫的一些技術從而達到極致效能。
本文將從單機引擎和分散式架構視角分別介紹。
01 單機引擎
讀寫分離架構
OceanBase 的儲存引擎採用分層 LSMTree 結構,資料分為兩部分:基線資料和增量資料。
基線資料是持久到磁碟上的資料,一旦生成就不會再修改,稱之為 SSTable。
增量資料存在於記憶體,使用者寫入都是先寫到增量資料,稱之為 MemTable,透過 Redo Log 來保證事務性。
當 MemTable 達到一定閾值時會觸發凍結( Frozen MemTable ),並重新開啟一個新的 MemTable ( Active MemTable ),Frozen MemTable 被轉存到轉儲 SSTable 中,然後在合併( LSMTree 結構特有的 compaction 動作 )時將轉儲 SSTable 合併入基線 SSTable,生成新的 SSTable 。
在查詢時,需要將 MemTable 和 SSTable 的資料進行歸併,才能得到最終的查詢結果。
系統為基線資料和增量資料指定不同的版本,資料版本是連續遞增的。
每生成一個新的 Active MemTable ,都會設定為上個 MemTable 的版本加1(實際生產中,兩次合併之間會有多次轉儲,這兩次合併之間生成的所有MemTable 表示一個大版本,每個 MemTable 會用小版本表示,例如下圖中的v3.1和v3.2)。當 SSTable 與 Frozen MemTable 合併之後,也會將版本設定為合併的 Frozen MemTable 的版本。
讀寫分離架構好處:因基線資料是靜止狀態,方便對其進行壓縮,減少儲存成本和提升查詢效能;做行級快取不用擔心寫入帶來的快取失效問題。
讀寫分離架構缺點:讀路徑變長,資料需要實時歸併可能帶來效能損耗,需要合併減少資料檔案,同時我們引入多層 Cache 來快取頻繁訪問的資料。
合併是基於 LSMTree 架構的儲存引擎特有的動作,用於基線資料和增量資料的歸併,在 HBase 和 RocksDB 稱為 major compaction。
OceanBase 通常在每天的業務低峰期觸發合併,非低峰期透過轉儲來釋放記憶體,我們稱之為“每日合併”。在合併視窗內做了資料壓縮、資料校驗、物化檢視、schema 變更等事情。
合併提供很多好處,但也需承受一定的代價——對系統的 IO 和 CPU 會有較多的消耗。
目前業界的 LSMTree 資料庫沒能很好的解決該問題,為此 OceanBase 做了最佳化,包括增量合併、漸進合併、並行合併、輪轉合併、IO隔離等技術。
相比較於傳統關係型資料庫,每日合併可以更好的利用業務低峰期的空閒IO,對資料壓縮、校驗、物化檢視等也提供更好的支援。
轉儲是將記憶體中的增量資料儲存於磁碟,類似於 HBase 和 RocksDB 的 minor compaction ,可以緩解 LSMTree 架構下的記憶體壓力。
OceanBase 採用了分層轉儲的策略,達到了讀取和寫入之間的平衡,避免轉儲 SSTable 過多拖慢讀效能,同時也避免了業務大量隨機寫造成的轉儲寫放大。
OceanBase 還進行了資源隔離(包括CPU、記憶體、磁碟IO、網路IO等四個方面的資源隔離)避免轉儲消耗過多資源,減少對使用者請求的影響。
這些方面的最佳化平滑了轉儲對效能的影響,使得 OceanBase 在 TPC-C 測試中效能曲線絕對平滑。
基線資料
SSTable 將資料按照主鍵的順序有序儲存,資料被劃分為2MB的塊,稱之為宏塊。
每個宏塊對應一個主鍵範圍的資料,為了避免讀一行要載入一個宏塊,宏塊內部又劃分為多個微塊,微塊的大小一般為16KB。合併時微塊寫滿16KB時,根據行資料的規律探測編碼規則,並根據選擇的編碼規則對資料進行編碼及計算 checksum ,所以微塊是讀IO的最小單位。
OceanBase 的微塊壓縮後是“變長資料塊”,空間浪費很少。而傳統關係型資料庫是“定長資料塊”,開啟壓縮會不可避免的造成儲存的空洞,存在較多的空間浪費,壓縮率受影響。
在相同塊大小(16KB),相同壓縮演算法,相同資料的情況下,在 OceanBase 中要比在傳統關係型資料庫中節省相當多的空間。
宏塊是合併時的基本單位,SSTable 按照宏塊來迭代,MemTable 按照行來迭代。如果 MemTable 的行不在宏塊的資料範圍裡面,新版本的 SSTable 直接引用該宏塊。如果宏塊的資料範圍內有更新資料,需要將宏塊開啟,解析微塊索引,並迭代所有微塊。
對於每一個微塊,載入行索引,將所有行解析出來跟MemTable進行合併,如果已經 append 的行達到16KB,則構建成一個新微塊,並進行編碼壓縮及計算 checksum。對於未修改過的微塊,不用解析微塊內的內容,直接將整個微塊複製到新的宏塊即可。
OceanBase 對資料質量保持著敬畏之心,合併時會做兩方面的資料校驗:
① 一方面同一份資料會有多個副本,會做副本間的一致性檢驗,保障業務資料在不同副本間是一致的;
② 另一方面會做主表和索引表的一致性校驗,保障資料在主表和索引表之間是一致的。
資料校驗是 OceanBase 對自己的一道防火牆,保障交付給業務的資料是完全正確的,避免客戶投訴才知曉。
增量資料
MemTable 在記憶體中維護歷史版本的事務,每一行將歷史事務針對該行的操作按時間序從新到舊組織成行操作鏈,新事務提交時會在行操作鏈頭部追加新的行操作。
如操作鏈儲存的歷史事務過多,將影響讀取效能,此時需要觸發 compaction 操作,融合歷史事務生成新的行操作鏈,compaction 操作不會刪除老的行操作鏈。
OceanBase 基於以上 MVCC 機制實現併發控制,讀事務與寫事務互不影響:
① 如讀請求只涉及一個分割槽或者單臺 OBServer 的多個分割槽,執行快照讀即可;
② 如涉及多臺 OBServer 的多個分割槽,需要執行分散式快照讀。
MemTable 中採用雙索引結構,一塊是 Hashtable ,用於 KV 類的快速查詢;一塊是 BTree ,用於 Scan 等掃描查詢。在插入/更新/刪除資料時,資料被寫入記憶體塊,在 Hashtable 和 BTree 中儲存的均為指向對應資料的指標。
每次事務執行時,會自動維護兩塊索引之間的一致性。兩種資料結構各有優劣:
① 插入一行資料的時候,需要先檢查此行資料是否已經存在,檢查衝突時,用 Hashtable 比 BTree 快;
② 事務在插入或者更新一行資料時,需要找到此行並對其進行上鎖,防止其他事務修改此行,在 OceanBaseMvccRow 中尋找行鎖時,用 Hashtable 比 BTree 快;
③ 範圍查詢時,由於 BTree 節點中的資料是有序的,能夠提高搜尋的區域性性,而 Hashtable 是無序的,需要遍歷整個 Hashtable 。
OceanBase 是一個準記憶體資料庫,絕大部分的熱點資料都是在記憶體中,以行的方式組織,且在記憶體不足時以MB粒度將資料寫入磁碟,避免了傳統關係型資料庫以頁面為單位的寫入放大問題,從架構層面大大提升了效能。
OceanBase 還引入記憶體資料庫的最佳化技術,包括記憶體多版本併發、無鎖資料結構等,實現最低延遲及最高效能。
同等硬體下,OceanBase 效能相比傳統關係型資料庫有很大提升,再加上得益於多副本強同步而採用的普通PC伺服器,資料庫的硬體成本大大降低。
快取
上文提到由於讀路徑變長,我們引入了多層 Cache 機制。
Cache 主要用於快取 SSTable 中頻繁訪問的資料,有用於快取 SSTable 資料的 block cache、有用於快取微塊索引的 block index cache 、有用於快取資料行的row cache,有用於快取 bloomfilter 可以快速過濾空查的 bloomfilter cache。
由於 SSTable 非合併期間只讀,所以不用擔心 Cache 失效的問題。
讀請求來臨時,首先從 Cache 中讀取資料,如果 Cache 沒有命中會產生磁碟 IO 讀取微塊資料,可以根據 Cache 命中次數及磁碟讀取次數來計算邏輯讀,邏輯讀用於評估 SQL 執行計劃的優劣。對於單行操作,如果行存在,則會命中 row cache ;如果行不存在,則命中 bloomfilter cache 。
因此絕大部分單行操作在基線資料中只需要一次快取查詢,沒有額外開銷。
OceanBase 中有兩個模組會佔用大量的記憶體:
① MemTable 為不可動態伸縮的記憶體;
② 就是 Cache 為可動態伸縮的記憶體。
與 linux 的 Cache 策略一樣,OceanBase 中的 Cache 也會盡量使用記憶體,力求把除 MemTable 以外的記憶體用滿。因此 OceanBase 為 Cache 設計了優先順序控制策略及智慧淘汰機制。
類似於 Oracle 的 AMM,設計了一套統一的 Cache 框架,所有不同租戶不同型別的 Cache 都由框架統一管理,對於不同型別的 Cache,會配置不同的優先順序。(例如 block index cache 優先順序較高,一般是常駐記憶體很少淘汰,row cache 比 block cache 效率更高,所以優先順序也會更高。)
不同型別的 Cache 會根據各自的優先順序以及資料訪問熱度做相互擠佔。優先順序一般不需要配置,特殊場景下可以透過引數控制各種 Cache 的優先順序。如果寫入速度較快,MemTable 佔用記憶體較多,會逐步淘汰 Cache 記憶體給MemTable 使用。
Cache 記憶體以2MB為單位進行淘汰,根據每個2MB記憶體塊上各個元素的訪問熱度為其計算一個分值,訪問越頻繁的記憶體塊的分越高,同時有一個後臺執行緒來定期對所有2M記憶體塊的分值做排序,淘汰掉分值較低的記憶體塊。
在淘汰時,會考慮各個租戶的記憶體上下限,控制各個租戶中 Cache 記憶體的使用量。
極致效能
基以上介紹,OceanBase 的儲存引擎非常接近於傳統關係型資料庫的單機引擎特性——一個資料分割槽的所有資料(基線資料 + 增量資料 + 快取資料 + 事務日誌)全部放於一個 OBServer 中。
因此針對一個資料分割槽的讀寫操作不會有跨機操作(除了事務日誌透過 Paxos 協議形成多數派同步),再加上準記憶體資料庫及極佳的 Cache 機制,從而能夠提供極致的OLTP效能。
02 分散式架構
資料分割槽
OceanBase 的兩種方式:
① 引入傳統關聯式資料庫中的資料分割槽表( Partition )的概念;
② 相容傳統關係型資料庫的分割槽表語法,支援 hash 分割槽和 range 分割槽。
支援二級分割槽機制,例如對於歷史庫場景,單表資料量龐大,一級按使用者分割槽,二級按時間分割槽。從而很好的解決大使用者擴充套件性的問題,如果要刪除過期資料,可以透過 drop 分割槽實現。
也支援生成列分割槽,生成列指這一列由其他列計算所得,該功能能夠滿足期望將某些欄位進行一定處理後作為分割槽鍵的需求。
對分割槽表查詢進行最佳化:
① 對於查詢中包含分割槽鍵或者分割槽表示式的查詢,能夠準確的計算出該查詢對應的分割槽,稱為分割槽裁剪;
② 對於多分割槽查詢,能夠使用分割槽間並行查詢、分割槽間排序消除、partition-wise join 等技術,從而大大提升查詢效能。
同一資料分割槽的副本構成一個 Paxos Group,自主進行選主,推舉出其中某個副本作為 Leader,其他副本自動成為 Follower,後續所有針對這個資料分割槽的讀寫請求,都會自動路由到對應的 Leader 進行服務。
OceanBase 採用 share-nothing 的分散式架構,每個 OBServer 都是對等的,管理不同的資料分割槽。
① 對一個資料分割槽所有讀寫操作都在其所在的 OBServer 完成,屬於單分割槽事務;
② 多個資料分割槽的事務,採用兩階段提交的方式在多個 OBServer 上執行,屬於分散式事務。
單機多分割槽事務,依然需要走兩階段提交,且針對單機做了專門的最佳化。分散式事務會增加事務延遲,可以透過表格組( table group ) 將經常一起訪問的多張表格聚集在一起,同一表格組的分割槽具有相同的 OBServer 分佈,且 Leader 位於同一臺機器上,避免跨機事務。
傳統關係型資料庫也支援分割槽功能,但所有分割槽仍存放在同一臺機器上。
而 OceanBase 能夠把所有分割槽打散到不同物理機器,從而能夠真正體現出分散式架構的優勢,從而徹底解決可擴充套件性問題。
當容量或者服務能力不足時,只需要增加更多的資料分割槽並打散到更多的 OBServer 即可,從而可以透過線上線性擴充套件的方式提升整體讀寫效能。在同樣系統容量充足或者處理能力富餘時,將機器下線,降低成本,提供良好的彈性伸縮能力。
多副本Paxos同步
OceanBase 中的資料分割槽冗餘有多個副本(例如,同城三副本部署架構為3個,三地五副本部署架構為5個),分佈於多個 OBServer 上。
事務提交時利用 Paxos 協議在多個副本間達成多數派提交,從而維護副本之間的一致性。當單臺 OBServer 當機時可以維護資料的完整性,並在較短的時間內恢復資料訪問,達到 RPO=0、RTO<30 秒的SLA。
使用者無需關心資料所在的具體位置,客戶端會根據使用者請求來定位資料所在的位置,從而將讀寫請求傳送給 Leader 進行處理,對外仍然展現為一個單機資料庫。
基於以上介紹的多副本架構我們引入了輪轉合併的概念,將使用者請求流量與合併過程錯開來。
比如:一個 OceanBase 叢集中 Partition 的副本個數為3,這三個副本分佈在3個不同的 Zone(1,2,3) 中,RootService 在控制合併時,比如先合併 Zone3 ,會首先將使用者流量切到 Zone(1,2) 中,只要切換所有 Partition的 Leader 到 Zone(1,2) 即可。Zone3 合併完成之後,準備合併 Zone2 ,則將 Zone2 的流量切走,之後再合併Zone1 ,最後三個副本全部合併完畢,恢復初始的 Leader 分佈。
多副本架構帶來了三個比較大的架構提升:
- 資料庫服務的可用性得到提升,如果某個 OBServer 突發當機或者網路分割槽,自動、迅速的把故障 OBServer 的讀寫服務切換到其他 OBServer 上,RPO=0,RTO<30秒。傳統關係型資料庫透過主備架構來容災,在不使用共享儲存的情況下,難以完美做到在主庫故障時資料零丟失,且由於資料一致性問題無法自動切換,切換效率也難以保證;
- 資料庫的資源使用率得到提升,利用 OceanBase 多庫多活的特性,配置讓三個 Zone 中的兩個提供讀寫服務,第三個 Zone 作為熱備庫接受事務日誌,隨時待命提供讀寫服務。OceanBase 的機器使用率達到了2/3,而傳統關係型資料庫主備架構只能使用到1/2的機器;
- 資料庫的資料壓縮率得到提升,由於輪轉合併的引入,使用者請求流量與合併過程錯峰,正在合併的 Zone 的 CPU 和磁碟 IO 可以大量用於複雜的資料編碼/壓縮,並選擇最優的編碼演算法,並且對業務資料寫入零影響。高壓縮率不但節省了儲存空間,同時也會極大的提升查詢效能。傳統關係型資料庫原地更新資料,與業務流量疊加,在“高壓縮率”和“低計算成本”兩者之間只能選擇後者,甚至建議使用者謹慎使用該功能。
03 總結
基於 LSMTree 的單機引擎和基於多副本強同步的分散式架構,才是完整的 OceanBase 儲存引擎。這種儲存引擎有眾多好處,既有類似於 Oracle 的關係型資料庫上層,又有類似於 spanner 的分散式底層。
這是 OceanBase 的核心能力,能夠規避 LSMTree 缺陷,獲得極致效能和絲般順滑,支援最核心的 OLTP 業務。同時又獲得了分散式架構的優勢,包括持續可用、線性擴充套件、自動容錯、低成本等特性。
寫在最後:
從2010年一路走來,每一步 OceanBase 猶如走在懸崖峭壁,走得十分小心翼翼。回頭看,非處當時之情景,不能理解當時之設計。好的設計不是“想”出來的,而是“痛”出來的,希望大家在閱讀時也能夠感受到這份成果背後的“痛並快樂著”。