技術分享 | 從庫 MTS 多執行緒並行回放(二)

  • 2020 年 3 月 13 日
  • 筆記

作者:高鵬 文章末尾有他著作的《深入理解 MySQL 主從原理 32 講》,深入透徹理解 MySQL 主從,GTID 相關技術知識。

本節包含一個筆記,鏈接如下:

https://www.jianshu.com/p/e920a6d33005


這一節會先描述 MTS 的工作執行緒執行 Event 的大概流程。然後重點描述一下 MTS 中檢查點的概念。在後面的第 25 節我們可以看到,MTS 的異常恢復很多情況下需要依賴這個檢查點,從檢查點位置開始掃描 relay log 做恢復操作,但是在 GTID AUTO_POSITION MODE 模式且設置了 recovery_relay_log=1 的情況下這種依賴將會弱化。

一、工作執行緒執行 Event

前面我們已經討論了協調執行緒分發 Event 的規則,實際上協調執行緒只是將 Event 分發到了工作執行緒的執行隊列中。那麼工作執行緒執行 Event 就需要從執行隊列中拿出這些 Event,然後進行執行。整個過程可以參考函數 slave_worker_exec_job_group。因為這個流程比較簡單,因此就不需要畫圖了,但是我們需要關注一些點如下:

(1)從執行隊列中讀取 Event。注意這裡如果執行隊列中沒有 Event 那麼就進入空閑等待,也就是工作執行緒處於無事可做的狀態,等待狀態為 『Waiting for an event from Coordinator』。

(2)如果執行到 XID_EVENT 那麼說明事務已經結束了那麼需要完成記憶體資訊更新操作。可參考 Slave_worker::slave_worker_exec_event 和 Xid_apply_log_event::do_apply_event_worker 函數。更新記憶體相關資訊可參考函數 commit_positions 函數。下面是一些更新的資訊,我們可以看到和 slave_worker_info 表中的資訊基本一致,如下:

1、更新當前資訊  strmake(group_relay_log_name, ptr_g->group_relay_log_name,  sizeof(group_relay_log_name) - 1);  group_relay_log_pos= ev->future_event_relay_log_pos;  set_group_master_log_pos(ev->common_header->log_pos);  set_group_master_log_name(c_rli->get_group_master_log_name());    2、將檢查點資訊進行寫入:  strmake(checkpoint_relay_log_name, ptr_g-  >checkpoint_relay_log_name,sizeof(checkpoint_relay_log_name) - 1);  checkpoint_relay_log_pos= ptr_g->checkpoint_relay_log_pos;  strmake(checkpoint_master_log_name, ptr_g-  >checkpoint_log_name,sizeof(checkpoint_master_log_name) - 1);  checkpoint_master_log_pos= ptr_g->checkpoint_log_pos;    3、設置GAQ序號:   checkpoint_seqno= ptr_g->checkpoint_seqno;  更新整個BITMAP,可能已經由檢查點進行GAQ出隊:  for (uint pos= ptr_g->shifted; pos < c_rli->checkpoint_group; pos++)  //重新設置點陣圖 因為checkpoint已經  {  //ptr_g->shifted是GAQ中出隊的事務個數  if (bitmap_is_set(&group_shifted, pos))  //這裡就需要偏移掉出隊的事務,恢復已經不需要了  bitmap_set_bit(&group_executed, pos - ptr_g->shifted);  }  4、設置點陣圖:  bitmap_set_bit(&group_executed, ptr_g->checkpoint_seqno);  //在本次事務相應的位置設置為1

(3)如果執行到 XID_EVENT 那麼說明事務已經結束了那麼需要完成記憶體資訊的持久化,即強制刷記憶體資訊持久化到 slave_worker_info 表中(relay_log_info_repository 設置為TABLE)。可參考函數 commit_positions 函數,如下:

if ((error= w->commit_positions(this, ptr_group,  w->is_transactional())))

(4)如果執行到 XID_EVENT 還需要進行事務的提交操作,也就是進行 Innodb 層事務的提交。

