type
status
date
slug
summary
tags
category
icon
password
前言
在 MySQL 8.0.27 之前,Replica 預設只有一個 IO_THREAD 和一個 SQL_THREAD:
- IO_THREAD 負責從 Source 接收 binlog 並寫入 Replica 的 relaylog
- SQL_THREAD 負責解析和重放 relaylog 中的 event
當 Source 有併發大量寫入時,Replica 的 IO_THREAD 因為是順序寫入一般不會導致 replication delay,但是只有單線程 SQL_THREAD 回放速度是跟不上有多線程寫入的 Source,因此會造成 replication delay 不斷變大,相應也導致 Replica 的 relaylog 大量堆積占滿 disk 空間。
因此從 MySQL 5.6 開始提供了 Multi-Tread Slave (MTS),透過多線程的 SQL_THREAD 來緩解這種問題,並且在後續的大版本中不斷進行優化。
各個版本的 MTS
基於 database 級別的 MTS (5.6)
在 MySQL 5.6 只有基於 Database 級別的 MTS,只有在不同 Database 的語句才可以並行執行,因此這無法解決單表高寫入所造成的同步延遲。

基於 Group Commit 的 MTS (5.7)
Group Commit 簡述
Group Commit 是 MySQL 5.6 版本引入用來優化 BinLog、RedoLog 在 2PC 時寫入的瓶頸,簡單來說原本每個 Transaction 都需要獨自
fsync
操作來寫入 Disk 持久化,經過 Group Commit 的優化後會將多個 Transaction 組成一個對列一起進行 fsync
操作,大幅減少 fsync
操作解決在雙 1 時造成的性能急速下降的問題。
關於 Group Commit 的具體描述,可參考 MySQL Group Commit 演進。
slave_parallel_type
在 MySQL 5.7 引入了
slave_parallel_type
這個新參數,可使用的值有以下 2 個:- DATABASE:也就是 5.6 版本,不同 DATABASE 的才能並行回放。
- LOGICAL_CLOCK:5.7 版本基於 Group Commit 的並行回放。
LOGICAL_CLOCK - Commit Parent Based 模式
在 Source 中能夠在同一個對列一起進行 Group commit,表示這個對列中的所有 Transaction 都沒有鎖衝突,因此也可在 Replica 內並行回放。
為了讓 Replica 能基於 Group Commit 實現 MTS,在 Binlog 中為每個 Transaction 添加了 LOGICAL CLOCK 也就是以下 2 個值:
- sequence_number:每個 Transaction 的唯一序列號,具體在 Transaction 進入 flush 階段的對列之前分配。
- last_commited:紀錄上次 Group commit 時最大的 sequence_number,也就是說 last_committed 相同表示同屬一個 Group。

