MySQL Group Commit 組提交詳解
組提交 (group commit) 是為了最佳化寫日誌時的刷磁碟問題,從最初只支援 InnoDB redo log 組提交,到 5.6 官方版本同時支援 redo log 和 binlog 組提交,大大提高了 MySQL 的交易處理效能。
下面將以 InnoDB 儲存引擎為例,詳細介紹組提交在各個階段的實現原理。
簡介
自 5.1 之後,binlog 和 innodb 採用類似兩階段交易認可的方式,不過不支援 group commit;在 5.6 中,將 binlog 的 commit 階段分為三個階段:flush stage、sync stage 以及 commit stage。
這三個階段中,每個階段都會去維護一個隊列,各個列表的定義如下。
Mutex_queue m_queue[STAGE_COUNTER];
如上,每個階段都在維護一個隊列,第一個進入該隊列的作為 leader 線程,否則作為 follower 線程;leader 線程會收集 follower 的事務,並負責做 sync,follower 線程等待 leader 通知操作完成。
儘管維護了三個隊列,但隊列中所有的 THD 實際上都是通過 next_to_commit 串連起來。binlog 在事務提交階段,也就是在 MYSQL_BIN_LOG::ordered_commit() 函數中,開始 3 個階段的流程。
接下來,看看 MySQL 中事務是如何提交的。
事務提交
接下來,看看 InnoDB 和 binlog 提交的流程。
二階段提交
詳細介紹下二階段提交的過程。
未開啟binlog時
InnoDB 通過 redo 和 undo 日誌來恢複資料庫 (safe crash recovery),當資料恢複時,通過 redo 日誌將所有已經在儲存引擎內部提交的事務應用 redo log 恢複,所有已經 prepared 但是沒有 commit 的事務則會通過 undo log 做復原。
然後用戶端串連時就能看到已經提交的資料存在資料庫內,未提交被復原地資料需要重新執行。
開啟binlog時
為了保證儲存引擎和 MySQL 的 binlog 保持一致,引入二階段提交 (two phase commit, 2pc) 。
因為備庫通過 binlog 重放主庫提交的事務,假設主庫儲存引擎已經提交而 binlog 沒有保持一致,則會使備庫資料丟失造成主備資料不一致。
二階段提交
如下是二階段提交流程。
詳細執行流程為:
InnoDB 的事務 Prepare 階段,即 SQL 已經成功執行並產生 redo 和 undo 的記憶體日誌;
binlog 提交,通過 write() 將 binlog 記憶體日誌資料寫入檔案系統快取;
fsync() 將 binlog 檔案系統快取日誌資料永久寫入磁碟;
InnoDB 內部提交,commit 階段在儲存引擎內提交,通過 innodb_flush_log_at_trx_commit 參數控制,使 undo 和 redo 永久寫入磁碟。
開啟 binlog 的 MySQL 在崩潰恢複 (crash recovery) 時:
以上提到單個事務的二階段提交過程,能夠保證 InnoDB 和 binlog 保持一致,但是在並發的情況下怎麼保證儲存引擎和 binlog 提交的順序一致?當並發提交的時,如果兩者不一致會造成什麼影響?
組提交異常
首先看看,對於上述的問題,當並發提交的時,如果兩者不一致會造成什麼影響?
如上所示,事務按照 T1、T2、T3 順序開始執行,並依相同次序按照寫入 binlog 記錄檔系統緩衝,調用 fsync() 進行一次組提交,將記錄檔永久寫入磁碟。
但是儲存引擎提交的順序為 T2、T3、T1,當 T2、T3 提交事務之後做了一個 On-line 的備份程式建立一個 slave 來做複製;而搭建備庫時,CHANGE MASTER TO
的日誌位移量在 T3 事務之後。
那麼事務 T1 在備機恢複 MySQL 資料庫時,發現 T1 未在儲存引擎內提交,那麼在恢複時,T1 事務就會被復原,此時就會導致主備資料不一致。
結論:需要保證 binlog 的寫入順序和 InnoDB 事務提交順序一致,用於 xtrabackup 備份恢複。
早期解決方案
早期,使用 prepare_commit_mutex 保證順序,只有當上一個事務 commit 後釋放鎖,下個事務才可以進行 prepara 操作,並且在每個事務過程中 binlog 沒有 fsync() 的調用。
由於記憶體資料寫入磁碟的開銷很大,如果頻繁 fsync() 把日誌資料永久寫入磁碟,資料庫的效能將會急劇下降。為此提供 sync_binlog 參數來設定多少個 binlog 日誌產生的時候調用一次 fsync() 把二進位日誌刷入磁碟來提高整體效能,該參數的設定作用為:
sync_binlog=0,二進位日誌 fsync() 的操作基於系統自動執行。
sync_binlog=1,每次事務提交都會調用 fsync(),最大限度保證資料安全,但影響效能。
sync_binlog=N,當資料庫崩潰時,可能會丟失 N-1 個事務。
prepare_commit_mutex 的鎖機制會嚴重影響高並發時的效能,而且 binlog 也無法執行組提交。
改進方案
接下來,看看如何保證 binlog 寫入順序和儲存引擎提交順序是一致的,並且能夠進行 binlog 的組提交?5.6 引入了組提交,並將提交過程分成 Flush stage、Sync stage、Commit stage 三個階段。
這樣,事務提交時分為了如下的階段:
InnoDB, Prepare
SQL已經成功執行並產生了相應的redo和undo記憶體日誌;
Binlog, Flush Stage
所有已經註冊線程都將寫入binlog緩衝;
Binlog, Sync Stage
binlog緩衝將sync到磁碟,sync_binlog=1時該隊列中所有事務的binlog將永久寫入磁碟;
InnoDB, Commit stage
leader根據順序調用儲存引擎提交事務;
每個 Stage 階段都有各自的隊列,從而使每個會話的事務進行排隊,提高並發效能。
如果當一個線程註冊到一個空隊列時,該線程就做為該隊列的 leader,後註冊到該隊列的線程均為 follower,後續的操作,都由 leader 控制隊列中 follower 行為。
leader 同時會帶領當前隊列的所有 follower 到下一個 stage 去執行,當遇到下一個 stage 為非空隊列時,leader 會變成 follower 註冊到此隊列中;注意:follower 線程絕不可能變成 leader 。
配置參數
與 binlog 組提交相關的參數主要包括了如下兩個參數。
binlog_max_flush_queue_time
單位為微妙,用於從 flush 隊列中取事務的逾時時間,這主要是防止並發事務過高,導致某些事務的 RT 上升,詳細的內容可以查看函數 MYSQL_BIN_LOG::process_flush_stage_queue()
。
注意:該參數在 5.7 之後已經取消了。
binlog_order_commits
當設定為 0 時,事務可能以和 binlog 不同的順序提交,其效能會有稍微提升,但並不是特別明顯.
源碼解析
binlog 的組提交是通過 Stage_manager 管理,其中比較核心內容如下。
class Stage_manager {
public:
enum StageID { // binlog的組提交包括了三個階段
FLUSH_STAGE,
SYNC_STAGE,
COMMIT_STAGE,
STAGE_COUNTER
};
private:
Mutex_queue m_queue[STAGE_COUNTER];
};
組提交 (Group Commit) 三階段流程,詳細實現如下。
MYSQL_BIN_LOG::ordered_commit() ← 執行事務順序提交,binlog group commit的主流程
|
|-#########>>>>>>>>> ← 進入Stage_manager::FLUSH_STAGE階段
|-change_stage(..., &LOCK_log)
| |-stage_manager.enroll_for() ← 將當前線程加入到m_queue[FLUSH_STAGE]中
| |
| | ← (follower)返回true
| |-mysql_mutex_lock() ← (leader)對LOCK_log加鎖,並返回false
|
|-finish_commit() ← (follower)對於follower則直接返回
| |-ha_commit_low()
|
|-process_flush_stage_queue() ← (leader)對於follower則直接返回
| |-fetch_queue_for() ← 通過stage_manager擷取隊列中的成員
| | |-fetch_and_empty() ← 擷取元素並清空隊列
| |-ha_flush_log()
| |-flush_thread_caches() ← 對於每個線程做該操作
| |-my_b_tell() ← 判斷是否超過了max_bin_log_size,如果是則切換binlog檔案
|
|-flush_cache_to_file() ← (follower)將I/O Cache中的內容寫到檔案中
|-RUN_HOOK() ← 調用HOOK函數,也就是binlog_storage->after_flush()
|
|-#########>>>>>>>>> ← 進入Stage_manager::SYNC_STAGE階段
|-change_stage()
|-sync_binlog_file()
| |-mysql_file_sync()
| |-my_sync()
| |-fdatasync() ← 調用系統API寫入磁碟,也可以是fsync()
|
|-#########>>>>>>>>> ← 進入Stage_manager::COMMIT_STAGE階段
|-change_stage() ← 該階段會受到binlog_order_commits參數限制
|-process_commit_stage_queue() ← 會遍厲所有線程,然後調用如下儲存引擎介面
| |-ha_commit_low()
| |-ht->commit() ← 調用儲存引擎handlerton->commit()
| | ← ### 注意,實際調用如下的兩個函數
| |-binlog_commit()
| |-innobase_commit()
|-process_after_commit_stage_queue() ← 提交之後的後續處理,例如semisync
| |-RUN_HOOK() ← 調用transaction->after_commit
|
|-stage_manager.signal_done() ← 通知其它線程事務已經提交
|
|-finish_commit()
在 enroll_for() 函數中,剛添加的線程如果是隊列的第一個線程,就將其設定為 leader 線程;否則就是 follower 線程,此時線程會睡眠,直到被 leader 喚醒 (m_cond_done) 。
注意,binlog_max_flush_queue_time 參數已經取消。
commit stage
如上所述,commit 階段會受到參數 binlog_order_commits 的影響,當該參數關閉時,會直接釋放 LOCK_sync ,各個 session 自行進入 InnoDB commit 階段,這樣不會保證 binlog 和事務 commit 的順序一致。
當然,如果你不關注兩者的一致性,那麼可以關閉這個選項來稍微提高點效能;當開啟了上述的參數,才會進入 commit stage 。