mysql複製那點事(2)-binlog組提交源碼分析和實現

  • 2019 年 10 月 3 日
  • 筆記

mysql複製那點事(2)-binlog組提交源碼分析和實現

[TOC]

0. 參考文獻

序號 文獻
1 MySQL 5.7 MTS源碼分析
2 MySQL 組提交
3 MySQL Redo/Binlog Group Commit , 2pc事務兩階段提交,Crash Recovery淺析
4 MySQL · 物理備份 · Percona XtraBackup 備份原理
5 條件變量(Condition Variable)詳解
6 Linux線程同步之條件變量

本文主要介紹了mysql binlog組提交的原理和源碼實現。感謝上述參考文獻在本文形成的過程中提供的幫助。本文所介紹的內容如下:

  1. mysql 兩階段提交實現的歷史以及存在的問題
  2. mysql binlog 組提交實現的原理

1. innodb和binlog的兩階段提交

眾所周知,事務在innodb上提交的時候需要日誌先行WAL(Write-Ahead-Log)。在binlog開啟的情況下,為了保證binlog和存儲引擎的一致性,會在事物提交的時候自動開啟兩階段提交。對於單個事務,mysql實現的兩階段提交流程如圖所示(參考文獻 1 和 文獻2 ):

兩階段提交

  1. 當事務進入PrePare階段的時候,會在存儲引擎層進行提交。生成undo log 和redo log 內存日誌。
  2. 之後生成binlog並調用sync落盤。
  3. 在存儲引擎層提交,通過 innodb_flush_log_at_trx_commit 參數的設置,使 undo 和 redo 永久寫入磁盤。

在mysql啟動恢復的階段,會執行如下的操作:

  1. 如果事務在prepare 階段mysql異常退出,且binlog和innodb都沒有提交。則在恢復階段直接忽略這個事務不進行提交。
  2. 如果事務在innodb commit的階段異常,但是binlog已經寫入了磁盤。則在恢復的時候,mysql會從binlog中提取信息,並把這個事務重做。

以上是mysql在開啟binlog的情況下使用兩階段提交保證binlog和innodb層面都提交的流程。不過在並發的情況下,會存在一定的問題。如圖所示,有3個事務T1,T2,T3 進入Prepare階段:

2階段提交1

下面來說明下圖中T1,T2,T3提交的過程中都發生了什麼:

  1. T1 ,T2,T3依次寫入binlog文件,並調用fsync一次性寫入磁盤。
  2. T2,T3 先行進入提交階段執行commit。
  3. 在T1提交之前,做了一次熱備份(例如使用mysqlbackup,xtrabackup等工具)。此時因為T1沒有提交,備份工具記錄的當前binlog位置是指向的T3提交的時刻。
  4. T1提交。

如果此時DBA使用上面第三點的備份數據,在其他機器上恢復備份並搭建主從複製,則T1事務會完美的被錯過造成主從數據不一致。原因是因為備份開始同步binlog的位置是指向了T3提交的時刻(不會拉取T3提交時刻以前的binlog,因此T1提交的binlog不會被讀取),而且因為T1在備份時刻沒有提交,則在恢復備份的時候會被mysql回滾。

對於這個問題,在mysql5.6之前使用 prepare_commit_mutex 保證順序。並且只有當上一個事務 commit 後釋放鎖,下個事務才可以進行 prepara 操作,並且在每個事務過程中 binlog 沒有 fsync() 的調用。接下來介紹下,在使用prepare_commit_mutex 保證事務順序提交的時候,為什麼能夠解決這個問題。

2階段提交2

同樣如上圖所示,展示了3個事務T1,T2,T3順序提交的時候的過程。如果DBA在T3寫入binlog之後commit之前建立了一次備份,則如上所述T3 因為沒有提交,在恢復備份的時候會被回滾。之後DBA在搭建同步的時候,根據備份時候備份工具(例如使用mysqlbackup,xtrabackup等工具)記錄的參數從T2commit的時刻開始拉取binlog,則此時可以拉取到T3提交的事務並重放,因此保證了主從的一致性。在這裡,可以看出如果使用了prepare_commit_mutex保證順序提交,則會極大的影響mysql的並發性能。因此在mysql5.6開始提出了binlog組提交的改進。

2. 組提交原理

上文提到mysql5.6 之後對於binlog的提交做了改進。首先去掉了prepare_commit_mutex鎖,並且把整個commit階段分為3個部分:

  1. FLUSH:在這個階段leader事務把thd的緩存寫到binlog文件的緩存中。
  2. SYNC:在這個階段leader事務調用fsync把緩存一次性落盤。
  3. COMMIT :在這個階段,根據參數binlog_order_commits的設定,讓事務依次提交或者各種提交(binlog中提交的順序可能會和innodb中提交的順序不同)

組提交的流程如圖所示:

組提交1

從上圖中可以看出,每個階段都會產生一個leader進程。當一個事務進程進入隊列的時候,會有如下的2種情況:

  1. 隊列為空。
  2. 隊列中已有其他的事務。

在第一種情況下,當前事務稱為leader進程,後續進來事務成為follower 並使用條件變量進入休眠。後續的工作會由leader進程代替follower進程完成。在第二種情況下,當前事務會成為followr進而休眠等到leader 完成剩餘的工作。

3. 組提交實現

前文介紹了組提交的原理,本小節將介紹下組提交在mysql源碼層面上的實現過程。本文去掉了代碼中關於錯誤處理、同步和其他輸出代碼,保留了組提交主流程的相關代碼。