備註:sequence_number、last_commited 只在同一個 BinLog 文件不重複,每當換到新的 BinLog 文件時會重新從 0 開始計數。
不過 Commit Parent Based 有一個缺陷,讓我們看一下例子:
每一個水平線代表一個 Transaction 由左到右的時間點,其中 P 表示 prepare 階段取得上一個 Group 更新 last_committed 的時間點,C 表示 commit 前更新 last_committed 的時間點。
其中可以觀察到:
- Trx4 的 P 時間點取得的是 Trx1 commit 產生的 last_committed
- Trx5 和 Trx6 的 P 時間點取得的是 Trx2 commit 產生的 last_committed
依照 Commit Parent 模式下 Trx5、Trx6 可以一起在 Replica 回放,但是 Trx4 不可以和 Trx5、Trx6 一起在 Replica 回放。
然而,實際上依照時間線我們可以看到 Trx4 在 prepare 到 commit 的過程中,Trx5、Trx6 有在這個過程中 prepare,也就是說實際上他們並沒有鎖衝突 (如果衝突 Trx5、Trx6 會卡在 lock wait),所以理論上他們在 Replica 是可以並行回放到。
LOGICAL_CLOCK - Lock Based 模式
為了進一步優化 Commit Parent Based 的缺陷,MySQL 5.7 馬上實現了 MySQL :: WL#7165: MTS: Optimizing MTS scheduling by increasing the parallelization window on master 的優化,也就是基於 Lock Based 模式的 LOGICAL_CLOCK,只要 Transaction 在各自持有的鎖沒有衝突時就可以並行執行。
在此模式下 binlog 中的 sequence_number、last_commited 涵義如下:
- sequence_number:每個 Transaction 的唯一序列號,具體在 Transaction 進入 flush 階段的對列之前分配,
- last_commited:當 Transaction 開始加鎖時,將全局變量 max_committed_transaction 當下的值作為 last_commited。
- 全局變量 max_committed_transaction:已經結束 Lock interval 的最大 sequence_number,每個 Transaction 在 InnoDB commit 階段時,如果自己的 sequence_number > max_committed_transaction 時會將其更新為自己的 sequence_number 。
- 因為無法預先知道哪一個鎖是最後一個,因此 Transaction 內每一個 DML 都會不斷更新該 Transaction 的 last_commited。
在 Source 寫入 sequence_number、last_commited 之後,接下來就是看 Replica 如何依據這 2 個直來實現 Lock Based 的 MTS。
首先複習一下,只有當 Transaction 和 Transaction 在 Lock ~ Commit (也就是釋放鎖) 之間有交集才能在 Replica 並行回放:
讓我們首先為上圖中的 L~C 的期間定義一個新的名詞
Lock interval
:- Lock interval 的起始點(上圖L):在 Binlog Prepare 階段取得最後一把鎖的時間點。
- Lock interval 的結束點(上圖C):在 InnoDB Commit 階段釋放第一把鎖的時間點。
也就是說對於 Replica 在讀取 BinLog 時:
- last_commited 作為 Lock interval 的起始點:因為 Transaction 開始加鎖的邏輯時間是目前最後一個已結束 lock interval 的最後一個 sequence_number,就是全局變量 max_committed_transaction。
- sequence_number 作為 Lock interval 的結束點:因為當該 Transaction 結束 lock interval 時會將自己的 sequence_number 更新到 max_committed_transaction,也就是說對於下個 Transaction 而言的 last_commited。
在 Replica 回放時只有 Transaction 之間如果 last_commited~sequence_number 之間有重疊就可以並行回放。
實現方式如下:
- 定義一個變量
last_lwm_timestamp
:為一個已經完成回放 Transaction 的 sequence_number ,該 Transaction 其 sequence_number 之前的所有 Transaction 都已經 commit。
- 當 coordinator 線程讀取一個 Transaction 的 last_committed:
- 當
last_committed
<last_lwm_timestamp
表示 Lock interval 有交集,因此可以丟給 work 線程並行回放。 - 當
last_committed
=last_lwm_timestamp
雖然 Lock interval 沒有交集,但是該情況表示前一個 Transaction 完成,所以當前 Transaction 才會拿到前一個的 sequence_number 作為自己的 last_commited,而last_lwm_timestamp
是已經 commit 的 Transaction,因此可以丟給 work 線程回放了。 - 當
last_committed
>last_lwm_timestamp
表示 Lock interval 沒有交集,因此不能丟給 work 線程並行回放。
Commit Parent Based VS Lock Based 舉例
假設有以下 binlog:

