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

bush2582發表於2019-08-12

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

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組提交的邏輯。限於本文的作者水平有限,文中的錯誤在所難免,懇請大家批評指正。

相關文章