從上面我們可以看到 MTS 中每次事務的提交並不會更新 slave_relay_log_info 表,而是進行 slave_worker_info 表的更新,將最新的資訊寫入到 slave_worker_info 表中。

我們前面也說過 SQL 執行緒已經蛻變為協調執行緒,那麼 slave_relay_log_info 表什麼時候更新呢?下面我們就能看到 slave_relay_log_info 表的更新實際上由協調執行緒在做完檢查點之後更新。

二、MTS 中檢查點中的重要概念

總的說來 MTS 中的檢查點是 MTS 進行異常恢復的起點。實際上就是代表到這個位置之前(包含自身)事務都是已經在從庫執行過了,但之後的事務可能執行完成了也可能沒有執行完成。檢查點由協調執行緒進行。

(1)協調執行緒的 GAQ 隊列

前面我們已經知道 MTS 中為每個工作執行緒維護了一個 Event 的分發隊列。除此之外協調執行緒還維護了一個非常的重要的隊列 GAQ,它是一個環形隊列。下面是源碼中的定義:

/*    master-binlog ordered queue of Slave_job_group descriptors of groups    that are under processing. The queue size is @c checkpoint_group. Group assigned  */  Slave_committed_queue *gaq;

每次協調執行緒分發事務的時候都會將事務記錄到 GAQ 隊列中,因此 GAQ 中事務的順序總是和 relay log 文件中事務的順序一致的。檢查點正是作用在 GAQ 隊列上的,每次檢查點的位置稱為 LWM,還記得上一節我叫大家先忽略的 LWM 嗎?就是這個。源碼中定義也正是如此,它在 GAQ 隊列中進行維護。如下:

/*     The last checkpoint time Low-Water-Mark  */  Slave_job_group lwm;

在 GAQ 隊列中還維護有一個叫做 checkpoint_seqno 的序號,它是最後一次檢查點以來每個分配事務的序號,下面是源碼中的定義:

uint checkpoint_seqno;  // counter of groups executed after the most recent CP

在協調執行緒讀取到 GTIDLOGEVENT 後為其分配序號,記作 checkpoint_seqno,如下:

rli->checkpoint_seqno++;//增加seqno

當協調執行緒進行檢查點的時候 checkpoint_seqno 序號會減去出隊的事務數量,如下:

checkpoint_seqno= checkpoint_seqno - shift; //這裡減去出隊的事務

在 MTS 異常恢復的時候也會用到這個序號,每個工作執行緒會通過這個序號來確認本工作執行緒執行事務的上限,如下:

for (uint i= (w->checkpoint_seqno + 1) - recovery_group_cnt,                   j= 0; i <= w->checkpoint_seqno; i++, j++)              {                if (bitmap_is_set(&w->group_executed, i))  //如果這一位 已經設置                {                  DBUG_PRINT("mts", ("Setting bit %u.", j));                  bitmap_fast_test_and_set(groups, j);  //那麼GTOUPS 這個 bitmap中應該設置,最終GTOUPS會包含全的需要恢復的事務                }              }

關於詳細的異常恢複流程將在第 25 節描述。

(2)工作執行緒的 Bitmap

有了 GAQ 隊列和檢查點就知道異常恢復開始的位置了。但是我們並不知道每一個工作執行緒都完成了哪些事務,哪些又沒有執行完成,因此就不能確認哪些事務需要恢復。在 MTS 中並行回放事務的提交並不是按分發順序的進行的,某些大事務(或者其他原因比鎖堵塞)可能遲遲不能提交,而一些小事務卻會很快提交完成。這些遲遲不能提交的事務就成為了所謂的 'gap',如果使用了 GTID 那麼在查看已經執行 GTID SET 的時候可能出現一些『空洞』,為了防止 'gap' 的發生通常需要設置參數 slave_preserve_commit_order。下一節我們將會看到這種『空洞』以及 slave_preserve_commit_order 的作用。但是如果要設置了 slave_preserve_commit_order 參數就需要開啟從庫記錄 binary log 的功能,因此必須開啟 log_slave_updates 參數。下面是源碼的判斷:

if (opt_slave_preserve_commit_order && rli->opt_slave_parallel_workers > 0 &&        opt_bin_log && opt_log_slave_updates)      commit_order_mngr= new Commit_order_manager(rli->opt_slave_parallel_workers);  //order commit 管理器

這裡先提前說一下 MTS 恢復的會有兩個關鍵階段:

  • 掃描階段 通過掃描檢查點以後的 relay log。通過每個工作執行緒的 Bitmap 區分出哪些事務已經執行完成,哪些事務沒有執行完成,並且匯總形成恢復 Bitmap,同時得到需要恢復的事務總量。
  • 執行階段 通過這個匯總的恢復 Bitmap,將這些沒有執行完成事務讀取 relay log 再次執行。

這個 Bitmap 點陣圖和 GAQ 中的事務一一對應。當執行 XID_EVENT 完成提交後這一位將會被設置為 『1』。

(3)協調執行緒資訊的持久化

這個已經在前面提到過,實際上每次進行檢查點的時候都需要將檢查點的位置固化到 slave_relay_log_info 表中(relay_log_info_repository 設置為 TABLE)。因此 slave_relay_log_info 中存儲的實際上不是實時的資訊而是檢查點的資訊。下面就是 slave_relay_log_info 表的表結構:

mysql> desc slave_relay_log_info;  +-------------------+---------------------+------+-----+---------+-------+  | Field             | Type                | Null | Key | Default | Extra |  +-------------------+---------------------+------+-----+---------+-------+  | Number_of_lines   | int(10) unsigned    | NO   |     | NULL    |       |  | Relay_log_name    | text                | NO   |     | NULL    |       |  | Relay_log_pos     | bigint(20) unsigned | NO   |     | NULL    |       |  | Master_log_name   | text                | NO   |     | NULL    |       |  | Master_log_pos    | bigint(20) unsigned | NO   |     | NULL    |       |  | Sql_delay         | int(11)             | NO   |     | NULL    |       |  | Number_of_workers | int(10) unsigned    | NO   |     | NULL    |       |  | Id                | int(10) unsigned    | NO   |     | NULL    |       |  | Channel_name      | char(64)            | NO   | PRI | NULL    |       |  +-------------------+---------------------+------+-----+---------+-------+

與此同時 show slave status 中的某些資訊也是檢查點的記憶體資訊。下面的資訊將是來自檢查點:

  • Relay_Log_File:最新一次檢查點的 relay log 文件名。
  • Relay_Log_Pos:最新一次檢查點的 relay log 位點。
  • Relay_Master_Log_File:最新一次檢查點的主庫 binary log 文件名。
  • Exec_Master_Log_Pos:最新一次檢查點的主庫 binary log 位點。
  • Seconds_Behind_Master:根據檢查點指向事務的提交時間計算的延遲。

需要注意的是我們的 GTID 模組獨立在這一套理論之外,在第 3 節我們講 GTID 模組的初始化的時候我們就說過 GTID 模組的初始化是在從庫資訊初始化之前就完成了。因此在做 MTS 異常恢復的時候使用 GTID AUTO_POSITION MODE 模式將會變得更加簡單和安全,細節將在第 25 節描述。

(4)工作執行緒資訊的持久化

工作執行緒的資訊就持久化在 slave_worker_info 表中,前面我們描述工作執行緒執行 Event 注意點的時候已經做了相應的描述。執行 XID_EVENT 完成事務提交之後會將資訊寫入到 slave_worker_info 表中(relay_log_info_repository 設置為 TABLE)。其中包括資訊:

  • Relay_log_name:工作執行緒最後一個提交事務的 relay log 文件名。
  • Relay_log_pos:工作執行緒最後一個提交事務的 relay log 位點。
  • Master_log_name:工作執行緒最後一個提交事務的主庫 binary log 文件名。
  • Master_log_pos:工作執行緒最後一個提交事務的主庫 binary log 文件位點。
  • Checkpoint_relay_log_name:工作執行緒最後一個提交事務對應檢查點的 relay log 文件名。
  • Checkpoint_relay_log_pos:工作執行緒最後一個提交事務對應檢查點的 relay log 位點。
  • Checkpoint_master_log_name:工作執行緒最後一個提交事務對應檢查點的主庫 binary log 文件名。
  • Checkpoint_master_log_pos:工作執行緒最後一個提交事務對應檢查點的主庫 binary log 位點。
  • Checkpoint_seqno:工作執行緒最後一個提交事務對應 checkpoint_seqno 序號。
  • Checkpoint_group_size:工作執行緒的 Bitmap 位元組數,約等於 GAQ 隊列大小 /8,因為 1 個位元組為 8 位。
  • Checkpoint_group_bitmap:工作執行緒對應的 Bitmap 點陣圖資訊。

關於 Checkpoint_group_size 的換算參考函數 Slave_worker::write_info。

(5)兩個參數

  • slave_checkpoint_group:GAQ 隊列大小。
  • slave_checkpoint_period:多久執行一次檢查點,默認 300 毫秒。

(6)檢查點執行的時機

  • 超過 slave_checkpoint_period 配置。可參考 next_event 函數如下:
if (rli->is_parallel_exec() && (opt_mts_checkpoint_period != 0 || force))  {  ulonglong period= static_cast<ulonglong>(opt_mts_checkpoint_period * 1000000ULL);  ...  (void) mts_checkpoint_routine(rli, period, force, true/*need_data_lock=true*/);  ...        }
  • 達到 GAQ 隊列已滿,如下:
//如果達到了 GAQ的大小 設置為force 強制checkpoint  bool force= (rli->checkpoint_seqno > (rli->checkpoint_group - 1));
  • 正常stop slave。

(7)一個列子

通常有壓力的情況下的 slave_worker_info 中的所有工作執行緒最大的 Checkpoint_master_log_pos 應該和 slave_relay_log_info 中的 Master_log_pos 相等,因為這是最後一個檢查點的位點資訊,如下:

三、MTS 中的檢查點的流程

這一部分將詳細描述一下檢查點的步驟,關於檢查點可以參考函數 mts_checkpoint_routine。

假設現在有 7 個事務是可以並行執行的,工作執行緒數量為 4 個。當前協調執行緒已經分發了 5 個,前面 4 個事務都已經執行完成,其中第 5 的一個事務是大事務。那麼可能當前的狀態圖如下:

前面 4 個事務每個工作執行緒都分到一個,最後一個大事務這裡假設由工作執行緒 2 進行執行,圖中用紅色部分表示。

(1)判斷是超過了 slave_checkpoint_period 設置的大小,如果超過需要進行檢查點。

if (!force && diff < period)  //是否需要進行檢查點是否超過了slave_checkpoint_period的設置    {      /*        We do not need to execute the checkpoint now because        the time elapsed is not enough.      */      DBUG_RETURN(FALSE);    }

(2)掃描 GAQ 隊列進行出隊操作,直到第一個沒有提交的事務為止。圖中紅色部分就是一個大事務,檢查點只能停留在它之前。

cnt= rli->gaq->move_queue_head(&rli->workers);  //work數組 返回出隊的個數

move_queue_head 部分程式碼如下:

if (ptr_g->worker_id == MTS_WORKER_UNDEF ||          my_atomic_load32(&ptr_g->done) == 0)  //當前GROUP是否已經執行完成 如果沒有執行完成就需要 停止本次檢查點        break; /* 'gap' at i'th */

(3)更新記憶體和 relay_log_info_repository 表的資訊為本次檢查點指向的位置。

先更新記憶體資訊,也就是我們 show slave status 中看到的資訊:

rli->set_group_master_log_pos(rli->gaq->lwm.group_master_log_pos);  rli->set_group_relay_log_pos(rli->gaq->lwm.group_relay_log_pos);  rli->set_group_relay_log_name(rli->gaq->lwm.group_relay_log_name);

然後強制寫入表 slave_relay_log_info 中:

error= rli->flush_info(TRUE);  //將本次檢查點資訊 寫入到relay_log_info_repository表中

(4)更新 last_master_timestamp 資訊為檢查點位置事務的 XID_EVENT 的 timstamp 值。

這個值在第 27 節中會詳細描述,它是計算 Seconds_behind_master 的一個因素:

/*      Update the rli->last_master_timestamp for reporting correct Seconds_behind_master.      If GAQ is empty, set it to zero.      Else, update it with the timestamp of the first job of the Slave_job_queue      which was assigned in the Log_event::get_slave_worker() function.    */  ts= rli->gaq->empty()? 0 : reinterpret_cast<Slave_job_group*>(rli->gaq->head_queue())->ts;  //rli->gaq->head_queue 檢查點位置的GROUP的時間  rli->reset_notified_checkpoint(cnt, ts, need_data_lock, true);  reset_notified_checkpoint函數中有:  last_master_timestamp= new_ts;

因此 MTS 中 Seconds_behind_master 的計算和檢查點息息相關。

(5)最後還會將前面 GAQ 出隊的事務數量進行統計,因為每個工作執行緒需要根據這個值來進行 Bitmap 點陣圖的偏移。並且還會維護我們前面說的 GAQ 的 checkpoint_seqno 值。

這個操作也是在函數 Relay_log_info::reset_notified_checkpoint 中完成的,實際上很簡單部分程式碼如下:

for (Slave_worker **it= workers.begin(); it != workers.end(); ++it)  //循環每個woker  w->bitmap_shifted= w->bitmap_shifted + shift;  //每個worker執行緒都會增加 這個偏移量  checkpoint_seqno= checkpoint_seqno - shift;  //這裡減去 移動的個數

到這裡整個檢查點的基本操作就完成了。我們看到實際上步驟並不多,拿到 Bitmap 偏移量後每個工作執行緒就會在隨後的第一個事務提交的時候進行點陣圖的偏移,checkpoint_seqno 計數也會更新。

我們前面的假設環境中,如果觸發了一次檢查點,並且協調執行緒將後兩個可以並行的事務發給了工作執行緒 1 和 3 進行處理並且處理完成。那麼我們的圖會變成如下:

這張圖中我用不同樣色表示了不同線條,因為它們交叉比較多。GAQ 中的紅色事務就是我們假設的大事務它仍然沒有執行完成,它也是我們所謂的 『gap』。如果這個時候 MySQL 實例異常重啟,那麼這個紅色 『gap』 就是我們啟動後需要找到的事務,方式就是通過 Bitmap 點陣圖進行比對,後面說異常恢復的時候再詳細討論。如果是開啟了 GTID,這種 『gap』 很容易就能觀察到,下一節將進行測試。

同時我們需要注意這個時候工作執行緒 2 並沒有分發新的事務執行,因為工作執行緒 2 沒有執行完大事務, 因此在 slave_woker_info 表中它的資訊仍然顯示為上一次提交事務的資訊。而工作執行緒 4 因為沒有分配到新的事務,因此 slave_woker_info 表中它的資訊也顯示為上一次提交事務的資訊。因此在 slave_woker_info 中工作執行緒 2 和工作執行緒 4 的檢查點資訊、Bitmap 資訊、checkpoint_seqno 都是老的資訊。

總結

好了,到這裡我已經說明了 MTS 中三個關鍵點。

  • 協調執行緒是根據什麼規則進行事務分發的。
  • 工作執行緒如何拿到分發的事務。
  • MTS 中的檢查點是如何進行的。

但是還有一個關鍵點沒有說,就是前面多次提到的異常恢復,第 25 節將重點解釋。

第 20 節結束。

最後推薦高鵬的專欄《深入理解 MySQL 主從原理 32 講》,想要透徹了解學習 MySQL 主從原理的朋友不容錯過。