在 Commit Parent Based 下:
- sequence_number 1~7 的 Transaction 其 last_committed 都是 0,所以可在 replica 並行回放。
- sequence_number 8 的 Transaction 其 last_committed 是 1,所以不能和 sequence_number 1~7一起在 replica 並行回放。 *備註:在 Commit Parent Based 下,正確的 last_committed 應該要是 7,此處僅方便舉例使用 Lock Based 舉例。
- sequence_number 9~14 的 Transaction 其 last_committed 都是 7,不能和 sequence_number 1~8 一起在 replica 並行回放。
在 Lock Based 下:
- sequence_number 1~7 的 Transaction 其 last_committed 都是 0 表示為同一個 Group,所以 1~7 可在 replica 並行回放。
- sequence_number 8 的 last_committed = 1,表示 8 和 1~7 的鎖不衝突,因此 1~8 可在 replica 並行回放。
- sequence_number 9~14 的 Transaction 其 last_committed 都是 7 表示為同一個 Group,同時 8~14 的鎖不衝突,因次 8~14 可在 replica 並行回放
缺陷
基於 Group Commit 的 MTS 不論是 Commit Parent Based 還是 Lock Based 都一樣,都是只有在 Source 上每個 Group 的 Transaction 足夠多,也就是併發度夠高的情況下才能在 Replica 上有較好的並行回放效率。
雖然在 5.7 新增
binlog_group_commit_sync_delay
、binlog_group_commit_sync_no_delay_count
這 2 個設定,可以讓一個 Group 有更多的 Transaction,然而效果仍然十分有限。基於 WriteSet 的 MTS (5.7.22、8.0)
MySQL 5.7 雖然透過 Group Commit 優化了 MTS,但這主要是優化在 Master 上有高並行度的情況下,如果 Master 並行度不高則同一個 Group 的 Event 相對少,因此 Slave 回放速度無法有效加快。
在 8.0 為了解決上述問題,即使在 Source 上是串行 commit 的 Transaction,只要互相不衝突那麼在 Replica 上就能並行回放。
在 8.0 新增了
binlog_transaction_dependency_tracking
這個參數來控制 binlog 寫入相關資訊,讓 Replica 據此進行並行回放,有以下三個值:- COMMIT_ORDER:使用 5.7 Group commit 的方式判斷。
- WRITESET:使用 WriteSet 的方式判斷 Transaction 是否有衝突。
- WRITESET_SESSION:WRITESET 的基礎上保證同一個 session 內的 Transaction 不可並行。
WriteSet 簡述
WriteSet
在 MySQL Group Replication(MGR) 中就已經實現了:
使用的地方是 certify 階段用來判斷 Transaction 是否允許 commit,這個時候就會透過
WriteSet
來判斷是否和其他 member 上的 Transaction 有衝突。因為 MGR 可以在多個 member 上寫入,因此不像單機模式可以透過 Lock 衝突來避免 Transaction 之間的衝突,同時為了提高效能 MGR 採用樂觀的方式不透過其他方式額外加鎖,只有準備 commit 的時候透過
WriteSet
判斷 member 之間的 Transaction 是否衝突。WriteSet 應用到 MTS 簡述
假設在 Source 上 Transaction commit 時間軸如下,同一個時間只有 1~2 個 Transaction:
上途中方塊對應 Transaction 修改的資料範圍,如果沒有重疊表示 Transaction 之間修改的數據不衝突,那麼透過 WriteSet 判斷 Transaction 之間是否衝突後,就可以在 Replica上如下並行:

不過上圖有個小問題是可能發生 T3 比 T2 早執行的狀況,導致 Source 和 Replica 中同一個 session 產生有不同的執行紀錄,如果評估後覺得不可接受有以下 2 個方式可以解決:
- slave_preserve_commit_order = ON
- binlog_transaction_dependency_tracking = WRITESET_SESSION

如上圖調整後可以發現同一個 session 的都不能並行回放。
實現方式
WriteSet 是什麼?
WriteSet 是一個 hash 數組,大小由
binlog_transaction_dependency_history_size
來決定。在 InooDB 修改數據後,會將修改的 row 數據以下內容進行 hash 後寫入
WriteSet
:
WriteSet 產出細節
產生的 Hash 值的方式可以參考 sql/rpl_write_set_handler.cc 中的 add_pke function
mysql-server/rpl_write_set_handler.cc at 8.0 · mysql/mysql-server · GitHub
範例:
偽代碼如下:
基於 WriteSet 的 MTS 怎麼實現?
該模式下 Replica 同樣是基於 Source 產生的 binlog 中的
last_commited
和 sequenct_number
來決定是否可以並行回放,也就是說如果要進一步增加並行回放的效率,就需要盡可能為每個 Transaction 找出更小的 last_commited
。基於 WriteSet 的 MTS 能找出更小的
last_commited
的方式就是維護一個先前 Transaction 所組成的 WriteSet 的歷史紀錄,之後新進來的 Transaction 計算 WriteSet 後和這個歷史紀錄進行衝突比對,以此來嘗試找出更小的 last_commited
。binlog_transaction_dependency_tracking 不同對 last_commit 的處理
基於 WriteSet 的 MTS 實際上是基於 ORDER_COMMIT (Group Commit) 進一步處理而已。
根據 binlog_transaction_dependency_tracking 的設定不同,在 Source code 有如下內容:
可以看到從 COMMIT_ORDER 到 WRITESET 再到 WRITESET_SESSION 其實都是以上一個設定的為基礎進一步透過一個新的 function 進行修改而已,這些 function 修改的是
last_commited
值。WriteSet 歷史紀錄詳解
WriteSet 的歷史紀錄包含了 2 個元素:
- WriteSet 的 Hash 值
- 最後一次修改該行的 Transaction 其
sequence_number
另外
binlog_transaction_dependency_history_size
決定了可以儲存幾組紀錄,內部會依照 WriteSet Hash 值進行排序。如果 WriteSet 的歷史紀錄達到
binlog_transaction_dependency_history_size
設定的值就會將歷史紀錄清空,並且本次的 Transaction 會成為清空後歷史紀錄的第一筆紀錄。另外除了歷史紀錄還有有一個
m_writeset_history_start
的值,用來儲存這個歷史紀錄中的最小 sequence_number
。WriteSet MTS 對 last_commit 的處理流程
這裡透過一個例子解釋,假設如下:
- 當前的 Transaction 基於 ORDER_COMMIT (Group Commit) 的方式產生了結果:
- last_commit = 125
- sequence_number = 130
- 該 Transaction 修改的表只有 PK 沒有 UK。
- 該 Transaction 修改了 4 行資料,分別為 ROW1、ROW7、ROW6、ROW10。
下圖展示了該 Transaction 和 WriteSet 歷史紀錄:

接下來就會透過 WriteSet 方式找到更小的 last_commit:
- 將 last_commit 由 125 調整為 100 (歷史紀錄中最小的 sequence_number
m_writeset_history_start
)。
備註:因為該 Transaction 比歷史紀錄中的 Transaction 晚執行,因此 last_commit 一定都比他們的 sequence_number 大。
- 將 ROW1 的 Hash 值在 WriteSet 歷史紀錄中確認,發現有修改相同紀錄的 Transaction:
- 將歷史紀錄中該行的 sequence_number 由 120 (歷史紀錄值) 調整為 130(該 Transaction)。
- 將該 Transaction 的 last_commit 由 100 調整為 120。
- 將 ROW7 的 Hash 值在 WriteSet 歷史紀錄中確認,發現有修改相同紀錄的 Transaction:
- 將歷史紀錄中該行的 sequence_number 由 114 (歷史紀錄值) 調整為 130(該 Transaction)。
- 當前 Transaction 當前 last_commit 為 120 比歷史紀錄中的 114 大,因為在 120 就衝突了,所以不能改成更小的 114,因此 last_commit 不變依舊是 120。
- 將 ROW6 的 Hash 值在 WriteSet 歷史紀錄中確認,發現有修改相同紀錄的 Transaction:
- 將歷史紀錄中該行的 sequence_number 由 105 (歷史紀錄值) 調整為 130(該 Transaction)。
- 當前 Transaction 當前 last_commit 為 120 比歷史紀錄中的 105 大,因為在 120 就衝突了,所以不能改成更小的 105,因此 last_commit 不變依舊是 120。
- 將 ROW10 的 Hash 值在 WriteSet 歷史紀錄中確認,發現並沒有修改相同紀錄的 Transaction:
- 因為沒有找到相同的 WriteSet,因此需要把該 Transaction ROW10 的 Hast 值和 sequence_number 寫入 WriteSet 歷史紀錄。
- 如果歷史紀錄大小超過
binlog_transaction_dependency_history_size
,則清空當前歷史紀錄,隨後將 Transaction ROW10 的 Hast 值和 sequence_number(130) 寫入 WriteSet 新的歷史紀錄,並將m_writeset_history_start
改為 130。 - 如果歷史紀錄大小沒有超過
binlog_transaction_dependency_history_size
,將 Transaction ROW10 的 Hast 值和 sequence_number(130) 寫入 WriteSet 當前歷史紀錄。
整個過程結束,該 Transaction 的 last_commit 由原本的 125 降低為 120,最後結果如下圖:

該過程在 Function Writeset_trx_dependency_tracker::get_dependency
中:
WRITESET_SESSION 怎麼做?
前面有提到過 WRITESET_SESSION 是基於 WRITESET 的基礎上繼續處理的,WRITESET_SESSION 要做到的是同一個 session 的 Transaction 不能在 Replica 並行回放,要實現非常簡單:
關於 binlog_transaction_dependency_history_size 參數說明
該參數默認值為 25000,代表的是 WriteSet 裡元素的數量。
從前面 WriteSet 實現細節說明中我們可以知道修改一行數據可能會產生多個 Hash,所以這個值不會等於修改的行數,可以理解為如下:
- 5.7 版本:binlog_transaction_dependency_history_size = 修改的行數 * ( 1 + UK 數量 ) * 2
- 8.0 版本:binlog_transaction_dependency_history_size = 修改的行數 * ( 1 + UK 數量 )
備註:不同原因在於 5.7 會生成包含 collation 和不包含 collation,在 8.0 中則沒有。
如果將這個參數加大,那麼 Source 上的 WriteSet 就能放越多的元素,也就是說 Transaction 可以生成更小的 last_commited,這在 Replica 上就能提高並行回放的效率,當然缺點就是在 Source 會消耗更多的資源。
WriteSet 不適用情境
以下情境不適用 WriteSet,MySQL 會自動退回使用 commit_order (基於 group commit) 模式
- 沒有 PK 也沒有 UK
- DDL
- session 的 hash 算法換 history 不同
- Transaction 更新了有 Forign key 關聯的欄位
slave_preserve_commit_order 介紹
當開啟 MTS 且 slave_parallel_type = LOGICAL_CLOCK (不論具體是基於 commit_order 還是 writeset) 的時候,有可能會發生 Source 和 Replica 執行順序不同的情況,雖然這並不會導致資料不一致的狀況,但是可能會發生在 Source 上先看到 T1 才看到 T2 卻在 Replica 上卻是先看到 T2 才看到 T1 執行,也就是說在 Source 和 Replica 各自的 binlog 歷史紀錄順序也會不一致,沒有保證
Causal Consistency
。Causal Consistency
(因果一致性) 意思是如果兩個事件有因果關係,那麼在所有節點都必須能觀測到這種因果關係。如果評估業務需要保證
Causal Consistency
,除了不使用 MTS 使用單線程 replication 也可以透過設置 slave_preserve_commit_order=ON
來避免,這會讓 Replica 上回放的 Transaction 在進入 flush 階段之前會先等待 sequence_number 之前的 Transaction 先進入 flush 階段。GAP
如果
slave_preserve_commit_order = OFF
除了上面提到 Causal Consistency
還有一個問題在官方文檔中稱為 GAP。開啟 MTS 時透過 show slave status 查看
Exec_Source_Log_Pos
指的是 low-watermark
也就是保證這個 postition 之前的 Transaction 都已經 commit,但是該 postition 之後的 Transaction 有可能 commit 也可能沒有 commit,相關參數
- slave_parallel_workers (5.6~8.0.25)、replica_parallel_workers (8.0.26~)

設定要在 replica 並行的 thread 數量。
如果 slave 有多個 channel,則每個 channel 都會有此數量的 thread。
設置此參數後必須重新 START REPLICA 才會生效。
- slave_parallel_type (5.7~8.0.25)、replica_parallel_type (8.0.26~8.0.29)
- DATABASE:Transaction 必須作用於不同 Database 才能並行。
- LOGICAL_CLOCK:基於 Source 寫入 binlog 的 timestamp 來決定 Transaction 的並行,也就是基於 Group Commit。

設定在 replica 上允許哪些 Transaction 並行回放
建議將 binlog_transaction_dependency_tracking 設置為 WRITESET 或 WRITESET_SESSION ,這樣在合適的情況下會走 WriteSet 來提高並行度。
預計 8.0.29 之後棄用此參數,總是以 LOGICAL_CLOCK 的方式運行。
- binlog_group_commit_sync_delay

控制 binlog commit 之後等待 N 微秒後才 fsync 到 Disk,設置越大單個 Group 可以有更多時間等到更多的 Transaction 一起 fsync Disk,減少 fsync 的次數及減少每個 Transaction commit 的單位時間。
此外適度的增加對於以下設置的 MTS 也能增加在 Slave 的並行度:
注意:會增加 server 上 transaction 的延遲,也就是 client 端收到 transaction commit 的時間會變晚,另外相應的會增加資源的競爭,因此需評估最好的設置。
補充:在有 Group Commit 之後,sync_binlog 的單位指的是 Group 而不是 Transaction,例如:sync_binlog = 1000,表示的不是每 1000 個 Transaction 就 sync binlog,而是每 1000 個 Group 才 sync binlog。
- binlog_group_commit_sync_no_delay_count

