第15節:MySQL層事務提交流程簡析

gaopengtttt發表於2019-07-17

本節將來解釋一下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,高清原圖包含在文末原圖中):

15-1.png


三、步驟解析第一階段(圖中藍色部分)

注意 :在第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節結束

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/7728585/viewspace-2650786/,如需轉載,請註明出處,否則將追究法律責任。

相關文章