作者 | 馬浩翔
日前,字節跳動技術社區 ByteTech 舉辦的第四期字節跳動技術沙龍圓滿落幕,本期沙龍以《字節云數據庫架構設計與實戰》為主題。在沙龍中,字節跳動基礎架構數據庫開發工程師馬浩翔,跟大家探討了 《從單機到分布式數據庫存儲系統的演進》,本文根據分享整理而成。
存儲系統概覽
存儲系統是指能高效存儲,持久化用戶數據的一系列系統軟件。在眾多的存儲系統中,以下是三類比較主流的存儲產品及其特點分析:
塊存儲
- 底層語義,基于 block 編程;
- 接口樸素:在 Linux 的 IO 軟件棧中,要直接使用塊存儲的話就要基于 LBA 編程,因此接口較為簡單樸素,再加上塊存儲本身處于整個存儲軟件棧的底層,這導致塊存儲使用起來并不十分友好;
- 追求低時延、高吞吐:研發一個塊存儲系統,在設計目標上我們往往會追求超高的性能,體現在超低的時延和超高的吞吐。但考慮到塊存儲的接口確實過于樸素,往往只有一些追求超高性能的系統才會直接基于塊存儲構建,然后自建應用層 cache。
對象存儲
- 公有云上的王牌存儲產品:在 IT 時代,“Everything is data”的趨勢迅速催化了對象存儲系統。尤其對于字節跳動的業務而言,使用對象存儲系統來處理視頻、圖片、音頻等非結構化的數據,語義最為自然;
- 非結構化數據,提供 immutable 語義:一旦圖片或視頻等非結構化數據上傳至對象存儲系統成功,則無法對其進行原地修改,只能通過刪除舊數據,重新上傳新數據的方式完成“修改”邏輯;
- 成本優先:一般不苛求單次操作的時延,但是非常注重系統吞吐 & 存儲成本。
文件系統
- 接口語義豐富,普適性強:遵循 POSIX/弱 POSIX 語義,諸如 Open、Write、Read 等許多操作數據的接口都能在文件系統中被找到。
- 擁有較多開源的分布式實現,生態良好。
- 一般也不苛求時延,注重系統吞吐 & 存儲成本。
單機數據庫存儲解析
單機數據庫存儲,要從內存層和持久化層兩個方面來解析。在內存層,僅說關系型數據庫,其內存數據結構特點可以總結為:一切都是“樹”。我們以最常見的 B+ 樹為例,B+ 樹具有以下突出的特點:
- In memory 操作效率非常高:B+ 樹搜索時間復雜度是 log 級別;并且 B+ 樹的葉子節點構成鏈表,非常有利于在內存中對數據進行 scan 操作。
- 磁盤操作效率高:B+ 樹的 Fanout 足夠大,樹的層級較少,呈矮胖狀,可以減少磁盤 IO 數;同時 B+ 樹的非葉子節點只存索引數據,葉子節點存實際數據,能大大壓縮樹高,進一步減少磁盤 IO 數。
- 數據結構高度統一:數據 & 索引都可以直接組織成 B+ 樹,因此代碼的可維護性、可讀性和開發效率都比較好。
僅有內存數據結構當然是不夠的,我們還需要設計高效的磁盤數據結構,下圖展示了從內存數據結構到磁盤數據結構的數據持久化過程:
左邊虛線框描繪的是 In Memory 結構示意圖。舉個例子,如果我們要修改 Page A 的某一行數據,對其中的一個字段進行自增,自增值是 2。自然而然會產生一個數據庫的物理操作日志,即 Redo Log,用來描述我們對 Page A 的修改。同時,在數據庫的事務執行過程中,可能還會產生大量臨時數據(圖里的 Temp data),當內存不夠用的時候也需要將其持久化。
右邊虛線框描繪的是 In Persistent Layer 的示意圖。假設我們使用比較友好的文件系統來將內存數據持久化,我們需要設置不同的文件,讓它們各司其職。例如圖里的藍色文件存儲 Page,綠色文件存儲 Redo Log,粉色文件存儲臨時數據。如果數據庫發生 crash ,在恢復階段我們就在各類文件中進行數據定位,結合 Redo Log File 和 Page File ,進行數據庫的數據恢復。除此之外,如果直接基于塊存儲進行持久化,就需要數據庫本身的存儲引擎管理好 LBA,需要在用戶態里面實現 buffer cache 等邏輯,這也是可行的。
那么基于單機的 FS / 塊存儲去做持久化,我們會遇到哪些問題呢?通過下面的單機數據庫系統的典型架構圖,我們可以發現三個問題:
- 單機容量瓶頸:在 Database 層的單機服務器上運行著 database 進程,服務器上掛載了大量本地磁盤用作數據持久化。但一臺物理服務器能掛載的磁盤容量總是有限的,這就導致了單機的容量瓶頸問題。
- 擴縮容困難:當容量、CPU 或者內存等資源不夠時,我們需要進行擴容。在單機時代,擴容意味著將數據從這個磁盤搬遷到另一個磁盤。但不管我們是通過網絡還是有線連接手段,都需要花費一定的時間,這可能導致業務較長時間的停寫(不可用),因此擴縮容是非常困難的。
- 多份獨立數據,成本高:如果我們要在“復制集”之間或者主備機之間去做數據冗余或數據同步,那么每新增一分計算能力(新增一個計算節點),就要新增一分存儲冗余,就會導致存儲成本提高。
分布式數據庫存儲解析
為了解決單機數據庫存儲系統面臨的問題和挑戰,字節跳動的數據庫團隊調研了一些業界主流的分布式數據庫方案。
MyRocks: MySQL + RocksDB
需要說明的是,MyRocks 不是分布式數據庫或者分布式解決方案,它是單機 SQL over kv 的典型代表。
- 核心理念:用 RocksDB 替換 InnoDB 。使用 RocksDB 能夠有效緩解單機容量瓶頸的問題;
- 特點:一是:數據可壓縮比例較高。RocksDB 實現了一種比較優秀的壓縮算法,根據實際調研結果顯示,在關系型數據庫場景,基本上它能實現 2-4 倍的壓縮比,能有效緩解單機的容量瓶頸問題。例如,單機原本掛載了 10 塊磁盤,只能承載 10 TB 數據,使用 RocksDB 就能在不改變硬件條件下幫助單機承載 20 TB 或 30 TB 等更多的數據;二是,順序寫性能較好,這也是 LSM-Tree 這種數據結構在 HDD 年代出現的核心原因。
- 難點:Compaction 會導致性能抖動,且兼容性一般。眾所周知,RocksDB 基于 LSM-Tree 構建,必然會遇到一些典型的 LSM-Tree-based 系統的問題。雖然 RocksDB 對順序寫特別友好,但它一定程度上犧牲了讀性能—— RocksDB 在讀的過程中會觸發 Compaction,可能引發性能抖動,導致前臺的寫出現卡頓現象;同時,這一類 SQL over kv 解決方案的兼容性能表現較為一般。
Amazon Aurora: 計算存儲分離
- 核心理念:計算存儲分離,Log is Database。
- 特點:架構靈活,存儲層帶有特定的數據庫計算邏輯,除了具備存儲能力之外,還具備 Redo Log 解析、回放生成數據庫 Page、維護多版本數據的能力。
- 優勢:兼容性強、讀擴展能力較優。基于共享存儲系統的特點,Amazon Aurora 的讀計算節點具備較好的擴展性,能夠實現一主 15 備的部署形態,其兼容性、讀擴展性表現較好。
Spanner 系: Shared-Nothing
- 核心理念:計算存儲分離,且 Share-Nothing。
- 特點:Spanner 系的數據庫系統一般基于分布式 k-v 存儲構建,由存儲層保證事務特性,計算層做成純計算的無狀態節點。
- 難點:要解決當前數據庫生態兼容性問題 & 分布式 k-v 系統的 hotspot 問題比較麻煩。
最佳實踐:veDB 分布式存儲系統
基于上述字節數據庫團隊的調研結果,我們設計了 veDB 分布式存儲系統以解決單機數據庫存儲系統面臨的問題與挑戰,本節將主要介紹 veDB 分布式存儲系統的系統目標與核心技術特點。
系統目標
在設計理念上,我們期望存儲系統能夠實現以下四個主要目標:
- 極致彈性:存儲節點與計算節點解耦,隨時彈性擴縮容;
- 極致易用性:構建 one-size-fits-all 的存儲系統,而非專用存儲,要能兼容多個主流數據庫(MySQL & PostgreSQL & Mongo……);
- 極致性價比:低時延、低成本;
- 極致可靠性:具備高性能、高可靠的備份恢復能力。
基于以上系統目標,數據庫團隊設計并開發了 veDB 分布式存儲系統,如下圖所示:
從圖中可以看出,分布式存儲層基于 LogStore 和 PageStore 這兩個子系統構建,其系統特點與我們的設計目標相互呼應。
- 高彈性:存儲層可獨立擴縮容,計算層完全不感知;
- 高性價比:在 LogStore 實現了高性能 Log 存儲 & 在 PageStore 實現了低成本 Page 存儲;
- 兼容性好:LogStore 和 PageStore 都支持多 DB Engine 插件化;
- 高可靠:PageStore 側支持 Segment 級別的 PITR 功能。
核心技術
以下主要從研發背景(Problem)、解決思路(Solution)、解決成效(Outcome)三個方面來分別介紹 veDB 分布式存儲系統的五個核心技術。
Distributed Data Model
Problem:從單機 FS 到分布式存儲,需要有高效的數據布局模型。基于單機的文件系統或塊存儲系統去實現數據持久化是比較簡單的,我們可以直接通過申請一批 LBA 或者一批文件來存儲數據,然后控制并發即可,但是這對于分布式存儲并不容易。從上面的示意圖可以看到,最上層的 Tablespace 代表一張數據庫表,里面可能包含上百萬甚至上千萬的 Page data(數據庫的基礎管理單元)。然而存儲系統的管理單元,卻不可能是 Page —— Page 的粒度過小,往往只有 KB 級別,如果存儲層以過小的粒度去管理數據,可能會造成元數據膨脹,增加管理成本。
Solution:Tablespace -> Segement 分布式映射。基于上述問題,我們可以在存儲層利用相對大的管理單元 Segment 去進行數據管理。此時,數據庫的管理單元是 Page,存儲系統的管理單元是 Segement。Tablespace 和 Segement 之間必然要存在一層映射關系,該映射關系可以根據不同數據庫引擎的數據管理空間大小要求進行設置,可能 MySQL 和 PostgreSQL 的映射規則就大不相同。上述示意圖展示了最簡單的模 2 規則,我們也可以發展出其他更加復雜的打散規則,此處不進行贅述。當我們將 Page 打散到對應的 Segement 之后,數據庫就不需要管數據 Replication 的邏輯,不管底層存儲是多副本還是 EC 策略,可以完全由存儲系統來做透明的 Replication ,數據庫就像在使用單機文件系統一樣簡單。
Outcome
- 天然負載均衡;
- 分布式打散,可最大程度實現并行計算;
- Scale in/out 簡單,僅需部分 Segement 數據 rebalance,摒棄了將整個數據庫表的 TB 級數據在硬盤間搬遷的繁雜流程。
Log-Only Segement
Problem:數據冗余成本高,需要降低存儲成本。
Solution:開發 Log-Only Segement,節省非必要的 Page 副本空間。什么是 Log-only segement? 關系型數據庫中往往都包含 Log 數據和 Page 數據。在存儲層中,存了多副本的 Log 數據后,我們可以選擇性地只回放一部分 Log 數據來生成 Page,讓另一部分 Log 數據保持不動,不要生成任何 Page 數據。以上面的示意圖為例,Rep_0 和 Rep_1 都是 Log 數據生成的各種版本 Page 數據,然而 Rep_2 是一個空的 Page 數據副本,它里面只有 Redo log。我們都知道 Redo log 和 Page data 的數據大小比例是比較夸張的,Page data 的大小可能是 Redo log 的幾倍甚至十幾倍,因此通過以上方法能夠較大的節省單機的 Page 存儲空間。
Outcome:結合單機引擎的壓縮算法,能將存儲空間放大倍數從 3.x -> 1.x,較好緩解成本問題。
高性能 IO 引擎
Problem:存儲層寫性能容易成為系統性能瓶頸,如何解決?
Solution:全異步 IO + 無鎖結構 +并發打散。當數據庫提交了一個 Redo log 到 Log Storage 之后,Log Storage 中會有一個無鎖的 Ring buffer 去對 Redo log 進行有序組織,然后我們將 Redo Log 的 Ring buffer 進行線性的定長切割,并發打散到底層存儲的 Blob 單元。
Outcome:4KB + depth 8,write latency ~100+us,較好支撐了數據庫下發日志的性能剛需。
PITR
PITR(Point-in-time Recovery)是指我們都可以迅速地恢復在過去一段時間內某個時間點的數據庫快照。
Problem:如何快速備份恢復,且降低對前臺業務影響?
Solution:基于 Segement 的高并發 PITR 機制,Segement 間互不影響。之前提到存儲層的管理單元是 Segement ,我們也可以基于 Segement 做備份恢復。這樣做有兩個好處:首先計算層是完全透明的,計算層完全不會感知,并且計算層的性能不會抖動。其次基于 Segment 可以做到天然的并發打散,因此備份恢復也可以做到并發恢復。
Outcome
- 性能優秀,恢復 1TB 數據 ~15min;
- 擴展性強,不受數據大小影響,性能與數據大小呈近常數關系:因為基于 Segment 單元去做并發備份恢復,每個 Segment 都是獨立的,其性能能夠與數據大小解耦開來。因此不管數據大小是多少,只要備份恢復資源足夠,都能做到常數級的備份恢復性能。
多計算引擎插件化
Problem:數據庫團隊希望統一的存儲層能夠支持不同的數據庫引擎,做到 100% 兼容和快速接入。
Solution:Write Ahead Log + Log Replay = 任意 Page Data。基于本地存儲引擎的 k-v 結構,或者基于裸的塊設備抽象出一種相對通用的數據結構,從而高效地存儲 Page data。同時,我們在 SDK 側和 Server 側都做了 Log parse 的插件化,要接入新的數據庫引擎只需要其提供適配存儲接口的日志插件,從而可以快速接入各式各樣的數據庫計算引擎。
Outcome:
- 基于統一接口,計算引擎僅需提供 Log parse + Replay lib 即可接入 veDB 存儲層。
- 統一存儲層已支持 MySQL、PostgreSQL、MongoDB 計算引擎,目前仍在持續拓展。
數據庫存儲系統:What's Next
在談及數據庫存儲的未來演進時,首先我們可以思考一下哪些因素會觸發數據庫存儲架構的變革和演進?答案可能包含:存儲架構自身的革命、數據庫理論的突破、或者新硬件沖擊引發存儲系統架構迭代。基于這三個方向的思考,我們總結了以下幾個數據庫存儲系統的演進趨勢:
HTAP/HSAP
我們總結的第一個趨勢即 HTAP/HSAP 系統將會逐漸爆發。在 HTAP/HSAP 系統中,“實時”是第一關鍵詞。為了支持實時,存儲系統可能會發生架構演進和變革,因此我們需要探索:
- 行列存 All-in-one:既要存儲行式的數據,又要存儲列式的數據。
- 近實時,寫時計算:我們需要在存儲層實現寫時計算的邏輯來支持實時性。
AI Enhancement
AI 技術運用領域廣泛,具體在數據庫存儲領域,我們可以利用 AI 技術進行以下工作:
- 存儲參數調優;
- 智能存儲格式:利用 AI 技術進行智能的行存和列存格式轉換,AI 可以提醒我們什么時間進行轉換,什么時候絕對不能轉換,從而避免格式轉換為前臺業務帶來的性能 overhead。
Hardware Revolution
在硬件變革趨勢上,我們總結了三個變革方向:
- 存儲介質變革:前幾年,我們可能更多關注 SSD、HDD。目前我們處于 SSD 往 persistent memory 轉變的風口,那么如何利用 persistent memory 去定制軟件架構?目前看在文件系統側已經有一些研究,但是在數據庫側并沒有太多公開實踐。
- 計算單元變革:CPU 產品已經從 multi-core 變成了 many-core (從 96c 變成了 192c、384c)。要怎么利用多核的能力?對于非計算密集型的存儲系統而言,多余的算力能否用來加速數據庫算子?一些無鎖的數據結構是不是需要要重新設計?以上都需要我們認真考慮。
- 網絡設施變革:例如 RDMA ,以及可編程的 P4 交換機這類全新的一些網絡設施,可能會對我們的軟件架構特別是分布式存儲架構造成較大的沖擊。相應地,我們需要在存儲側做出調整。