MySQL 並行複製演進及 MySQL 8.0 中基於 WriteSet 的優化
來自專欄資料庫內核11 人贊了文章
MySQL 8.0 可以說是MySQL發展歷史上里程碑式的一個版本,包括了多個重大更新,目前 Generally Available 版本已經已經發布,正式版本即將發布,在此將介紹8.0版本中引入的一個重要的新特性————基於 WriteSet 的並行複製方案,此方案號稱是徹底解決困擾MySQL運維人員多年的複製延遲問題。
說到並行複製,這裡簡單的回顧一下各個版本的MySQL複製的演進,以幫助理解8.0版本中對並行複製MTS的優化。
MySQL 主從複製模型
一切都要從MySQL的主從複製模型開始說起,下圖是最經典的MySQL主從複製模型架構圖
MySQL的主從架構依賴於 MySQL Binlog 功能, Master節點上產生Binlog並將Binlog寫入到Binlog文件中。Slave節點上啟動兩個線程:一個IO線程,從MySQL上撈取Binlog日誌並寫入到本地的RelayLog日誌;另一個SQL線程,不斷的從RelayLog日誌中讀取日誌,並解析執行。這樣通過在主機和從機上增加幾個文件的順序讀寫操作,就可以保證所有在主機上執行過的SQL語句都在從機上一摸一樣的執行過一遍。而複製延遲,指的就是一個事務在Master執行完成以後,要多久以後才能在Slave上執行完成。
由於對Binlog文件以及RelayLog文件的讀寫均為順序操作,在生產環境中,Slave上的IO線程對Binlog文件的Dump操作是很少產生延遲的。 實際上,從MySQL 5.5 開始,MySQL官方提供了半同步複製插件,每個事務的Binlog需要保證傳輸到Slave寫入 RelayLog 後才能提交,這種架構在主從之間提供了數據完整性,保證了主機在發生故障後從機可以擁有完整的數據副本。因此,複製延遲通常發生在SQL線程執行的過程中。從架構圖上可以看到,最早的主從複製模型中,只有一個線程負責執行 Relaylog,也就是說所有在主機上的操作,在從機上是串列回放的。 這就帶來一個問題,如果主上寫入壓力比較大,那麼從上的回放速度很有可能會一直跟不上主。(除此之外,MySQL的架構決定了Binlog只有在Commit階段才會寫入Binlog文件並Dump給從機,這也導致主從事務必然有執行延遲,這個問題在大事務中體現的特別明顯,不過這個問題就不在本文的討論範圍內了)
既然主從延遲的問題是單線程回放RelayLog太慢,那麼減少主從延遲的方案自然就是提高從機上回放RelayLog 的並行度。
5.6中的並行複製————Schema級別的並行複製
MySQL官方在5.6中引入了一個比較簡單並行複製方案,其架構如下:(圖片來自姜承堯老師的博客)
紅色框部分為並行回放的關鍵,5.6中若開啟並行回放的功能,便會啟動多個WorkThread ,而原來負責回放的SQLThread會轉變成Coordinator角色,負責判斷事務能否並行執行並分發給WorkThread。
如果事務分別屬於不同的Schema,並且不是DDL語句且沒有跨Schema操作,那麼就可以並行回放,否則需要等所有Worker線程執行完成後再執行當前日誌中的內容。
這種並行回放是Schema級別的並行,如果實例上有多個Schema將會因此收益,而如果實例上只有一個Schema,那麼事務將無法並行回放,而且還會因多了分發的操作導致效率略微下降。而在實際應用中,單庫多表才是更常見的情況。
5.7中的並行複製————基於Group Commit 的並行複製
雖然5.6中的並行複製在大多數應用場景中對回放速度的提升不大,但是該架構卻成為了後來MySQL並行複製的基礎——既在Slave上並行回放RelayLog,SQL線程負責判斷能否並行回放,並分配給Work線程回放。
5.6 中引入Group Commit技術,這是為了解決事務提交的時候需要fsync導致並發性不夠而引入的。簡單來說,就是由於事務提交時必須將Binlog寫入到磁碟上而調用fsync,這是一個代價比較高的操作,事務並發提交的情況下,每個事務各自獲取日誌鎖並進行fsync會導致事務實際上以串列的方式寫入Binlog文件,這樣就大大降低了事務提交的並發程度。5.6中採用的Group Commit技術將事務的提交階段分成了 Flush, Sync, Commit 三個階段,每個階段維護一個隊列,並且由該隊列中第一個線程負責執行該步驟,這樣實際上就達到了一次可以將一批事務的Binlog fsync到磁碟的目的,這樣的一批同時提交的事務稱為同一個Group的事務。
Group Commit 雖然是屬於並行提交的技術,但是卻意外的解決了從機上事務並行回放的一個難題————既如何判斷哪些事務可以並行回放。如果一批事務是同時Commit的,那麼這些事務必然不會互斥的持有鎖,也不會有執行上的相互依賴,因此這些事務必然可以並行的回放。
因此MySQL 5.7 中引入了新的並行回放類型, 由參數 slave_parallel_type
決定,默認值DATABASE
將會採用5.6版本中的SCHEMA級別的並行回放,設置為 LOGICAL_LOCK
則會採用基於GroupCommit的並行回放,同一個Group內的事務將會在Slave上並行回放。
為了標記事務所屬的組,MySQL 5.7 版本在產生 Binlog 日誌時會有兩個特殊的值記錄在Binlog Event中, last_committed
和 sequence_number
, 其中 last_committed 指的是該事務提交時,上一個事務提交的編號,sequence_number 是事務提交的序列號,在一個Binlog文件內單調遞增。如果兩個事務的 last_committed
值一致,這兩個事務就是在一個組內提交的。
root@localhost:~# mysqlbinlog mysql-bin.0000006 | grep last_committed#150520 14:23:11 server id 88 end_log_pos 259 CRC32 0x4ead9ad6 GTID last_committed=0 sequence_number=1#150520 14:23:11 server id 88 end_log_pos 1483 CRC32 0xdf94bc85 GTID last_committed=0 sequence_number=2#150520 14:23:11 server id 88 end_log_pos 2708 CRC32 0x0914697b GTID last_committed=0 sequence_number=3#150520 14:23:11 server id 88 end_log_pos 3934 CRC32 0xd9cb4a43 GTID last_committed=0 sequence_number=4#150520 14:23:11 server id 88 end_log_pos 5159 CRC32 0x06a6f531 GTID last_committed=0 sequence_number=5#150520 14:23:11 server id 88 end_log_pos 6386 CRC32 0xd6cae930 GTID last_committed=0 sequence_number=6#150520 14:23:11 server id 88 end_log_pos 7610 CRC32 0xa1ea531c GTID last_committed=6 sequence_number=7#150520 14:23:11 server id 88 end_log_pos 8834 CRC32 0x96864e6b GTID last_committed=6 sequence_number=8#150520 14:23:11 server id 88 end_log_pos 10057 CRC32 0x2de1ae55 GTID last_committed=6 sequence_number=9#150520 14:23:11 server id 88 end_log_pos 11280 CRC32 0x5eb13091 GTID last_committed=6 sequence_number=10#150520 14:23:11 server id 88 end_log_pos 12504 CRC32 0x16721011 GTID last_committed=6 sequence_number=11#150520 14:23:11 server id 88 end_log_pos 13727 CRC32 0xe2210ab6 GTID last_committed=6 sequence_number=12#150520 14:23:11 server id 88 end_log_pos 14952 CRC32 0xf41181d3 GTID last_committed=12 sequence_number=13
如上 binlog 文件中, sequence_number 1-6 的事務 last_committed 都是0 ,因此屬於同一個組,可以在slave上並行回放, 7-12的last_committed 都是6,也屬於同一個組,因此可以並行回放。
5.7 中引入的基於Logical_Lock極大的提高了在主機並發壓力比較大的情況下,從機上的回放速度。基本上做到了主機上如何提交的,在從機上如何回放。
MySQL MGR 中的 WriteSet
在5.7中基於邏輯時鐘 Logical_Clock 的並行複製任然有不盡人意的地方,必須是在主上並行提交的事務才能在從上並行回放,如果主上並發壓力不大,那麼就無法享受到並行複製帶來的好處。5.7 中引入了
binlog_group_commit_sync_delay
和 binlog_group_commit_sync_no_delay_count
兩個參數,通過讓Binlog在執行 fsync 前等待一小會來提高Master上組提交的比率。但是無論如何,從上並行回放的速度還是取決於主上並行提交的情況。MySQL 8.0中引入了一種新的機制來判斷事務能否並行回放,通過檢測事務在運行過程中是否存在寫衝突來決定從機上的回放順序,這使得從機上的並發程度不再依賴於主機。
事實上,該機制在 MySQL 5.7.20 版本中就已經悄悄的應用了。5.7.20版本引入了一個重要的特性: Group Replication,通過Paxso協議在多個MySQL節點間分發binlog,使得一個事務必須在集群內大多數節點(N/2+1)上提交成功才能提交。為了支持多主寫入,MySQL MRG 在Binlog分發節點完成後,通過一個 Certify 階段來決定Binlog中的事務是否寫入RelayLog 中。這個過程中,Certify 階段採用的就是WriteSet的方式驗證事務之間是否存在衝突,同時,在寫入RelayLog 時會將沒有衝突的事務的 last_committed 值設置為相同的值。
比如在5.7.20中,進行如下操作:
> -- create a group replication cluster.> STOP GROUP_REPLICATION; START GROUP_REPLICATION;Query OK, 0 rows affected (9.10 sec)> -- All the next commands on the primary member of the group:> CREATE DATABASE test_ws_mgr ;Query OK, 1 row affected (0.01 sec)> CREATE TABLE test_ws_mgr.test ( id int primary key auto_increment, str varchar(64) not null );Query OK, 1 row affected (0.01 sec)> INSERT INTO test_ws_mgr.test(`str`) VALUES ("a");Query OK, 1 row affected (0.01 sec)> INSERT INTO test_ws_mgr.test(`str`) VALUES ("b");Query OK, 1 row affected (0.01 sec)> INSERT INTO test_ws_mgr.test(`str`) VALUES ("c");Query OK, 1 row affected (0.01 sec)
以上代碼在一個MGR 集群中創建了一個資料庫和一個INNODB表,並插入了三條記錄。這個時候,如何查詢Primary 節點上的Binlog,可能會得到如下結果
# mysqlbinlog mysql-bin.N | grep last_ | sed -e s/server id.*last/[...] last/ -e s/.rbr_only.*/ [...]/#180106 19:31:59 [...] last_committed=0 sequence_number=1 [...] -- CREATE DB#180106 19:32:02 [...] last_committed=1 sequence_number=2 [...] -- CREATE TB#180106 19:32:05 [...] last_committed=2 sequence_number=3 [...] -- INSERT a#180106 19:32:08 [...] last_committed=3 sequence_number=4 [...] -- INSERT b#180106 19:32:11 [...] last_committed=4 sequence_number=5 [...] -- INSERT c
可以看到,由於是在一個Session中,這些操作按著串列的順序有著不同的 last_committed , 正常情況下,這些BinlogEvent應該在從機上同樣以串列的方式回放。我們看一下在MGR集群中的relaylog 情況。
# mysqlbinlog mysql-relay.N | grep -e last_ | sed -e s/server id.*last/[...] last/ -e s/.rbr_only.*/ [...]/#180106 19:31:36 [...] last_committed=0 sequence_number=0 [...]#180106 19:31:36 [...] last_committed=1 sequence_number=2 [...] -- CREATE DB#180106 19:31:36 [...] last_committed=2 sequence_number=3 [...] -- CREATE TB#180106 19:31:36 [...] last_committed=3 sequence_number=4 [...] -- INSERT a#180106 19:31:36 [...] last_committed=3 sequence_number=5 [...] -- INSERT b#180106 19:31:36 [...] last_committed=3 sequence_number=6 [...] -- INSERT c
有趣的是,在 Secondary 節點的 RelayLog 中, 這些事務有著相同的 last_committed 值,也就是說這些事務在MGR集群中,回放的時候可以以並行的方式回放。
MGR中,使用的正是 WriteSet 技術檢測不同事務之間是否存在寫衝突,並重規划了事務的並行回放,這一技術在8.0中被移到了Binlog生成階段,並採用到了主從複製的架構中。
MySQL 8.0 中的並行複製
說了這麼多,終於講到 MySQL 8.0 , 通過以上描述,讀者應該對 MySQL 8.0 中並行複製的優化的原理有了一個大致的輪廓。通過基於 WriteSet 的衝突檢測,在主機上產生 Binlog 的時候,不再基於組提交,而是基於事務本身的更新衝突來確定並行關係。
相關的 MySQL 參數
MySQL 8.0 中引入參數 binlog_transaction_depandency_tracking
用於控制如何決定事務的依賴關係。該值有三個選項:默認的 COMMIT_ORDERE
表示繼續使用5.7中的基於組提交的方式決定事務的依賴關係;WRITESET
表示使用寫集合來決定事務的依賴關係;還有一個選項 WRITESET_SESSION
表示使用 WriteSet 來決定事務的依賴關係,但是同一個Session內的事務不會有相同的 last_committed 值。
在代碼實現上,MySQL採用一個 vector<uint64>
的變數存儲已經提交的事務的HASH值,所有已經提交的事務的所修改的 主鍵和非空的 UniqueKey 的值經過HASH後與該vector中的值對比,以判斷當前提交的事務是否與已經提交的事務更新了同一行,並以此確定依賴關係。該向量的大小由參數 binlog_transaction_dependency_history_size
控制,取值範圍為 1-1000000 ,初始默認值為 25000。 同時有參數 transaction_write_set_extraction
控制檢測事務依賴關係時採用的HASH演算法,有三個取值 OFF| XXHASH64 | MURMUR32
, 如果 binlog_transaction_depandency_tracking 取值為 WRITESET 或 WRITESET_SESSION, 那麼該值取值不能為OFF,且不能變更。
WriteSet 依賴檢測條件
WriteSet 是通過檢測兩個事務是否更新了相同的記錄來判斷事務能否並行回放的,因此需要在運行時保存已經提交的事務信息以記錄歷史事務更新了哪些行。記錄歷史事務的參數為 binlog_transaction_dependency_history_size. 該值越大可以記錄更多的已經提交的事務信息,不過需要注意的是,這個值並非指事務大小,而是指追蹤的事務更新信息的數量。在開啟了 WRITESET 或 WRITESET_SESSION 後,MySQL 按以下的方式標識並記錄事務的更新。
- 如果事務當前更新的行有主鍵(Primary Key),則將 HASH(DB名,TABLE名,KEY名稱,KEY_VALUE1, KEY_VALUE2,.....) 加入到當前事務的 vector write_set 中。
- 如果事務當前更新的行有非空的唯一鍵 (Unique Key Not NULL), 同樣將 HASH(DB名, TABLE名,KEY名, KEY_VALUE1, ....)加入到當前事務的 write_set 中。
- 如果事務更新的行有外鍵約束( FOREIGN KEY )且不為空,則將該 外鍵信息與VALUE 的HASH加到當前事務的 write_set 中
- 如果事務當前更新的表的主鍵是其他某個表的外鍵,並設置當前事務 has_related_foreign_key = true
- 如果事務更新了某一行且沒有任何數據被加入到 write_set 中,則標記當前事務 has_missing_key = true
在執行衝突檢測的時候,先會檢查 has_related_foreign_key 和 has_missing_key , 如果為true, 則退到 COMMIT_ORDER 模式。否則,會依照事務的 write_set 中的HASH值與已提交的事務的 write_set 進行比對,如果沒有衝突,則當前事務與最後一個已提交的事務共享相同的 last_commited, 否則將從全局已提交的 write_set 中刪除那個衝突的事務之前提交的所有write_set,並退化到 COMMIT_ORDER 計算last_committed 。 每次計算完事務的 last_committed 值以後,檢測當前全局已提交事務的 write_set 是否已經超過了 binlog_transaction_dependency_history_size 設置的值,如果超過,則清空已提交事務的全局 write_set。
從檢測條件上看,該特性依賴於 主鍵和唯一索引,如果事務涉及的表中沒有主鍵且沒有唯一非空索引,那麼將無法從此特性中獲得性能的提升。除此之外,還需要將 Binlog 格式設置為 Row 格式。
性能提升
MySQL High Availability 對開啟了WriteSet的複製性能做了測試,這裡直接將測試結果搬運過來,有興趣的可以直接訪問原博客
測試時通過Sysbench 先在主機上執行100W條事務,然後開啟Slave的複製線程,測試環境在Xeon E5-2699-V3 16核主機上執行,以下是測試結果
可以看到,在客戶端線程比較少的時候,WRITESET 具有最好的性能,在只有一個連接的時候 WRITESET_SESSION 和 COMMIT_ORDER 差別不大。
結論
從 MySQL Hight Availability 的測試中可以看到,開啟了基於 WriteSet 的事務依賴後,對Slave上RelayLog回放速度提升顯著。Slave上的 RelayLog 回放速度將不再依賴於 Master 上提交時的並行程度,使得Slave上可以發揮其最大的吞吐能力, 這個特性在Slave上複製停止一段時間後恢複復制時尤其有效。
這個特性使得 Slave 上可能擁有比 Master 上更大的吞吐量,同時可能在保證事務依賴關係的情況下,在 Slave 上產生 Master 上沒有產生過的提交場景,事務的提交順序可能會在 Slave 上發生改變。 雖然在5.7 的並行複製中就可能發生這種情況,不過在8.0中由於 Slave 上更高的並發能力,會使該場景更加常見。 通常情況下這不是什麼大問題,不過如果在 Slave 上做基於 Binlog 的增量備份,可能就需要保證在 Slave 上與Master 上一致的提交順序,這種情況下可以開啟 slave_preserve_commit_order
這是一個 5.7 就引入的參數,可以保證 Slave 上並行回放的線程按 RelayLog 中寫入的順序 Commit。
參考:
http://jfg-mysql.blogspot.jp/2018/01/an-update-on-write-set-parallel-replication-bug-fix-in-mysql-8-0.html
http://jfg-mysql.blogspot.jp/2018/01/write-set-in-mysql-5-7-group-replication.html
https://mysqlhighavailability.com/improving-the-parallel-applier-with-writeset-based-dependency-tracking/
推薦閱讀:
※MySQL MGR成員管理與故障恢復實現
※PL/SQL Developer連接虛擬機Oracle資料庫(圖文詳解,全網最詳細)
※MySQL鎖之源碼探索
※MySQL Replication 常用架構
※MySQL5.7綠色版安裝