在 Group commit 中等待的 N 個 Transaction 後就不等待 binlog_group_commit_sync_delay 設置的時間直接開始 sync binlog。
當 binlog_group_commit_sync_delay = 0 ,此參數無效。
- slave_preserve_commit_order (5.7~8.0.25)、replica_preserve_commit_order (8.0.26~)

只有當 slave_parallel_type = LOGICAL_CLOCK 且 log-slave-updates 開啟時才能設置。
當設置為 0 或 OFF 時,在 Replica 上的讀取操作無法滿足
Causal Consistency
,在 Source 和 Replica 上 Transaction 在 binlog 中可能有不同的寫入順序,另外在檢查 Replica 上最近執行的 Transaction 無法保證對應到 Source 上該 Transaction 位置之前的 Transaction 都已經執行完畢。設置為 1 或 ON 確保 Transaction 在執行時按照在 relay log 中的順序,這可以讓 Master 和 Replica 有相同的 Transaction history log,也就是符合
Causal Consistency
。- binlog_transaction_dependency_tracking (5.7.22~)
- COMMIT_ORDER:使用 5.7 Group commit 的方式判斷。
- WRITESET:使用 WriteSet 的方式判斷 Transaction 是否有衝突。
- WRITESET_SESSION:WRITESET 的基礎上保證同一個 session 內的 Transaction 不可並行。

指定 Source 依據什麼方式來生成 Transaction 之間的依賴關係寫入 binlog,協助 Replica 確定那些 Transaction 能夠並行執行。
必須設置 replica_parallel_type 為 LOGICAL_CLOCK。
有以下三種值:
- binlog_transaction_dependency_history_size (8.0~)

WriteSet 會判斷 Transaction 之間是否衝突,因此需要將 commit 的 Transaction 修改的行 hash 後暫時保存在內存。
此參數用來設定儲存的 hash 上限,超過此上限會清除先前的歷史紀錄。
若 Source 性能有餘裕可以考慮提升此參數,進一步提高 Replica 的並行度。
- transaction_write_set_extraction

設定 WriteSet 使用的 Hash 演算法。
MySQL 5.7 預設為 OFF,MySQL 8.0.26 後棄用,一般不用特別調整。
官方測試數據
以下為官方使用SYSBENCH進行壓測的圖表,可以觀察到:
- 在 Source 低並行率的情況,WRITESET 的機制下 Replica 仍舊能夠有良好的並行率。
- 當 Source 並行率越高,COMMIT_ORDER 和 WriteSet 差距會縮小。
親自測試
環境:Mysql 8.0.12,測試前stop slave,待sysbench跑完後在start slave
確認在performance_schema中,MTS相關的統計ENABLED皆有開啟(YES)

(*啟用或禁用transaction event的收集)
(分別為當前的transaction event,每個線程最近的transaction event,global(跨線程)最近的transaction event)
查詢MTS並行度的語法
首次壓測以Threads 1 進行10分鐘壓測

在commit_order下測試(即MySQL 5.7使用)

在WriteSet下測試(MySQL 8.0新方案)


接著試試看Threads 128進行10分鐘壓測
在commit_order下測試(即MySQL 5.7使用)

在WriteSet下測試(MySQL 8.0新方案)


測試結果基本上和官方提供的差不多,主要是解決在Master低並行度的情況下,提高MTS的效率。
LOG
當開啟 MTS 且 log_error_verbosity = 3 (NOTE) 時,會在
懶人包
MySQL 5.7~5.7.21 參數設定
- Source (Master)
- Replica (Slave)
MySQL 5.7.22~8.0.XX 參數設定
- Source (Master)
- Replica (Slave)
MTS 效率確認
調整後可以使用以下語法查看調整後 MTS 並行的效率,理想的情況下同一個 channel 的每個 sql thread 的 count_star 應該差不多:
BUG
- MySQL Bugs: #103636: Slave hangs with slave_preserve_commit_order On
說明:當 replica 設置了 replica_preserve_commit_order = 1 在高負載下長時間使用時,可能會用完 commit order sequence tickets 導致 applier 掛起 (hang) 並且無期限的持續等待 commit order queue。
影響版本:MySQL 8.0.28 之前
修復版本: MySQL 8.0.28