一、MySQL的主從複制
1.1 主從複制基本原理
MySQL的主從架構依賴于MySQL Binlog功能,Master節點上産生Binlog并且寫入到檔案中。
Master節點上啟動一個DUMP線程:當Slave節點I/O線程連接配接Master時,Master建立這個線程,DUMP線程負責從Master的binlog檔案讀取記錄,然後發送給Slave。每個連接配接到Master的Slave都有一個DUMP線程。
Slave節點上啟動兩個線程:IO線程和SQL線程,IO線程從MySQL上拉取Binlog日志并寫入到本地的RelayLog日志;SQL線程不斷從RelayLog日志中讀取日志并解析執行,這樣就可以保證所有在主伺服器上執行過的SQL語句都在從伺服器上一模一樣的執行過一遍。
1.2 複制延遲
複制延遲,指的就是一個事務在Master執行完成以後,要多久以後才能在Slave上執行完成。
由于對Binlog檔案以及RelayLog檔案的讀寫均為順序操作,在生産環境中,Slave上的IO線程對Binlog檔案的Dump操作是很少産生延遲的。實際上,從MySQL5.5開始,MySQL官方提供了半同步複制插件,每個事務的Binlog需要保證傳輸到Slave寫入RelayLog後才能送出,這種架構在主從之間提供了資料完整性,保證了主伺服器在發生故障後從伺服器可以擁有完整的資料副本。是以,複制延遲通常發生在SQL線程執行的過程中。
在上面的架構圖上可以看到,最早的主從複制模型中,隻有一個線程負責執行Relaylog,也就是說所有在主伺服器上的操作,在從伺服器上是串行回放的。這就帶來一個問題,如果主伺服器上寫入壓力比較大,那麼從伺服器上的回放速度很有可能會一直跟不上主。既然主從延遲的問題是單線程回放RelayLog太慢,那麼減少主從延遲的方案自然就是提高從伺服器上回放RelayLog的并行度。
二、MySQL5.6的并行複制
2.1 MySQL5.6并行複制簡介
MySQL從5.6版本開始支援所謂的并行複制,但是其并行隻是基于schema的,也就是基于庫的。如果使用者的MySQL資料庫執行個體中存在多個schema且schema下表數量較少,對于從伺服器複制的速度的确可以有比較大的幫助。
在mysql5.6開啟并行複制功能,SQL線程就變成了coordinator線程,那麼coordinator線程主要負責兩部分内容:
1.若判斷可以并行執行,那麼選擇worker線程執行事務的二進制日志
2.若判斷不可以并行執行,如該操作是DDL,亦或者是事務跨schema操作,則等待所有的worker線程執行完成之後在執行目前的日志
是以,coordinator線程并不是僅将日志發送給worker線程,也可以回放日志,但是所有可以并行的操作傳遞由worker線程完成。
2.2 MySQL5.6并行複制存在的問題
基于schema級别的并行複制存在一個問題,schema級别的并行複制效果并不高,如果使用者執行個體有很少的庫和較多的表,那麼并行回放效果會很差,甚至性能會比原來的單線程更差,而單庫多表是比多庫多表更為常見的一種情形。
三、MySQL5.7的并行複制
3.1 MySQL5.7并行複制簡介
MySQL5.6基于庫的并行複制出來後,基本無人問津,在沉寂了一段時間之後,MySQL 5.7出來了,它的并行複制以一種全新的姿态出現在了DBA面前。MySQL5.7中slave伺服器的回放與master是一緻的,即master伺服器上是怎麼并行執行的,那麼slave上就怎樣進行并行回放。不再有庫的并行複制限制。
下面來看看MySQL 5.7中的并行複制究竟是如何實作的?
組送出:通過對事務進行分組,優化減少了生成二進制日志所需的操作數。當事務同時送出時,它們将在單個操作中寫入到二進制日志中。如果事務能同時送出成功,那麼它們就不會共享任何鎖,這意味着它們沒有沖突,是以可以在Slave上并行執行。是以通過在二進制日志中添加組送出資訊,實作Slave可以并行地安全地運作事務。
Group Commit技術在MySQL5.6中是為了解決事務送出的時候需要fsync導緻并發性不夠而引入的。簡單來說,就是由于事務送出時必須将Binlog寫入到磁盤上而調用fsync,這是一個代價比較高的操作,事務并發送出的情況下,每個事務各自擷取日志鎖并進行fsync會導緻事務實際上以串行的方式寫入Binlog檔案,這樣就大大降低了事務送出的并發程度。

