08.14 MySQL 8.0新特性:徹底解決困擾運維的複製延遲問題,你信嗎?

MySQL 8.0可以說是MySQL發展歷史上里程碑式的一個版本,包括了多個重大更新,目前Generally Available版本已經已經發布,在此將介紹8.0版本中引入的一個重要的新特性——基於WriteSet的並行複製方案,此方案號稱是徹底解決困擾MySQL運維人員多年的複製延遲問題。

說到並行複製,這裡簡單的回顧一下各個版本的MySQL複製的演進,以幫助理解8.0版本中對並行複製MTS的優化。

一、MySQL主從複製模型

一切都要從MySQL的主從複製模型開始說起,下圖是最經典的MySQL主從複製模型架構圖:

MySQL 8.0新特性:徹底解決困擾運維的複製延遲問題,你信嗎?

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.7中的並行複製

1、Schema級別的並行複製

MySQL官方在5.6中引入了一個比較簡單並行複製方案,其架構如下:

MySQL 8.0新特性:徹底解決困擾運維的複製延遲問題,你信嗎?

(圖片來自姜承堯老師的博客)

紅色框部分為並行回放的關鍵,5.6中若開啟並行回放的功能,便會啟動多個WorkThread ,而原來負責回放的SQLThread會轉變成Coordinator角色,負責判斷事務能否並行執行並分發給WorkThread。

如果事務分別屬於不同的Schema,並且不是DDL語句,同時沒有跨Schema操作,那麼就可以並行回放,否則需要等所有Worker線程執行完成後再執行當前日誌中的內容。

這種並行回放是Schema級別的並行,如果實例上有多個Schema將會因此收益,而如果實例上只有一個Schema,那麼事務將無法並行回放,而且還會因多了分發的操作導致效率略微下降。而在實際應用中,單庫多表才是更常見的情況。

2、基於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值一致,這兩個事務就是在一個組內提交的。

  1. root@localhost:~# mysqlbinlog mysql-bin.0000006 | grep last_committed
  2. #150520 14:23:11 server id 88 end_log_pos 259 CRC32 0x4ead9ad6 GTID last_committed=0 sequence_number=1
  3. #150520 14:23:11 server id 88 end_log_pos 1483 CRC32 0xdf94bc85 GTID last_committed=0 sequence_number=2
  4. #150520 14:23:11 server id 88 end_log_pos 2708 CRC32 0x0914697b GTID last_committed=0 sequence_number=3
  5. #150520 14:23:11 server id 88 end_log_pos 3934 CRC32 0xd9cb4a43 GTID last_committed=0 sequence_number=4
  6. #150520 14:23:11 server id 88 end_log_pos 5159 CRC32 0x06a6f531 GTID last_committed=0 sequence_number=5
  7. #150520 14:23:11 server id 88 end_log_pos 6386 CRC32 0xd6cae930 GTID last_committed=0 sequence_number=6
  8. #150520 14:23:11 server id 88 end_log_pos 7610 CRC32 0xa1ea531c GTID last_committed=6 sequence_number=7
  9. #150520 14:23:11 server id 88 end_log_pos 8834 CRC32 0x96864e6b GTID last_committed=6 sequence_number=8
  10. #150520 14:23:11 server id 88 end_log_pos 10057 CRC32 0x2de1ae55 GTID last_committed=6 sequence_number=9
  11. #150520 14:23:11 server id 88 end_log_pos 11280 CRC32 0x5eb13091 GTID last_committed=6 sequence_number=10
  12. #150520 14:23:11 server id 88 end_log_pos 12504 CRC32 0x16721011 GTID last_committed=6 sequence_number=11
  13. #150520 14:23:11 server id 88 end_log_pos 13727 CRC32 0xe2210ab6 GTID last_committed=6 sequence_number=12
  14. #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中,進行如下操作:

  1. > -- create a group replication cluster.
  2. > STOP GROUP_REPLICATION; START GROUP_REPLICATION;
  3. Query OK, 0 rows affected (9.10 sec)
  4. > -- All the next commands on the primary member of the group:
  5. > CREATE DATABASE test_ws_mgr ;
  6. Query OK, 1 row affected (0.01 sec)
  7. > CREATE TABLE test_ws_mgr.test ( id int primary key auto_increment, str varchar(64) not null );
  8. Query OK, 1 row affected (0.01 sec)
  9. > INSERT INTO test_ws_mgr.test(`str`) VALUES ("a");
  10. Query OK, 1 row affected (0.01 sec)
  11. > INSERT INTO test_ws_mgr.test(`str`) VALUES ("b");
  12. Query OK, 1 row affected (0.01 sec)
  13. > INSERT INTO test_ws_mgr.test(`str`) VALUES ("c");
  14. Query OK, 1 row affected (0.01 sec)