3.1 order_commit

如上圖所示,組提交的入口是order_commit 函數:

 9498 int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit)   9499 {    				... ...     9570   if (change_stage(thd, Stage_manager::FLUSH_STAGE, thd, NULL, &LOCK_log))   9571   {   9572     DBUG_PRINT("return", ("Thread ID: %u, commit_error: %d",   9573                           thd->thread_id(), thd->commit_error));   9574     DBUG_RETURN(finish_commit(thd));   9575   }            ... ...     9594   flush_error= process_flush_stage_queue(&total_bytes, &do_rotate,   9595                                                  &wait_queue);              ... ...     9646   /*   9647     Shall introduce a delay only if it is going to do sync   9648     in this ongoing SYNC stage. The "+1" used below in the   9649     if condition is to count the ongoing sync stage.   9650     When sync_binlog=0 (where we never do sync in BGC group),   9651     it is considered as a special case and delay will be executed   9652     for every group just like how it is done when sync_binlog= 1.   9653   */   9654   if (!flush_error && (sync_counter + 1 >= get_sync_period()))   9655     stage_manager.wait_count_or_timeout(opt_binlog_group_commit_sync_no_delay_count,   9656                                         opt_binlog_group_commit_sync_delay,   9657                                         Stage_manager::SYNC_STAGE);       				... ...     9639   if (change_stage(thd, Stage_manager::SYNC_STAGE, wait_queue, &LOCK_log, &LOCK_sync))   9640   {   9641     DBUG_PRINT("return", ("Thread ID: %u, commit_error: %d",   9642                           thd->thread_id(), thd->commit_error));   9643     DBUG_RETURN(finish_commit(thd));   9644   }              ... ...     9661   if (flush_error == 0 && total_bytes > 0)   9662   {   9663     DEBUG_SYNC(thd, "before_sync_binlog_file");   9664     std::pair<bool, bool> result= sync_binlog_file(false);   9665     sync_error= result.first;   9666   }   9667   				 ... ...     9702 commit_stage:   9703   if (opt_binlog_order_commits &&   9704       (sync_error == 0 || binlog_error_action != ABORT_SERVER))   9705   {   9706     if (change_stage(thd, Stage_manager::COMMIT_STAGE,   9707                      final_queue, leave_mutex_before_commit_stage,   9708                      &LOCK_commit))             ... ...     9736     process_commit_stage_queue(thd, commit_queue);   9737     mysql_mutex_unlock(&LOCK_commit);             ... ...     9759   /* Commit done so signal all waiting threads */   9760   stage_manager.signal_done(final_queue);           ... ...           }  

如源碼所示,在commit階段會調用change_stage函數3次,分別傳入不同的參數FLUSH_STAGE、SYNC_STAGE和COMMIT_STAGE。change_stage主要用於事務加入隊列。在代碼中有一個值得注意的地方是在9655行中,sync落盤緩存之前會等到binlog_group_commit_sync_delay毫秒或收集到binlog_group_commit_sync_no_delay_count個事務之後再sync。

order_commit

3.2 change_stage和enroll_for

change_stage 函數主要的作用是將當期事務加入對應的隊列中,並返回這個事務是否成為leader。函數關鍵代碼如下所示:

 9140 bool   9141 MYSQL_BIN_LOG::change_stage(THD *thd,   9142                             Stage_manager::StageID stage, THD *queue,   9143                             mysql_mutex_t *leave_mutex,   9144                             mysql_mutex_t *enter_mutex)   9145 {            ... ...     9156   if (!stage_manager.enroll_for(stage, queue, leave_mutex))   9157   {   9158     DBUG_ASSERT(!thd_get_cache_mngr(thd)->dbug_any_finalized());   9159     DBUG_RETURN(true);   9160   }          ... ...        }  

在change_stage函數中主要調用了enroll_for函數進行註冊,enroll_for函數關鍵代碼如下:

 2149 bool   2150 Stage_manager::enroll_for(StageID stage, THD *thd, mysql_mutex_t *stage_mutex)   2151 {   2152   // If the queue was empty: we're the leader for this batch   2153   DBUG_PRINT("debug", ("Enqueue 0x%llx to queue for stage %d",   2154                        (ulonglong) thd, stage));   2155   bool leader= m_queue[stage].append(thd);   2156       			... ...     2213   if (!leader)   2214   {   2215     mysql_mutex_lock(&m_lock_done);   2216 #ifndef DBUG_OFF   2217     /*   2218       Leader can be awaiting all-clear to preempt follower's execution.   2219       With setting the status the follower ensures it won't execute anything   2220       including thread-specific code.   2221     */   2222     thd->get_transaction()->m_flags.ready_preempt= 1;   2223     if (leader_await_preempt_status)   2224       mysql_cond_signal(&m_cond_preempt);   2225 #endif   2226     while (thd->get_transaction()->m_flags.pending)   2227       mysql_cond_wait(&m_cond_done, &m_lock_done);   2228     mysql_mutex_unlock(&m_lock_done);   2229   }   2230   return leader;   2231 }  

在代碼中可以看出在接入對應的隊列後,如果發現當前事務不能成為leader 則會在後續調用條件變量進行休眠。當order_commit函數中,leader 完成了所有的任務,則在9760行使用條件變量喚醒其他Follower進程。follower進程會調用DBUG_RETURN(finish_commit(thd))完成commit並退出函數。

img

4. 小結

本文主要介紹了關於binlog組提交的邏輯。限於本文的作者水平有限,文中的錯誤在所難免,懇請大家批評指正。