Group Commit技術将事務的送出階段分成了Flush、Sync、Commit三個階段,每個階段維護一個隊列,并且由該隊列中第一個線程負責執行該步驟,這樣實際上就達到了一次可以将一批事務的Binlog fsync到磁盤的目的,這樣的一批同時送出的事務稱為同一個Group的事務。
Group Commit雖然是屬于并行送出的技術,但是卻意外解決了從伺服器上事務并行回放的一個難題——即如何判斷哪些事務可以并行回放。如果一批事務是同時Commit的,那麼這些事務必然不會有互斥的持有鎖,也不會有執行上的互相依賴,是以這些事務必然可以并行的回放。
為了标記事務所屬的組,MySQL5.7版本在産生Binlog日志時會有兩個特殊的值記錄在 Binlog Event 中,last_committed 和 sequence_number,其中 last_committed指的是該事務送出時,上一個事務送出的編号,sequence_number是事務送出的序列号,在一個Binlog檔案内單調遞增。如果兩個事務的last_committed值一緻,這兩個事務就是在一個組内送出的。
為了相容MySQL5.6基于庫的并行複制,5.7引入了新的變量slave-parallel-type,其可以配置的值有:DATABASE(預設值,基于庫的并行複制方式)、LOGICAL_CLOCK(基于組送出的并行複制方式)。
3.2 MySQL5.7并行複制存在的問題
在上文可以看到,MySQL主從複制的SQL線程回放在5.6實作了庫級别的并行,在5.7實作了組送出級别的并行,但是在MySQL5.7中,基于Logical_Clock的并行複制仍然有不盡人意的地方,比如必須是在主伺服器上并行送出的事務才能在從伺服器上并行回放,如果主伺服器上并發壓力不大,那麼就無法享受到并行複制帶來的好處。MySQL5.7中引入了binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count兩個參數,通過讓Binlog在執行fsync前等待一小會來提高Master上組送出的比例。但是無論如何,從伺服器上并行回放的速度還是取決于主伺服器上并行送出的情況。
(關于MySQL5.7并行複制具體可參考之前的文章)
四、MySQL8.0的并行複制
4.1 基于WriteSet的并行複制
在MySQL8.0中引入了一種新的機制來判斷事務能否并行回放,通過檢測事務在運作過程中是否存在寫沖突來決定從伺服器上的回放順序,這使得從伺服器上的并發程度不再依賴于主伺服器。
事實上,該機制在MySQL5.7.20版本中就已經應用了。MySQL在5.7.20版本引入了一個重要的特性:Group Replication(俗稱MGR),通過Paxso協定在多個MySQL節點間分發binlog,使得一個事務必須在叢集内大多數節點((N/2)+1)上送出成功才能送出。為了支援多主寫入,MGR在Binlog分發節點完成後,通過一個Certify階段來決定Binlog中的事務是否寫入RelayLog中。這個過程中,Certify階段采用的就是WriteSet的方式驗證事務之間是否存在沖突,同時,在寫入RelayLog時會将沒有沖突的事務的last_committed值設定為相同的值。在MySQL8.0中,MySQL的并行複制引用了這種機制,通過基于WriteSet的沖突檢測,在主伺服器上産生Binlog的時候,不再基于組送出,而是基于事務本身的更新沖突來确定并行關系。
writeset的思想是:不同僚物修改了不同行的資料,那麼可以視為同一組。MySQL 會對這個送出的事務中的一行記錄做一個 HASH值,這些 HASH 值稱為 writeset。writeset會存入一張 HASH 表。其他事務送出時會檢查這張 HASH 表中是否有相同的記錄,如果不相同,則視為同組,如果有相同,則視為不同組。怎麼判斷是否同組,依然采用了last_committed,sequence_number。
4.2 相關參數
在MySQL 8.0中,引入了參數binlog_transaction_dependency_tracking用于控制如何決定事務的依賴關系。
該值有三個選項:
COMMIT_ORDERE:表示繼續使用5.7中的基于組送出的方式決定事務的依賴關系(預設值);
WRITESET:表示使用寫集合來決定事務的依賴關系;
WRITESET_SESSION:表示使用WriteSet來決定事務的依賴關系,但是同一個Session内的事務不會有相同的last_committed值。
在代碼實作上,MySQL采用一個vector的變量存儲已經送出的事務的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,且不能變更。
4.3 WriteSet依賴檢測條件
WriteSet是基于主鍵的沖突檢測(binlog_transaction_depandency_tracking = COMMIT_ORDERE|WRITESET|WRITESET_SESSION,修改的row的主鍵或非空唯一鍵沒有沖突,即可并行)。在開啟了WRITESET或WRITESET_SESSION後,MySQL按以下的方式辨別并記錄事務的更新:
如果事務目前更新的行有主鍵,則将HASH(DB名、TABLE名、KEY名稱、KEY_VALUE1、KEY_VALUE2……)加入到目前事務的vector write_set中。
如果事務目前更新的行有非空的唯一鍵,同樣将HASH(DB名、TABLE名、KEY名、KEY_VALUE1)……加入到目前事務的write_set中。
如果事務更新的行有外鍵限制且不為空,則将該外鍵資訊與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格式。
五、總結
在MySQL8.0中,開啟了基于WriteSet的事務依賴後,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。