目錄
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組提交的原理和原始碼實現。感謝上述參考文獻在本文形成的過程中提供的幫助。本文所介紹的內容如下:
- mysql 兩階段提交實現的歷史以及存在的問題
- mysql binlog 組提交實現的原理
1. innodb和binlog的兩階段提交
眾所周知,事務在innodb上提交的時候需要日誌先行WAL(Write-Ahead-Log)。在binlog開啟的情況下,為了保證binlog和儲存引擎的一致性,會在事物提交的時候自動開啟兩階段提交。對於單個事務,mysql實現的兩階段提交流程如圖所示(參考文獻 1 和 文獻2 ):
- 當事務進入PrePare階段的時候,會在儲存引擎層進行提交。生成undo log 和redo log 記憶體日誌。
- 之後生成binlog並呼叫sync落盤。
- 在儲存引擎層提交,通過 innodb_flush_log_at_trx_commit 引數的設定,使 undo 和 redo 永久寫入磁碟。
在mysql啟動恢復的階段,會執行如下的操作:
- 如果事務在prepare 階段mysql異常退出,且binlog和innodb都沒有提交。則在恢復階段直接忽略這個事務不進行提交。
- 如果事務在innodb commit的階段異常,但是binlog已經寫入了磁碟。則在恢復的時候,mysql會從binlog中提取資訊,並把這個事務重做。
以上是mysql在開啟binlog的情況下使用兩階段提交保證binlog和innodb層面都提交的流程。不過在併發的情況下,會存在一定的問題。如圖所示,有3個事務T1,T2,T3 進入Prepare階段:
下面來說明下圖中T1,T2,T3提交的過程中都發生了什麼:
- T1 ,T2,T3依次寫入binlog檔案,並呼叫fsync一次性寫入磁碟。
- T2,T3 先行進入提交階段執行commit。
- 在T1提交之前,做了一次熱備份(例如使用mysqlbackup,xtrabackup等工具)。此時因為T1沒有提交,備份工具記錄的當前binlog位置是指向的T3提交的時刻。
- T1提交。
如果此時DBA使用上面第三點的備份資料,在其他機器上恢復備份並搭建主從複製,則T1事務會完美的被錯過造成主從資料不一致。原因是因為備份開始同步binlog的位置是指向了T3提交的時刻(不會拉取T3提交時刻以前的binlog,因此T1提交的binlog不會被讀取),而且因為T1在備份時刻沒有提交,則在恢復備份的時候會被mysql回滾。
對於這個問題,在mysql5.6之前使用 prepare_commit_mutex 保證順序。並且只有當上一個事務 commit 後釋放鎖,下個事務才可以進行 prepara 操作,並且在每個事務過程中 binlog 沒有 fsync() 的呼叫。接下來介紹下,在使用prepare_commit_mutex 保證事務順序提交的時候,為什麼能夠解決這個問題。
同樣如上圖所示,展示了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個部分:
- FLUSH:在這個階段leader事務把thd的快取寫到binlog檔案的快取中。
- SYNC:在這個階段leader事務呼叫fsync把快取一次性落盤。
- COMMIT :在這個階段,根據引數binlog_order_commits的設定,讓事務依次提交或者各種提交(binlog中提交的順序可能會和innodb中提交的順序不同)
組提交的流程如圖所示:
從上圖中可以看出,每個階段都會產生一個leader程式。當一個事務程式進入佇列的時候,會有如下的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。
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並退出函式。
4. 小結
本文主要介紹了關於binlog組提交的邏輯。限於本文的作者水平有限,文中的錯誤在所難免,懇請大家批評指正。