以上代碼在一個MGR集群中創建了一個數據庫和一個InnoDB表,並插入了三條記錄。這個時候,查詢Primary節點上的Binlog可能會得到如下結果:

  1. # mysqlbinlog mysql-bin.N | grep last_ | sed -e 's/server id.*last/[...] last/' -e 's/.rbr_only.*/ [...]/'
  2. #180106 19:31:59 [...] last_committed=0 sequence_number=1 [...] -- CREATE DB
  3. #180106 19:32:02 [...] last_committed=1 sequence_number=2 [...] -- CREATE TB
  4. #180106 19:32:05 [...] last_committed=2 sequence_number=3 [...] -- INSERT a
  5. #180106 19:32:08 [...] last_committed=3 sequence_number=4 [...] -- INSERT b
  6. #180106 19:32:11 [...] last_committed=4 sequence_number=5 [...] -- INSERT c

可以看到,由於是在一個Session中,這些操作按著串行的順序有著不同的 last_committed,正常情況下,這些BinlogEvent應該在從機上同樣以串行的方式回放。我們看一下在MGR集群中的RelayLog情況:

  1. # mysqlbinlog mysql-relay.N | grep -e last_ | sed -e 's/server id.*last/[...] last/' -e 's/.rbr_only.*/ [...]/'
  2. #180106 19:31:36 [...] last_committed=0 sequence_number=0 [...]
  3. #180106 19:31:36 [...] last_committed=1 sequence_number=2 [...] -- CREATE DB
  4. #180106 19:31:36 [...] last_committed=2 sequence_number=3 [...] -- CREATE TB
  5. #180106 19:31:36 [...] last_committed=3 sequence_number=4 [...] -- INSERT a
  6. #180106 19:31:36 [...] last_committed=3 sequence_number=5 [...] -- INSERT b
  7. #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的時候,不再基於組提交,而是基於事務本身的更新衝突來確定並行關係。

1、相關的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。/<uint64>

同時參數transaction_write_set_extraction控制檢測事務依賴關係時採用的HASH算法有三個取值OFF|XXHASH64|MURMUR32, 如binlog_transaction_depandency_tracking取值為WRITESET或WRITESET_SESSION,那麼該值取值不能為OFF,且不能變更。

2、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格式。

3、性能提升

MySQL High Availability對開啟了WriteSet的複製性能做了測試,這裡直接將測試結果搬運過來,有興趣的可以直接訪問原博客。

測試時通過Sysbench先在主機上執行100W條事務,然後開啟Slave的複製線程,測試環境在Xeon E5-2699-V3 16核主機上執行,以下是測試結果:

MySQL 8.0新特性:徹底解決困擾運維的複製延遲問題,你信嗎?

MySQL 8.0新特性:徹底解決困擾運維的複製延遲問題,你信嗎?

MySQL 8.0新特性:徹底解決困擾運維的複製延遲問題,你信嗎?

可以看到,在客戶端線程比較少的時候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。


分享到:


相關文章: