在安裝MariaDB的時候了解到代替InnoDB的TokuDB,看簡介非常的棒,這里對ToduDB做一個初步的整理,使用后再做更多的分享。
什么是TokuDB?
在MySQL最流行的支持全事務的引擎為INNODB。其特點是數據本身是用B-TREE來組織,數據本身即是龐大的根據主鍵聚簇的B-TREE索引。 所以在這點上,寫入速度就會有些降低,因為要每次寫入要用一次IO來做索引樹的重排。特別是當數據量本身比內存大很多的情況下,CPU本身被磁盤IO糾纏的做不了其他事情了。這時我們要考慮如何減少對磁盤的IO來排解CPU的處境,常見的方法有:
- 把INNODB 個PAGE增大(默認16KB),但增大也就帶來了一些缺陷。 比如,對磁盤進行CHECKPOINT的時間將延后。
- 把日志文件放到更快速的磁盤上,比如SSD。
TokuDB 是一個支持事務的“新”引擎,有着出色的數據壓縮功能,由美國 TokuTek 公司(現在已經被 Percona 公司收購)研發。擁有出色的數據壓縮功能,如果您的數據寫多讀少,而且數據量比較大,強烈建議您使用TokuDB,以節省空間成本,並大幅度降低存儲使用量和IOPS開銷,不過相應的會增加 CPU 的壓力。
TokuDB 的特性
1.豐富的索引類型以及索引的快速創建
TokuDB 除了支持現有的索引類型外, 還增加了(第二)集合索引, 以滿足多樣性的覆蓋索引的查詢, 在快速創建索引方面提高了查詢的效率
2.(第二)集合索引
也可以稱作非主鍵的集合索引, 這類索引也包含了表中的所有列, 可以用於覆蓋索引的查詢需要, 比如以下示例, 在where 條件中直接命中 index_b 索引, 避免了從主鍵中再查找一次.
1
2
3
4
5
6
7
8
9
10
|
CREATE TABLE table (
column_a INT,
column_b INT,
column_c INT,
PRIMARY KEY index_a (column_a),
CLUSTERING KEY index_b (column_b)) ENGINE = TokuDB;
SELECT column_c
FROM table
WHERE column_b BETWEEN 10 AND 100;
|
見: http://tokutek.com/2009/05/introducing_multiple_clustering_indexes/
3.索引在線創建(Hot Index Creation)
TokuDB 允許直接給表增加索引而不影響更新語句(insert, update 等)的執行。可以通過變量 tokudb_create_index_online 來控制是否開啟該特性, 不過遺憾的是目前還只能通過 CREATE INDEX 語法實現在線創建, 不能通過 ALTER TABLE 實現. 這種方式比通常的創建方式慢了許多, 創建的過程可以通過 show processlist 查看。不過 tokudb 不支持在線刪除索引, 刪除索引的時候會對標加全局鎖。
1
2
3
4
|
> SET tokudb_create_index_online=ON;
Query OK, 0 rows affected (0.00 sec)
> CREATE INDEX index ON table (field_name);
|
4.在線更改列(Add, Delete, Expand, Rename)
TokuDB 可以在輕微阻塞更新或查詢語句的情況下, 允許實現以下操作:
- 增加或刪除表中的列
- 擴充字段: char, varchar, varbinary 和 int 類型的列
- 重命名列, 不支持字段類型: TIME, ENUM, BLOB, TINYBLOB, MEDIUMBLOB, LONGBLOB
這些操作通常是以表鎖級別阻塞(幾秒鍾時間)其他查詢的執行, 當表記錄下次從磁盤加載到內存的時候, 系統就會隨之對記錄進行修改操作(add, delete 或 expand), 如果是 rename 操作, 則會在幾秒鍾的停機時間內完成所有操作。
TokuDB的這些操作不同於 InnoDB, 對表進行更新后可以看到 rows affected 為 0, 即更改操作會放到后台執行, 比較快速的原因可能是由於 Fractal-tree 索引的特性, 將隨機的 IO 操作替換為順序 IO 操作, Fractal-tree的特性中, 會將這些操作廣播到所有行, 不像 InnoDB, 需要 open table 並創建臨時表來完成.
看看官方對該特性的一些指導說明:
- 所有的這些操作不是立即執行, 而是放到后台中由 Fractal Tree 完成, 操作包括主鍵和非主鍵索引。也可以手工強制執行這些操作, 使用 OPTIMIZE TABLE X 命令即可, TokuDB 從1.0 開始OPTIMIZE TABLE命令也支持在線完成, 但是不會重建索引
- 不要一次更新多列, 分開對每列進行操作
- 避免同時對一列進行 add, delete, expand 或 drop 操作
- 表鎖的時間主要由緩存中的臟頁(dirty page)決定, 臟頁越多 flush 的時間就越長. 每做一次更新, MySQL 都會關閉一次表的連接以釋放之前的資源
- 避免刪除的列是索引的一部分, 這類操作會特別慢, 非要刪除的話可以去掉索引和該列的關聯再進行刪除操作
- 擴充類的操作只支持 char, varchar, varbinary 和 int 類型的字段
- 一次只 rename 一列, 操作多列會降級為標准的 MySQL 行為, 語法中列的屬性必須要指定上, 如下:
-
123ALTER TABLE tableCHANGE column_old column_newDATA_TYPE REQUIRED_NESS DEFAULT
-
- rename 操作還不支持字段: TIME, ENUM, BLOB, TINYBLOB, MEDIUMBLOB, LONGBLOB.
- 不支持更新臨時表;
5.數據壓縮
TokuDB中所有的壓縮操作都在后台執行, 高級別的壓縮會降低系統的性能, 有些場景下會需要高級別的壓縮. 按照官方的建議: 6核數以下的機器建議標准壓縮, 反之可以使用高級別的壓縮。
每個表在 create table 或 alter table 的時候通過 ROW_FORMAT 來指定壓縮的算法:
1
2
3
4
|
CREATE TABLE table (
column_a INT NOT NULL PRIMARY KEY,
column_b INT NOT NULL) ENGINE=TokuDB
ROW_FORMAT=row_format;
|
ROW_FORMAT默認由變量 tokudb_row_format 控制, 默認為 tokudb_zlib, 可以的值包括:
- tokudb_zlib: 使用 zlib 庫的壓縮模式,提供了中等級別的壓縮比和中等級別的CPU消耗。
- tokudb_quicklz: 使用 quicklz 庫的壓縮模式, 提供了輕量級的壓縮比和較低基本的CPU消耗。
- tokudb_lzma: 使用lzma庫壓縮模式,提供了高壓縮比和高CPU消耗。
- tokudb_uncompressed: 不使用壓縮模式。
6.Read free 復制特性
得益於 Fracal Tree 索引的特性, TokuDB 的 slave 端能夠以低於讀IO的消耗來應用 master 端的變化, 其主要依賴 Fractal Tree 索引的特性,可以在配置里啟用特性
- insert/delete/update操作部分可以直接插入到合適的 Fractal Tree 索引中, 避免 read-modify-write 行為的開銷;
- delete/update 操作可以忽略唯一性檢查帶來的 IO 方面的開銷
不好的是, 如果啟用了 Read Free Replication 功能, Server 端需要做如下設置:
- master:復制格式必須為 ROW, 因為 tokudb 還沒有實現對 auto-increment函數進行加鎖處理, 所以多個並發的插入語句可能會引起不確定的 auto-increment值, 由此造成主從兩邊的數據不一致.
- slave:開啟 read-only; 關閉唯一性檢查(set tokudb_rpl_unique_checks=0);關閉查找(read-modify-write)功能(set tokudb_rpl_lookup_rows=0);
slave 端的設置可以在一台或多台 slave 中設置:MySQL5.5 和 MariaDB5.5中只有定義了主鍵的表才能使用該功能, MySQL 5.6, Percona 5.6 和 MariaDB 10.X 沒有此限制
7.事務, ACID 和恢復
- 默認情況下, TokuDB 定期檢查所有打開的表, 並記錄 checkpoint 期間所有的更新, 所以在系統崩潰的時候, 可以恢復表到之前的狀態(ACID-compliant), 所有的已提交的事務會更新到表里,未提交的事務則進行回滾. 默認的檢查周期每60s一次, 是從當前檢查點的開始時間到下次檢查點的開始時間, 如果 checkpoint 需要更多的信息, 下次的checkpoint 檢查會立即開始, 不過這和 log 文件的頻繁刷新有關. 用戶也可以在任何時候手工執行 flush logs 命令來引起一次 checkpoint 檢查; 在數據庫正常關閉的時候, 所有開啟的事務都會被忽略.
- 管理日志的大小: TokuDB 一直保存最近的checkpoing到日志文件中, 當日志達到100M的時候, 會起一個新的日志文件; 每次checkpoint的時候, 日志中舊於當前檢查點的都會被忽略, 如果檢查的周期設置非常大, 日志的清理頻率也會減少。 TokuDB也會為每個打開的事務維護回滾日志, 日志的大小和事務量有關, 被壓縮保存到磁盤中, 當事務結束后,回滾日志會被相應清理.
- 恢復: TokuDB自動進行恢復操作, 在崩潰后使用日志和回滾日志進行恢復, 恢復時間由日志大小(包括未壓縮的回滾日志)決定.
- 禁用寫緩存: 如果要保證事務安全, 就得考慮到硬件方面的寫緩存. TokuDB 在 MySQL 里也支持事務安全特性(transaction safe), 對系統而言, 數據庫更新的數據不一樣真的寫到磁盤里, 而是緩存起來, 在系統崩潰的時候還是會出現丟數據的現象, 比如TokuDB不能保證掛載的NFS卷可以正常恢復, 所以如果要保證安全,最好關閉寫緩存, 但是可能會造成性能的降低.通常情況下需要關閉磁盤的寫緩存, 不過考慮到性能原因, XFS文件系統的緩存可以開啟, 不過穿線錯誤”Disabling barriers”后,就需要關閉緩存. 一些場景下需要關閉文件系統(ext3)緩存, LVM, 軟RAID 和帶有 BBU(battery-backed-up) 特性的RAID卡
8.過程追蹤
TokuDB 提供了追蹤長時間運行語句的機制. 對 LOAD DATA 命令來說,SHOW PROCESSLIST 可以顯示過程信息, 第一個是類似 “Inserted about 1000000 rows” 的狀態信息, 下一個是完成百分比的信息, 比如 “Loading of data about 45% done”; 增加索引的時候, SHOW PROCESSLIST 可以顯示 CREATE INDEX 和 ALTER TABLE 的過程信息, 其會顯示行數的估算值, 也會顯示完成的百分比; SHOW PROCESSLIST 也會顯示事務的執行情況, 比如 committing 或 aborting 狀態.
9.遷移到 TokuDB
可以使用傳統的方式更改表的存儲引擎, 比如 “ALTER TABLE … ENGINE = TokuDB” 或 mysqldump 導出再倒入, INTO OUTFILE 和 LOAD DATA INFILE 的方式也可以。
10.熱備
Percona Xtrabackup 還未支持 TokuDB 的熱備功能, percona 也為表示有支持的打算 http://www.percona.com/blog/2014/07/15/tokudb-tips-mysql-backups/ ;對於大表可以使用 LVM 特性進行備份, https://launchpad.net/mylvmbackup , 或 mysdumper 進行備份。TokuDB 官方提供了一個熱備插件 tokudb_backup.so, 可以進行在線備份, 詳見 https://github.com/Tokutek/tokudb-backup-plugin, 不過其依賴 backup-enterprise, 無法編譯出 so 動態庫, 是個商業的收費版本, 見 https://www.percona.com/doc/percona-server/5.6/tokudb/tokudb_installation.html
總結
TokuDB的優點:
- 高壓縮比,默認使用zlib進行壓縮,尤其是對字符串(varchar,text等)類型有非常高的壓縮比,比較適合存儲日志、原始數據等。官方宣稱可以達到1:12。
- 在線添加索引,不影響讀寫操作
- HCADER 特性,支持在線字段增加、刪除、擴展、重命名操作,(瞬間或秒級完成)
- 支持完整的ACID特性和事務機制
- 非常快的寫入性能, Fractal-tree在事務實現上有優勢,無undo log,官方稱至少比innodb高9倍。
- 支持show processlist 進度查看
- 數據量可以擴展到幾個TB;
- 不會產生索引碎片;
- 支持hot column addition,hot indexing,mvcc
TokuDB缺點:
- 不支持外鍵(foreign key)功能,如果您的表有外鍵,切換到 TokuDB引擎后,此約束將被忽略。
- TokuDB 不適大量讀取的場景,因為壓縮解壓縮的原因。CPU占用會高2-3倍,但由於壓縮后空間小,IO開銷低,平均響應時間大概是2倍左右。
- online ddl 對text,blob等類型的字段不適用
- 沒有完善的熱備工具,只能通過mysqldump進行邏輯備份
適用場景:
- 訪問頻率不高的數據或歷史數據歸檔
- 數據表非常大並且時不時還需要進行DDL操作
TokuDB的索引結構–分形樹的實現
TokuDB和InnoDB最大的不同在於TokuDB采用了一種叫做Fractal Tree的索引結構,使其在隨機寫數據的處理上有很大提升。目前無論是SQL Server,還是MySQL的innodb,都是用的B+Tree(SQL Server用的是標准的B-Tree)的索引結構。InnoDB是以主鍵組織的B+Tree結構,數據按照主鍵順序排列。對於順序的自增主鍵有很好的性能,但是不適合隨機寫入,大量的隨機I/O會使數據頁分裂產生碎片,索引維護開銷很多大。TokuDB解決隨機寫入的問題得益於其索引結構,Fractal Tree 和 B-Tree的差別主要在於索引樹的內部節點上,B-Tree索引的內部結構只有指向父節點和子節點的指針,而Fractal Tree的內部節點不僅有指向父節點和子節點的指針,還有一塊Buffer區。當數據寫入時會先落到這個Buffer區上,該區是一個FIFO結構,寫是一個順序的過程,和其他緩沖區一樣,滿了就一次性刷寫數據。所以TokuDB上插入數據基本上變成了一個順序添加的過程。
BTree和Fractal tree的比較:
Structure | Inserts | Point Queries | Range Queries |
B-Tree | Horrible | Good | Good (young) |
Append | Wonderful | Horrible | Horrible |
Fractal Tree | Good | Good | Good |
Fractal tree(分形樹)簡介
分形樹是一種寫優化的磁盤索引數據結構。 在一般情況下, 分形樹的寫操作(Insert/Update/Delete)性能比較好,同時它還能保證讀操作近似於B+樹的讀性能。據Percona公司測試結果顯示, TokuDB分形樹的寫性能優於InnoDB的B+樹), 讀性能略低於B+樹。
ft-index的磁盤存儲結構
ft-index采用更大的索引頁和數據頁(ft-index默認為4M, InnoDB默認為16K), 這使得ft-index的數據頁和索引頁的壓縮比更高。也就是說,在打開索引頁和數據頁壓縮的情況下,插入等量的數據, ft-index占用的存儲空間更少。ft-index支持在線修改DDL (Hot Schema Change)。 簡單來講,就是在做DDL操作的同時(例如添加索引),用戶依然可以執行寫入操作, 這個特點是ft-index樹形結構天然支持的。 此外, ft-index還支持事務(ACID)以及事務的MVCC(Multiple Version Cocurrency Control 多版本並發控制), 支持崩潰恢復。正因為上述特點, Percona公司宣稱TokuDB一方面帶給客戶極大的性能提升, 另一方面還降低了客戶的存儲使用成本。
ft-index的索引結構圖如下:
灰色區域表示ft-index分形樹的一個頁,綠色區域表示一個鍵值,兩格綠色區域之間表示一個兒子指針。 BlockNum表示兒子指針指向的頁的偏移量。Fanout表示分形樹的扇出,也就是兒子指針的個數。 NodeSize表示一個頁占用的字節數。NonLeafNode表示當前頁是一個非葉子節點,LeafNode表示當前頁是一個葉子節點,葉子節點是最底層的存放Key-value鍵值對的節點, 非葉子節點不存放value。 Heigth表示樹的高度, 根節點的高度為3, 根節點下一層節點的高度為2, 最底層葉子節點的高度為1。Depth表示樹的深度,根節點的深度為0, 根節點的下一層節點深度為1。
分形樹的樹形結構非常類似於B+樹, 它的樹形結構由若干個節點組成(我們稱之為Node或者Block,在InnoDB中,我們稱之為Page或者頁)。 每個節點由一組有序的鍵值組成。假設一個節點的鍵值序列為[3, 8], 那么這個鍵值將(-00, +00)整個區間划分為(-00, 3), [3, 8), [8, +00) 這樣3個區間, 每一個區間就對應着一個兒子指針(Child指針)。 在B+樹中, Child指針一般指向一個頁, 而在分形樹中,每一個Child指針除了需要指向一個Node的地址(BlockNum)之外,還會帶有一個Message Buffer (msg_buffer), 這個Message Buffer 是一個先進先出(FIFO)的隊列,用來存放Insert/Delete/Update/HotSchemaChange這樣的更新操作。
按照ft-index源代碼的實現, 對ft-index中分形樹更為嚴謹的說法:
- 節點(block或者node, 在InnoDB中我們稱之為Page或者頁)是由一組有序的鍵值組成, 第一個鍵值設置為null鍵值, 表示負無窮大。
- 節點分為兩種類型,一種是葉子節點, 一種是非葉子節點。 葉子節點的兒子指針指向的是BasementNode, 非葉子節點指向的是正常的Node 。 這里的BasementNode節點存放的是多個K-V鍵值對, 也就是說最后所有的查找操作都需要定位到BasementNode才能成功獲取到數據(Value)。這一點也和B+樹的LeafPage類似, 數據(Value)都是存放在葉子節點, 非葉子節點用來存放鍵值(Key)做索引。 當葉子節點加載到內存后,為了快速查找到BasementNode中的數據(Value), ft-index會把整個BasementNode中的key-value都轉換為一棵弱平衡二叉樹, 這棵平衡二叉樹有一個很逗逼的名字,叫做替罪羊樹。
- 每個節點的鍵值區間對應着一個兒子指針(Child Pointer)。 非葉子節點的兒子指針攜帶着一個MessageBuffer, MessageBuffer是一個FIFO隊列。用來存放Insert/Delete/Update/HotSchemaChange這樣的更新操作。兒子指針以及MessageBuffer都會序列化存放在Node的磁盤文件中。
- 每個非葉子節點(Non Leaf Node)兒子指針的個數必須在[fantout/4, fantout]這個區間之內。 這里fantout是分形樹(B+樹也有這個概念)的一個參數,這個參數主要用來維持樹的高度。當一個非葉子節點的兒子指針個數小於fantout/4 , 那么我們認為這個節點的太空虛了,需要和其他節點合並為一個節點(Node Merge), 這樣能減少整個樹的高度。當一個非葉子節點的兒子指針個數超過fantout, 那么我們認為這個節點太飽滿了, 需要將一個節點一拆為二(Node Split)。 通過這種約束控制,理論上就能將磁盤數據維持在一個正常的相對平衡的樹形結構,這樣可以控制插入和查詢復雜度上限。
- 注意: 在ft-index實現中,控制樹平衡的條件更加復雜, 例如除了考慮fantout之外,還要保證節點總字節數在[NodeSize/4, NodeSize]這個區間, NodeSize一般為4M ,當不在這個區間時, 需要做對應的合並(Merge)或者分裂(Split)操作。
分形樹的Insert/Delete/Update實現
我們說到分形樹是一種寫優化的數據結構, 它的寫操作性能要優於B+樹的寫操作性能。 那么它究竟如何做到更優的寫操作性能呢?首先, 這里說的寫操作性能,指的是隨機寫操作。 舉個簡單例子,假設我們在MySQL的InnoDB表中不斷執行這個SQL語句: insert into sbtest set x = uuid(), 其中sbtest表中有一個唯一索引字段為x。 由於uuid()的隨機性,將導致插入到sbtest表中的數據散落在各個不同的葉子節點(Leaf Node)中。 在B+樹中, 大量的這種隨機寫操作將導致LRU-Cache中大量的熱點數據頁落在B+樹的上層(如下圖所示)。這樣底層的葉子節點命中Cache的概率降低,從而造成大量的磁盤IO操作, 也就導致B+樹的隨機寫性能瓶頸。但B+樹的順序寫操作很快,因為順序寫操作充分利用了局部熱點數據, 磁盤IO次數大大降低。
下面來說說分形樹插入操作的流程。 為了方便后面描述,約定如下:
- 以Insert操作為例, 假定插入的數據為(Key, Value)
- 加載節點(Load Page),都是先判斷該節點是否命中LRU-Cache。僅當緩存不命中時, ft-index才會通過seed定位到偏移量讀取數據頁到內存
- 暫時不考慮崩潰日志和事務處理。
詳細流程如下:
- 加載Root節點;
- 判斷Root節點是否需要分裂(或合並),如果滿足分裂(或者合並)條件,則分裂(或者合並)Root節點。 具體分裂Root節點的流程,感興趣的同學可以開開腦洞。
- 當Root節點height>0, 也就是Root是非葉子節點時, 通過二分搜索找到Key所在的鍵值區間Range,將(Key, Value)包裝成一條消息(Insert, Key, Value) , 放入到鍵值區間Range對應的Child指針的Message Buffer中。
- 當Root節點height=0時,即Root是葉子節點時, 將消息(Insert, Key, Value) 應用(Apply)到BasementNode上, 也就是插入(Key, Value)到BasementNode中。
這里有一個非常詭異的地方,在大量的插入(包括隨機和順序插入)情況下, Root節點會經常性的被撐飽滿,這將會導致Root節點做大量的分裂操作。然后,Root節點做了大量的分裂操作之后,產生大量的height=1的節點, 然后height=1的節點被撐爆滿之后,又會產生大量height=2的節點, 最終樹的高度越來越高。 這個詭異的之處就隱藏了分形樹寫操作性能比B+樹高的秘訣: 每一次插入操作都落在Root節點就馬上返回了, 每次寫操作並不需要搜索樹形結構最底層的BasementNode, 這樣會導致大量的熱點數據集中落在在Root節點的上層(此時的熱點數據分布圖類似於上圖), 從而充分利用熱點數據的局部性,大大減少了磁盤IO操作。
Update/Delete操作的情況和Insert操作的情況類似, 但是需要特別注意的地方在於,由於分形樹隨機讀性能並不如InnoDB的B+樹。因此,Update/Delete操作需要細分為兩種情況考慮,這兩種情況測試性能可能差距巨大:
- 覆蓋式的Update/Delete (overwrite)。 也就是當key存在時, 執行Update/Delete; 當key不存在時,不做任何操作,也不需要報錯。
- 嚴格匹配的Update/Delete。 當key存在時, 執行update/delete ; 當key不存在時, 需要報錯給上層應用方。 在這種情況下,我們需要先查詢key是否存在於ft-index的basementnode中,於是Point-Query默默的拖了Update/Delete操作的性能后退。
此外,ft-index為了提升順序寫的性能,對順序插入操作做了一些優化,例如順序寫加速。
分形樹的Point-Query實現
在ft-index中, 類似select from table where id = ? (其中id是索引)的查詢操作稱之為Point-Query; 類似select from table where id >= ? and id <= ? (其中id是索引)的查詢操作稱之為Range-Query。 上文已經提到, Point-Query讀操作性能並不如InnoDB的B+樹, 這里詳細描述Point-Query的相關流程。 (這里假設要查詢的鍵值為Key)
- 加載Root節點,通過二分搜索確定Key落在Root節點的鍵值區間Range, 找到對應的Range的Child指針。
- 加載Child指針對應的的節點。 若該節點為非葉子節點,則繼續沿着分形樹一直往下查找,一直到葉子節點停止。 若當前節點為葉子節點,則停止查找。
查找到葉子節點后,我們並不能直接返回葉子節點中的BasementNode的Value給用戶。 因為分形樹的插入操作是通過消息(Message)的方式插入的, 此時需要把從Root節點到葉子節點這條路徑上的所有消息依次apply到葉子節點的BasementNode。 待apply所有的消息完成之后,查找BasementNode中的key對應的value,就是用戶需要查找的值。
分形樹的查找流程基本和 InnoDB的B+樹的查找流程類似, 區別在於分形樹需要將從Root節點到葉子節點這條路徑上的messge buffer都往下推,並將消息apply到BasementNode節點上。注意查找流程需要下推消息, 這可能會造成路徑上的部分節點被撐飽滿,但是ft-index在查詢過程中並不會對葉子節點做分裂和合並操作, 因為ft-index的設計原則是: Insert/Update/Delete操作負責節點的Split和Merge, Select操作負責消息的延遲下推(Lazy Push)。 這樣,分形樹就將Insert/Delete/Update這類更新操作通過未來的Select操作應用到具體的數據節點,從而完成更新。
分形樹的Range-Query實現
下面來介紹Range-Query的查詢實現。簡單來講, 分形樹的Range-Query基本等價於進行N次Point-Query操作,操作的代價也基本等價於N次Point-Query操作的代價。 由於分形樹在非葉子節點的msg_buffer中存放着BasementNode的更新操作,因此我們在查找每一個Key的Value時,都需要從根節點查找到葉子節點, 然后將這條路徑上的消息apply到basenmentNode的Value上。 這個流程可以用下圖來表示。
但是在B+樹中, 由於底層的各個葉子節點都通過指針組織成一個雙向鏈表, 結構如下圖所示。 因此,我們只需要從跟節點到葉子節點定位到第一個滿足條件的Key, 然后不斷在葉子節點迭代next指針,即可獲取到Range-Query的所有Key-Value鍵值。因此,對於B+樹的Range-Query操作來說,除了第一次需要從root節點遍歷到葉子節點做隨機寫操作,后繼數據讀取基本可以看做是順序IO。
通過比較分形樹和B+樹的Range-Query實現可以發現, 分形樹的Range-Query查詢代價明顯比B+樹代價高,因為分型樹需要遍歷Root節點的覆蓋Range的整顆子樹,而B+樹只需要一次Seed到Range的起始Key,后續迭代基本等價於順序IO。
總結
總體來說,分形樹是一種寫優化的數據結構,它的核心思想是利用節點的MessageBuffer緩存更新操作,充分利用數據局部性原理, 將隨機寫轉換為順序寫,這樣極大的提高了隨機寫的效率。Tokutek研發團隊的iiBench測試結果顯示: TokuDB的insert操作(隨機寫)的性能比InnoDB快很多,而Select操作(隨機讀)的性能低於InnoDB的性能,但是差距較小,同時由於TokuDB采用有4M的大頁存儲,使得壓縮比較高。這也是Percona公司宣稱TokuDB更高性能,更低成本的原因。
另外,在線更新表結構(Hot Schema Change)實現也是基於MessageBuffer來實現的, 但和Insert/Delete/Update操作不同的是, 前者的消息下推方式是廣播式下推(父節點的一條消息,應用到所有的兒子節點), 后者的消息下推方式單播式下推(父節點的一條消息,應用到對應鍵值區間的兒子節點), 由於實現類似於Insert操作,所以不再展開描述。
TokuDB的多版本並發控制(MVCC)
在傳統的關系型數據庫(例如Oracle, MySQL, SQLServer)中,事務可以說是研發和討論最核心內容。而事務最核心的性質就是ACID。
- A表示原子性,也就是組成事務的所有子任務只有兩種結果:要么隨着事務的提交,所有子任務都成功執行;要么隨着事務的回滾,所有子任務都撤銷。
- C表示一致性,也就是無論事務提交或者回滾,都不能破壞數據的一致性約束,這些一致性約束包括鍵值唯一約束、鍵值關聯關系約束等。
- I表示隔離性,隔離性一般是針對多個並發事務而言的,也就是在同一個時間點,t1事務和t2事務讀取的數據應該是隔離的,這兩個事務就好像進了同一酒店的兩間房間一樣,各自在各自的房間里面活動,他們相互之間並不能看到各自在干嘛。
- D表示持久性,這個性質保證了一個事務一旦承諾用戶成功提交,那么即便是后繼數據庫進程crash或者操作系統crash,只要磁盤數據沒壞,那么下次啟動數據庫后,這個事務的執行結果仍然可以讀取到。
TokuDB目前完全支持事務的ACID。 從實現上看, 由於TokuDB采用的分形樹作為索引,而InnoDB采用B+樹作為索引結構,因而TokuDB在事務的實現上和InnoDB有很大不同。
在InnoDB中, 設計了redo和undo兩種日志,redo存放頁的物理修改日志,用來保證事務的持久性; undo存放事務的邏輯修改日志,它實際存放了一條記錄在多個並發事務下的多個版本,用來實現事務的隔離性(MVCC)和回滾操作。由於TokuDB的分形樹采用消息傳遞的方式來做增刪改更新操作,一條消息就是事務對該記錄修改的一個版本,因此,在TokuDB源碼實現中,並沒有額外的undo-log的概念和實現,取而代之的是一條記錄多條消息的管理機制。雖然一條記錄多條消息的方式可以實現事務的MVCC,卻無法解決事務回滾的問題,因此TokuDB額外設計了tokudb.rollback這個日志文件來做幫助實現事務回滾。
這里主要分析TokuDB的事務隔離性的實現,也就是常提到的多版本並發控制(MVCC)。
TokuDB的事務表示
在tokudb中, 在用戶執行的一個事務,具體到存儲引擎層面會被拆開成許多個小事務(這種小事務記為txn)。 例如用戶執行這樣一個事務:
1
2
3
|
begin;
insert into hello set id = 1, value = '1';
commit;
|
對應到TokuDB存儲引擎的redo-log中的記錄為:
1
2
3
4
5
6
|
xbegin 'b': lsn=236599 xid=15,0 parentxid=0,0 crc=29e4d0a1 len=53
xbegin 'b': lsn=236600 xid=15,1 parentxid=15,0 crc=282cb1a1 len=53
enq_insert 'I': lsn=236601 filenum=13 xid=15,1 key={...} value={...} crc=a42128e5 len=58
xcommit 'C': lsn=236602 xid=15,1 crc=ec9bba3d len=37
xprepare 'P': lsn=236603 xid=15,0 xa_xid={...} crc=db091de4 len=67
xcommit 'C': lsn=236604 xid=15,0 crc=ec997b3d len=37
|
對應的事務樹如下圖所示:
對一個較為復雜一點,帶有savepoint的事務例子:
1
2
3
4
5
6
|
begin;
insert into hello set id = 2, value = '2' ;
savepoint mark1;
insert into hello set id = 3, value = '3' ;
savepoint mark2;
commit;
|
對應的redo-log的記錄為:
1
2
3
4
5
6
7
8
9
10
11
|
xbegin 'b': lsn=236669 xid=17,0 parentxid=0,0 crc=c01888a6 len=53
xbegin 'b': lsn=236670 xid=17,1 parentxid=17,0 crc=cf400ba6 len=53
enq_insert 'I': lsn=236671 filenum=13 xid=17,1 key={...} value={...} crc=8ce371e3 len=58
xcommit 'C': lsn=236672 xid=17,1 crc=ec4a923d len=37
xbegin 'b': lsn=236673 xid=17,2 parentxid=17,0 crc=cb7c6fa6 len=53
xbegin 'b': lsn=236674 xid=17,3 parentxid=17,2 crc=c9a4c3a6 len=53
enq_insert 'I': lsn=236675 filenum=13 xid=17,3 key={...} value={...} crc=641148e2 len=58
xcommit 'C': lsn=236676 xid=17,3 crc=ec4e143d len=37
xcommit 'C': lsn=236677 xid=17,2 crc=ec4cf43d len=37
xprepare 'P': lsn=236678 xid=17,0 xa_xid={...} crc=76e302b4 len=67
xcommit 'C': lsn=236679 xid=17,0 crc=ec42b43d len=37
|
這個事務組成的一棵事務樹如下:
在tokudb中,使用{parent_id, child_id}這樣一個二元組來記錄一個txn和其他txn的依賴關系。這樣從根事務到葉子幾點的一組標號就可以唯一標示一個txn, 這一組標號列表稱之為xids, xids我認為也可以稱為事務號。 例如txn3的xids = {17, 2, 3 } , txn2的xids = {17, 2}, txn1的xids= {17, 1}, txn0的xids = {17, 0}。
於是對於事務中的每一個操作(xbegin/xcommit/enq_insert/xprepare),都有一個xids來標識這個操作所在的事務號。 TokuDB中的每一條消息(insert/delete/update消息)都會攜帶這樣一個xids事務號。這個xids事務號,在TokuDB的實現中扮演這非常重要的角色,與之相關的功能也特別復雜。
事務管理器
事務管理器用來管理TokuDB存儲引擎所有事務集合, 它主要維護着這幾個信息:
- 活躍事務列表。活躍事務列表只會記錄root事務,因為根據root事務其實可以找到整棵事務樹的所有child事務。 這個事務列表保存這當前時間點已經開始,但是尚未結束的所有root事務。
- 鏡像讀事務列表(snapshot read transaction)。
- 活躍事務的引用列表(referenced_xids)。這個概念有點不好理解,假設一個活躍事務開始(xbegin)時間點為begin_id, 提交(xcommit)的時間點為end_id。那么referenced_xids就是維護(begin_id, end_id)這樣一個二元組,這個二元組的用處就是可以找到一個事務的整個生命周期的所有活躍事務,用處主要是用來做后文說到的full gc操作。
分形樹LeafEntry
上文分形樹的樹形結構中說到,在做insert/delete/update這樣的操作時,會把從root到leaf的所有消息都apply到LeafNode節點中。 為了后面詳細描述apply的過程,先介紹下LeafNode的存儲結構。
leafNode簡單來說,就是由多個leafEntry組成,每個leafEntry就是一個{k, v1, v2, … }這樣的鍵值對, 其中v1, v2 .. 表示一個key對應的值的多個版本。具體到一個key對應得leafEntry的結構詳細如下圖所示。
由上圖看出,一個leafEntry其實就是一個棧, 這個棧底部[0~5]這一段表示已經提交(commited transaction)的事務的Value值。棧的頂部[6~9]這一段表示當前尚未提交的活躍事務(uncommited transaction)。 棧中存放的單個元素為(txid, type, len, data)這樣一個四元組,表明了這個事務對應的value取值。更通用一點講,[0, cxrs-1]這一段棧表示已經提交的事務,本來已經提交的事務不應存在於棧中,但之所以存在,就是因為有其他事務通過snapshot read的方式引用了這些事務,因此,除非所有引用[0, cxrs-1]這段事務的所有事務都提交,否則[0, cxrs-1]這段棧的事務就不會被回收。[cxrs, cxrs+pxrs-1]這一段棧表示當前活躍的尚未提交的事務列表,當這部分事務提交時,cxrs會往后移動,最終到棧頂。
MVCC實現
1)寫入操作
這里我們認為寫入操作包括三種,分別為insert / delete / commit 三種類型。對於insert和delete這兩種類型的寫入操作,只需要在LeafEntry的棧頂放置一個元素即可。 如下圖所示:
對於commit操作,只需把LeafEntry的棧頂元素放到cxrs這個指針處,然后收縮棧頂指針即可。如下圖所示:
2)讀取操作
對讀取操作而言, 數據庫一般支持多個隔離級別。MySQL的InnoDB支持Read UnCommitted(RU)、Read REPEATABLE(RR)、Read Commited(RC)、SERIALIZABLE(S)。其中RU存在臟讀的情況(臟讀指讀取到未提交的事務), RC/RR/RU存在幻讀的情況(幻讀一般指一個事務在更新時可能會更新到其他事務已經提交的記錄)。
TokuDB同樣支持上述4中隔離級別, 在源碼實現時, ft-index將事務的讀取操作按照事務隔離級別分成3類:
- TXN_SNAPSHOT_NONE : 這類不需要snapshot read, SERIALIZABLE和Read Uncommited兩個隔離級別屬於這一類。
- TXN_SNAPSHOT_ROOT : Read REPEATABLE隔離級別屬於這類。在這種其情況下, 說明事務只需要讀取到root事務對應的xid之前已經提交的記錄即可。
- TXN_SNAPSHOT_CHILD: READ COMMITTED屬於這類。在這種情況下,兒子事務A需要根據自己事務的xid來找到snapshot讀的版本,因為在這個事務A開啟時,可能有其他事務B做了更新,並提交,那么事務A必須讀取B更新之后的結果。
多版本記錄回收
隨着時間的推移,越來越多的老事務被提交,新事務開始執行。 在分形樹中的LeafNode中commited的事務數量會越來越多,假設不想方設法把這些過期的事務記錄清理掉的話,會造成BasementNode節點占用大量空間,也會造成TokuDB的數據文件存放大量無用的數據。 在TokuDB中, 清理這些過期事務的操作稱之為垃圾回收(Garbage Collection)。 其實InnoDB也存在過期事務回收這么一個過程,InnoDB的同一個Key的多個版本的Value存放在undo log 頁上, 當事務過期時, 后台有一個purge線程專門來復雜清理這些過期的事務,從而騰出undo log頁給后面的事務使用, 這樣可以控制undo log無限增長。
TokuDB存儲引擎中沒有類似於InnoDB的purge線程來負責清理過期事務,因為過期事務的清理都是在執行更新操作是順便GC的。 也就是在Insert/Delete/Update這些操作執行時,都會判斷以下當前的LeafEntry是否滿足GC的條件, 若滿足GC條件時,就刪除LeafEntry中過期的事務, 重新整理LeafEntry 的內存空間。按照TokuDB源碼的實現,GC分為兩種類型:
- Simple GC:在每次apply 消息到leafentry 時, 都會攜帶一個gc_info, 這個gc_info 中包含了oldest_referenced_xid這個字段。 那么simple_gc的意思是什么呢? simple_gc就是做一次簡單的GC, 直接把commited的事務列表清理掉(記住要剩下一個commit事務的記錄, 否則下次查找這條commited的記錄怎么找的到? )。這就是simple_gc, 簡單暴力高效。
- Full GC:full gc的觸發條件和gc流程都比較復雜, 根本意圖都是要清理掉過期的已經提交的事務。這里不再展開。
總結
本文大致介紹了TokuDB事務的隔離性實現原理, 包括TokuDB的事務表示、分形樹的LeafEntry的結構、MVCC的實現流程、多版本記錄回收方式這些方面的內容。 TokuDB之所有沒有undo log,就是因為分形樹中的更新消息本身就記錄了事務的記錄版本。另外, TokuDB的過期事務回收也不需要像InnoDB那樣專門開啟一個后台線程異步回收,而是才用在更新操作執行的過程中分攤回收。總之,由於TokuDB基於分形樹之上實現事務,因而各方面的思路都有大的差異,這也是TokuDB團隊的創新吧。
參考資料:
- http://docs.tokutek.com/tokudb/tokudb-index-using-tokudb.html
- http://openinx.github.io/2015/12/13/ft-mvcc/
- http://openinx.github.io/2015/11/25/ft-index-implement/
- https://highdb.com/tokudb-%E7%89%B9%E6%80%A7%E6%A6%82%E8%A7%88/
轉自:https://www.biaodianfu.com/tokudb.html