本節包含一個筆記如下: https://www.jianshu.com/writer#/notebooks/37013486/notes/50142567 本節将來解釋一下MySQL層詳細的送出流程,但是由于能力有限,這裡不可能包含全部的步驟,隻是包含了一些重要的并且我學習過的步驟。我們首先需要來假設參數設定,因為某些參數的設定會直接影響到送出流程,我們也會逐一解釋這些參數的含義。本節介紹的大部分内容都集中在函數MYSQL_BIN_LOG::prepare和MYSQL_BIN_LOG::ordered_commit之中。
一、參數設定
本部分假定參數設定為:
- binlog_group_commit_sync_delay:0
- binlog_group_commit_sync_no_delay_count:0
- binlog_order_commits:ON
- sync_binlog:1
- binlog_transaction_dependency_tracking:COMMIT_ORDER
關于參數binlog_transaction_dependency_tracking需要重點說明一下。我們知道Innodb的行鎖是在語句運作期間就已經擷取,是以如果多個事務同時進入了送出流程(prepare階段),在Innodb層送出釋放Innodb行鎖資源之前各個事務之間肯定是沒有行沖突的,是以可以在從庫端并行執行。在基于COMMIT_ORDER 的并行複制中,last commit和seq number正是基于這種思想生成的,如果last commit相同則視為可以在從庫并行回放,在19節我們将解釋從庫判定并行回放的規則。而在基于WRITESET的并行複制中,last commit将會在WRITESET的影響下繼續降低,來使從庫獲得更好的并行回放效果,但是它也是COMMIT_ORDER為基礎的,這個下一節将讨論。我們這節隻讨論基于COMMIT_ORDER 的并行複制中last commit和seq number的生成方式。
而sync_binlog參數則有兩個功能:
- sync_binlog=0:binary log不sync刷盤,依賴于OS刷盤機制。同時會在flush階段後通知DUMP線程發送Event。
- sync_binlog=1:binary log每次sync隊列形成後都進行sync刷盤,約等于每次group commit進行刷盤。同時會在sync階段後通知DUMP線程發送Event。注意sync_binlog非1的設定可能導緻從庫比主庫多事務。
- sync_binlog>1:binary log将在指定次sync隊列形成後進行sync刷盤,約等于指定次group commit後刷盤。同時會在flush階段後通知DUMP線程發送Event。
第二功能将在第17節還會進行介紹。
二、總體流程圖
這裡我們先展示整個流程,如下(圖15-1,高清原圖包含在文末原圖中):
三、步驟解析第一階段(圖中藍色部分)
注意:在第1步之前會有一個擷取MDL_key::COMMIT鎖的操作,是以FTWRL将會堵塞‘commit’操作,堵塞狀态為‘Waiting for commit lock’,這個可以參考FTWRL調用的函數make_global_read_lock_block_commit。
(1.) binlog準備。将上一次COMMIT隊列中最大的seq number寫入到本次事務的last_commit中。可參考binlog_prepare函數。
(2.) Innodb準備。更改事務的狀态為準備并且将事務的狀态和XID寫入到Undo中。可參考trx_prepare函數。
(3.) XID_EVENT生成并且寫到binlog cache中。在第10節中我們說過實際上XID來自于query_id,早就生成了,這裡隻是生成Event而已。可參考MYSQL_BIN_LOG::commit函數。
四、步驟解析第二階段(圖中粉色部分)
(4.) 形成FLUSH隊列。這一步正在不斷的有事務加入到這個FLUSH隊列。第一個進入FLUSH隊列的為本階段的leader,非leader線程将會堵塞,直到COMMIT階段後由leader線程的喚醒。
(5.) 擷取LOCK log 鎖。
(6.) 這一步就是将FLUSH階段的隊列取出來準備進行處理。也就是這個時候本FLUSH隊列就不能在更改了。可參考stage_manager.fetch_queue_for函數。
(7.) 這裡事務會進行Innodb層的redo持久化,并且會幫助其他事務進行redo的持久化。可以參考MYSQL_BIN_LOG::process_flush_stage_queue函數。下面是注釋和一小段代碼:
/*
We flush prepared records of transactions to the log of storage
engine (for example, InnoDB redo log) in a group right before
flushing them to binary log.
*/
ha_flush_logs(NULL, true);//做innodb redo持久化
(8.) 生成GTID和seq number,并且連同前面的last commit生成GTID_EVENT,然後直接寫入到binary log中。我們注意到這裡直接寫入到了binary log而沒有寫入到binlog cache,是以GTID_EVENT是事務的第一個Event。參考函數binlog_cache_data::flush中下面一段:
trn_ctx->sequence_number= mysql_bin_log.m_dependency_tracker.step();
//int64 state +1
...
if (!error)
if ((error= mysql_bin_log.write_gtid(thd, this, &writer)))
//生成GTID 寫入binary log檔案
thd->commit_error= THD::CE_FLUSH_ERROR;
if (!error)
error= mysql_bin_log.write_cache(thd, this, &writer);
//将其他Event寫入到binary log檔案
而對于seq number和last commit的取值來講,實際上在MySQL内部維護着一個全局的結構Transaction_dependency_tracker。其中包含三種可能取值方式,如下 :
- Commit_order_trx_dependency_tracker
- Writeset_trx_dependency_tracker
- Writeset_session_trx_dependency_tracker
到底使用哪一種取值方式,由參數binlog_transaction_dependency_tracking來決定的。
這裡我們先研究參數設定為COMMIT_ORDER 的取值方式,對于WRITESET取值的方式下一節專門讨論。
對于設定為COMMIT_ORDER會使用Commit_order_trx_dependency_tracker的取值方式,有如下特點:
特點 |
---|
每次事務送出seq number将會加1。 |
last commit在前面的binlog準備階段就指派給了每個事務。這個前面已經描述了。 |
last commit是前一個COMMIT隊列的最大seq number。這個我們後面能看到。 |
其次seq number和last commit這兩個值類型都為Logical_clock,其中維護了一個叫做offsets偏移量的值,用來記錄每次binary log切換時sequence_number的相對偏移量。是以seq number和last commit在每個binary log總是重新計數,下面是offset的源碼注釋:
/*
Offset is subtracted from the actual "absolute time" value at
logging a replication event. That is the event holds logical
timestamps in the "relative" format. They are meaningful only in
the context of the current binlog.
The member is updated (incremented) per binary log rotation.
*/
int64 offset;
下面是我們計算seq number的方式,可以參考Commit_order_trx_dependency_tracker::get_dependency函數。
sequence_number=
trn_ctx->sequence_number - m_max_committed_transaction.get_offset();
//這裡擷取seq number
我們清楚的看到這裡有一個減去offset的操作,這也是為什麼我們的seq number和last commit在每個binary log總是重新計數的原因。
(9.) 這一步就會将我們的binlog cache裡面的所有Event寫入到我們的binary log中了。對于一個事務來講,我們這裡應該很清楚這裡包含的Event有:
- QUERY_EVENT
- MAP_EVENT
- DML EVENT
- XID_EVENT
注意GTID_EVENT前面已經寫入到的binary logfile。這裡我說的寫入是調用的Linux的write函數,正常情況下它會進入圖中的OS CACHE中。實際上這個時候可能還沒有真正寫入到磁盤媒體中。
重複 7 ~ 9步 把FLUSH隊列中所有的事務做同樣的處理。
注意:如果sync_binlog != 1 這裡将會喚醒DUMP線程進行Event的發送。
(10.) 這一步還會判斷binary log是否需要切換,并且設定一個切換标記。依據就是整個隊列每個事務寫入的Event總量加上現有的binary log大小是否超過了max_binlog_size。可參考MYSQL_BIN_LOG::process_flush_stage_queue函數,如下部分:
if (total_bytes > 0 && my_b_tell(&log_file) >= (my_off_t) max_size)
*rotate_var= true; //标記需要切換
但是注意這裡是先将所有的Event寫入binary log,然後才進行的判斷。是以對于大事務來講其Event肯定都包含在同一個binary log中。
到這裡FLUSH階段就結束了。
五、步驟解析第三階段(圖中紫色部分)
(11.) FLUSH隊列加入到SYNC隊列。第一個進入的FLUSH隊列的leader為本階段的leader。其他FLUSH隊列加入SYNC隊列,且其他FLUSH隊列的leader會被LOCK sync堵塞,直到COMMIT階段後由leader線程的喚醒。
(12.) 釋放LOCK log。
(13.) 擷取LOCK sync。
(14.) 這裡根據參數delay的設定來決定是否等待一段時間。我們從圖中我們可以看出如果delay的時間越久那麼加入SYNC隊列的時間就會越長,也就可能有更多的FLUSH隊列加入進來,那麼這個SYNC隊列的事務就越多。這不僅會提高sync效率,并且增大了GROUP COMMIT組成員的數量(因為last commit還沒有更改,時間拖得越長那麼一組事務中事務數量就越多),進而提高了從庫MTS的并行效率。但是缺點也很明顯可能導緻簡單的DML語句時間拖長,是以不能設定過大,下面是我簡書中的一個案列就是因為delay參數設定不當引起的,如下:
https://www.jianshu.com/p/bfd4a88307f2參數delay一共包含兩個參數如下:
- binlog_group_commit_sync_delay:通過人為的設定delay時長來加大整個GROUP COMMIT組中事務數量,并且減少進行磁盤刷盤sync的次數,但是受到binlog_group_commit_sync_no_delay_count的限制。機關為1/1000000秒,最大值1000000也就是1秒。
- binlog_group_commit_sync_no_delay_count:在delay的時間内如果GROUP COMMIT中的事務數量達到了這個設定就直接跳出等待,而不需要等待binlog_group_commit_sync_delay的時長。機關是事務的數量。
(15.) 這一步就是将SYNC階段的隊列取出來準備進行處理。也就是這個時候SYNC隊列就不能再更改了。這個隊列和FLUSH隊列并不一樣,事務的順序一樣但是數量可能不一樣。
(16.) 根據sync_binlog的設定決定是否刷盤。可以參考函數MYSQL_BIN_LOG::sync_binlog_file,邏輯也很簡單。
到這裡SYNC階段就結束了。
注意:如果sync_binlog = 1 這裡将會喚醒DUMP線程進行Event的發送。
六、步驟解析第四階段(圖中黃色部分)
(17.) SYNC隊列加入到COMMIT隊列。第一個進入的SYNC隊列的leader為本階段的leader。其他SYNC隊列加入COMMIT隊列,且其他SYNC隊列的leader會被LOCK commit堵塞,直到COMMIT階段後由leader線程的喚醒。
(18.) 釋放LOCK sync。
(19.) 擷取LOCK commit。
(20.) 根據參數binlog_order_commits的設定來決定是否按照隊列的順序進行Innodb層的送出,如果binlog_order_commits=1 則按照隊列順序送出則事務的可見順序和送出順序一緻。如果binlog_order_commits=0 則下面21步到23步将不會進行,也就是這裡不會進行Innodb層的送出。
(21.) 這一步就是将COMMIT階段的隊列取出來準備進行處理。也就是這個時候COMMIT隊列就不能在更改了。這個隊列和FLUSH隊列和SYNC隊列并不一樣,事務的順序一樣,數量可能不一樣。
注意:如果rpl_semi_sync_master_wait_point參數設定為‘AFTER_SYNC’,這裡将會進行ACK确認,可以看到實際的Innodb層送出操作還沒有進行,等待期間狀态為‘Waiting for semi-sync ACK from slave’。
(22.) 在Innodb層送出之前必須要更改last_commit了。COMMIT隊列中每個事務都會去更新它,如果大于則更改,小于則不變。可參考Commit_order_trx_dependency_tracker::update_max_committed函數,下面是這一小段代碼:
{
m_max_committed_transaction.set_if_greater(sequence_number);
//如果更大則更改
}
(23.) COMMIT隊列中每個事務按照順序進行Innodb層的送出。可參考innobase_commit函數。
這一步Innodb層會做很多動作,比如:
- Readview的更新
- Undo的狀态的更新
- Innodb 鎖資源的釋放
完成這一步,實際上在Innodb層事務就可以見了。我曾經遇到過一個由于leader線程喚醒本組其他線程出現問題而導緻整個commit操作hang住,但是在資料庫中這些事務的修改已經可見的案例。
循環22~23直到COMMIT隊列處理完。
注意:如果rpl_semi_sync_master_wait_point參數設定為‘AFTER_COMMIT’,這裡将會進行ACK确認,可以看到實際的Innodb層送出操作已經完成了,等待期間狀态為‘Waiting for semi-sync ACK from slave’。
(24.) 釋放LOCK commit。
到這裡COMMIT階段就結束了。
七、步驟解析第五階段(圖中綠色部分)
(25.) 這裡leader線程會喚醒所有的組内成員,各自進行各自的操作了。
(26.) 每個事務成員進行binlog cache的重置,清空cache釋放臨時檔案。
(27.) 如果binlog_order_commits設定為0,COMMIT隊列中的每個事務就各自進行Innodb層送出(不按照binary log中事務的的順序)。
(28.) 根據前面第10步設定的切換标記,決定是否進行binary log切換。
(29.) 如果切換了binary log,則還需要根據expire_logs_days的設定判斷是否進行binlog log的清理。
八、總結
- 整個過程我們看到生成last commit和seq number的過程并沒有其它的開銷,但是下一節介紹的基于WRITESET的并行複制就有一定的開銷了。
- 我們需要明白的是FLUSH/SYNC/COMMIT每一個階段都有一個相應的隊列,每個隊列并不一樣。但是其中的事務順序卻是一樣的,是否能夠在從庫進行并行回放完全取決于準備階段擷取的last_commit,這個我們将在第19節較長的描述。
- 對于FLUSH/SYNC/COMMIT三個隊列事務的數量實際有這樣關系,即COMMIT隊列>=SYNC隊列>=FLUSH隊列。如果壓力不大它們三者可能相同且都隻包含一個事務。
- 從流程中可以看出基于COMMIT_ORDER 的并行複制如果資料庫壓力不大的情況下可能出現每個隊列都隻有一個事務的情況。這種情況就不能在從庫并行回放了,但是下一節我們講的基于WRITESET的并行複制卻可以改變這種情況。
- 這裡我們也更加明顯的看到大事務的Event會在送出時刻一次性的寫入到binary log。如果COMMIT隊列中包含了大事務,那麼必然堵塞本隊列中的其它事務送出,後續的送出操作也不能完成。我認為這也是MySQL不适合大事務的一個重要原因。
第15節結束