MySQL 高性能存儲引擎:TokuDB初探

在安裝MariaDB的時候瞭解到代替InnoDB的TokuDB,看簡介很是的棒,這裏對ToduDB作一個初步的整理,使用後再作更多的分享。html

什麼是TokuDB?

在MySQL最流行的支持全事務的引擎爲INNODB。其特色是數據自己是用B-TREE來組織,數據自己便是龐大的根據主鍵聚簇的B-TREE索引。 因此在這點上,寫入速度就會有些下降,由於要每次寫入要用一次IO來作索引樹的重排。特別是當數據量自己比內存大不少的狀況下,CPU自己被磁盤IO糾纏的作不了其餘事情了。這時咱們要考慮如何減小對磁盤的IO來排解CPU的處境,常見的方法有:node

  • 把INNODB 個PAGE增大(默認16KB),但增大也就帶來了一些缺陷。 好比,對磁盤進行CHECKPOINT的時間將延後。
  • 把日誌文件放到更快速的磁盤上,好比SSD。

TokuDB 是一個支持事務的「新」引擎,有着出色的數據壓縮功能,由美國 TokuTek 公司(如今已經被 Percona 公司收購)研發。擁有出色的數據壓縮功能,若是您的數據寫多讀少,並且數據量比較大,強烈建議您使用TokuDB,以節省空間成本,並大幅度下降存儲使用量和IOPS開銷,不過相應的會增長 CPU 的壓力。mysql

TokuDB 的特性

1.豐富的索引類型以及索引的快速建立git

TokuDB 除了支持現有的索引類型外, 還增長了(第二)集合索引, 以知足多樣性的覆蓋索引的查詢, 在快速建立索引方面提升了查詢的效率github

2.(第二)集合索引算法

也能夠稱做非主鍵的集合索引, 這類索引也包含了表中的全部列, 能夠用於覆蓋索引的查詢須要, 好比如下示例, 在where 條件中直接命中 index_b 索引, 避免了從主鍵中再查找一次.sql

見: 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 不支持在線刪除索引, 刪除索引的時候會對標加全局鎖。安全

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 行爲, 語法中列的屬性必需要指定上, 以下:
  • rename 操做還不支持字段: TIME, ENUM, BLOB, TINYBLOB, MEDIUMBLOB, LONGBLOB.
  • 不支持更新臨時表;

5.數據壓縮

TokuDB中全部的壓縮操做都在後臺執行, 高級別的壓縮會下降系統的性能, 有些場景下會須要高級別的壓縮. 按照官方的建議: 6核數如下的機器建議標準壓縮, 反之可使用高級別的壓縮。

每一個表在 create table 或 alter table 的時候經過 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定位到偏移量讀取數據頁到內存
  • 暫時不考慮崩潰日誌和事務處理。

詳細流程以下:

  1. 加載Root節點;
  2. 判斷Root節點是否須要分裂(或合併),若是知足分裂(或者合併)條件,則分裂(或者合併)Root節點。 具體分裂Root節點的流程,感興趣的同窗能夠開開腦洞。
  3. 當Root節點height>0, 也就是Root是非葉子節點時, 經過二分搜索找到Key所在的鍵值區間Range,將(Key, Value)包裝成一條消息(Insert, Key, Value) , 放入到鍵值區間Range對應的Child指針的Message Buffer中。
  4. 當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)

  1. 加載Root節點,經過二分搜索肯定Key落在Root節點的鍵值區間Range, 找到對應的Range的Child指針。
  2. 加載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)。 例如用戶執行這樣一個事務:

對應到TokuDB存儲引擎的redo-log中的記錄爲:

對應的事務樹以下圖所示:

對一個較爲複雜一點,帶有savepoint的事務例子:

對應的redo-log的記錄爲:

這個事務組成的一棵事務樹以下:

在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團隊的創新吧。

參考資料:

轉自:https://www.biaodianfu.com/tokudb.html

相關文章
相關標籤/搜索