MySQL Group Commit 組提交詳解

來源:互聯網
上載者:User

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 沒有保持一致,則會使備庫資料丟失造成主備資料不一致。

二階段提交

如下是二階段提交流程。

詳細執行流程為:

  1. InnoDB 的事務 Prepare 階段,即 SQL 已經成功執行並產生 redo 和 undo 的記憶體日誌;

  2. binlog 提交,通過 write() 將 binlog 記憶體日誌資料寫入檔案系統快取;

  3. fsync() 將 binlog 檔案系統快取日誌資料永久寫入磁碟;

  4. InnoDB 內部提交,commit 階段在儲存引擎內提交,通過 innodb_flush_log_at_trx_commit 參數控制,使 undo 和 redo 永久寫入磁碟。

開啟 binlog 的 MySQL 在崩潰恢複 (crash recovery) 時:

  • 在 prepare 階段崩潰,恢複時該事務未寫入 binlog 且 InnoDB 未提交,該事務直接復原;

  • 在 binlog 已經 fsync() 永久寫入 binlog,但 InnoDB 未來得及 commit 時崩潰;恢複時,將會從 binlog 中擷取提交的資訊,重做該事務並提交,使 InnoDB 和 binlog 始終保持一致。

以上提到單個事務的二階段提交過程,能夠保證 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 。

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.