第19節 從庫MTS多執行緒並行回放(一)

gaopengtttt發表於2020-01-09

從庫MTS多執行緒並行回放(一)


歡迎關注我的《深入理解MySQL主從原理 32講 》,如下:

image.png

如果圖片不能顯示可檢視下面連結:
https://www.jianshu.com/p/d636215d767f

本節包含一個筆記如下:
https://www.jianshu.com/p/8706d7422d89


之所以將MTS的相關知識放到這裡講解,是因為後面的幾節會用到這部分知識,如果不先講解後面幾節將沒有辦法進行描述。

一、綜述

與單SQL執行緒的回放不同,MTS包含多個工作執行緒,原有的SQL執行緒蛻變為協調執行緒。SQL協調執行緒同時還承擔了檢查點的工作。我們知道並行回放的方式有兩種,包含LOGICAL_CLOCK和DATABASE,體現在判定哪些事物能夠並行回放的規則不同。實際上原始碼對應兩個不同的類:

  • Mts_submode_logical_clock
  • Mts_submode_database

這裡只准備討論基於LOGICAL_CLOCK的併發方式,而不會討論老的基於DATABASE的方式,下面是我設定的引數:

  • slave_parallel_type:LOGICAL_CLOCK
  • slave_parallel_workers :4

注意slave_parallel_workers設定的是工作執行緒的個數,且不包協調執行緒,因此如果不想使用MTS應該將這個引數設定為0,然後‘stop slave;start slave’才能生效。因為工作執行緒在啟動的時候已經初始化完畢了。

因為我們知道在5.7中即便不開啟GTID也包含的匿名的GTID Event,它攜帶了last commit和seq number,因此即便關閉GTID也可以使用MTS,但是不建議後面第26節可以找到原因。

在前面我們討論了MySQL層事務提交的流程和基於WRITESET的並行複製方式,我們總共提到了三種生成last commit和seq number的方式:

  • ORDER_COMMIT
  • WRITESET
  • WRITESET_SESSION

它們控制的是生成last commit和seq number的規則。而從庫只要將引數slave_parallel_type設定為LOGICAL_CLOCK,其能否並行的依據就是last commit和seq number。

我們下面的描述還是以一個正常的‘Delete’語句刪除一行資料的Event來描述,那麼這個事物Event的順序如下:

Event型別
GTID_LOG_EVENT
QUERY_EVENT
MAP_EVENT
DELETE_EVENT
XID_EVENT

同時在此之前我們先來明確一下MySQL中持久化MTS資訊的三個場所,因為和傳統的單SQL執行緒的主從不同,MTS需要儲存更多的資訊。注意我們只討論master_info_repository和relay_log_info_repository為TABLE的情況,如下:

  • slave_master_info表:由IO執行緒進行更新,超過sync_master_info設定更新,單位Event個數。
  • relay_log_info_repository表:由SQL協調執行緒執行檢查點的時候進行更新。
  • slave_worker_info表:由工作執行緒每次提交事務的時候更新。

更加詳細的解釋參考第25節,同時會解釋為什麼只考慮master_info_repository和relay_log_info_repository為TABLE的原因。

二、協調執行緒的分發機制

協調執行緒在Event的分發中主要完成下面兩個工作:

  • 判定事務是否可以並行回放。
  • 判定事務由哪一個工作執行緒進行回放。

和單SQL執行緒執行的流程不同,主要體現在函式apply_event_and_update_pos下面,對於單執行緒而言會完成Event的應用,而對用MTS而言就是隻會完成Event的分發,具體的應用將會由工作執行緒完成。
這裡說一下簡化的流程,具體函式呼叫參考筆記。下面是一張流程圖(圖19-1,高清原圖包含在文末原圖中):

19-1.png

三、步驟解析

下面對每一步進行解析如下:

(1)如果是GTID_LOG_EVENT代表事物開始,將本事物加入到GAQ佇列中(下一節會詳細描述GAQ)。可參考函式Log_event::get_slave_worker。

(2)將GTID_LOG_EVENT加入到curr_group_da佇列中暫存。可參考函式Log_event::get_slave_worker。

(3)獲取GTID_LOG_EVENT中的last commit和seq number值。可參考函式Mts_submode_logical_clock::schedule_next_event。

(4)獲取current_lwm值,這個值代表的是所有在GAQ佇列上還沒有提交完成事務中最早的那個事務的前一個已經提交事務的seq number,可能後面的事務已經提交完成了,聽起來可能比較拗口但很重要,如果都提交完成了那麼就是取最新提交的事務的seq number,下面的圖表達的就是這個意思,這個圖是原始碼中的。這個值的獲取可參考函式Mts_submode_logical_clock::get_lwm_timestamp。

       the last time index containg lwm
               +------+
               | LWM  |
               |  |   |
               V  V   V
GAQ:x  xoooooxxxxxXXXXX...X
             ^   ^
             |   | LWM+1(LWM代表的是檢查點指向的位置)
             |
             + new current_lwm(這裡就是current_lwm)
      <---- logical (commit) time ----
here `x' stands for committed, `X' for committed and discarded from
the running range of the queue, `o' for not committed.

我們可以先不看LWM部分,對於檢查點的LWM後面在討論。seq number從右向左遞增,在GAQ中實際上有三種值:

  • X:已經做了檢查點,在GAQ中出隊的事物。
  • x:已經提交完成的事物。
  • o:沒有提交完成的事物。

我們可以看到我們需要獲取的current_lwm並不是最新一次提交事物的seq number的值,而是最早未提交事物的前一個已經提交事物的seq number。這一點很重要,因為理解後就會知道大事務是如何影響MTS的並行回放的,同時中間的5個‘o’實際上就是所謂的‘gap’,關於‘gap’下一節還會詳細描述。

(5)將GTID_LOG_EVENT中的last commit和當前current_lwm進行比較。可以參考函式Mts_submode_logical_clock::schedule_next_event。下面是大概的規則:

  • 如果last commit小於等於current_lwm表示可以進行並行回放,繼續。
  • 如果last commit大於current_lwm則表示不能進行並行回放。這個時候協調執行緒就需要等待了,直到小於等於的條件成立。成立後協調執行緒會被工作執行緒喚醒。等待期間狀態被置為“Waiting for dependent transaction to commit”。

原始碼處也比較簡單如下:

    longlong lwm_estimate= estimate_lwm_timestamp(); 
//這個值 只有在 出現 下面等待的時候 才會設定 min_waited_timestamp ,
//設定了min_waited_timestamp才會更新lwm_estimate
    if (!clock_leq(last_committed, lwm_estimate) && 
//  @return   true  when a "<=" b,false otherwise  last_committed<=lwm_estimate
        rli->gaq->assigned_group_index != rli->gaq->entry) 
    {
      if (wait_for_last_committed_trx(rli, last_committed, lwm_estimate)) 
//等待上一次 組提交的完成 Waiting for dependent transaction to commit

(6)如果是QUERY_EVENT則加入到curr_group_da佇列中暫存。

(7)如果是MAP_EVENT進行工作執行緒的分配。參考函式Mts_submode_logical_clock::get_least_occupied_worker,分配工作執行緒如下:

  • 如果有空閒的工作執行緒則分配完成,繼續。
  • 如果沒有空閒的工作執行緒則等待空閒的工作執行緒。這種情況下狀態會置為“Waiting for slave workers to process their queues”。

下面是分配的標準,其實也很簡單:

  for (Slave_worker **it= rli->workers.begin(); it != rli->workers.end(); ++it)
  {
    Slave_worker *w_i= *it;
    if (w_i->jobs.len == 0)
//任務佇列為0表示本Worker執行緒空閒可以分配
      return w_i;
  }
  return 0;

(8)將GTID_LOG_EVENT和QUERY_EVENT分配給工作執行緒。可參考append_item_to_jobs函式。

前面工作執行緒已經分配了,這裡就可以開始將Event分配給這個工作執行緒了。分配的時候需要檢查工作執行緒的任務佇列是否已滿,如果滿了需要等待,狀態置為“Waiting for Slave Worker queue”。因為分配的單位是Event,對於一個事務而言可能包含很多Event,如果工作執行緒應用的速度趕不上協調執行緒入隊的速度,可能導致任務佇列的積壓,因此任務佇列被佔滿是可能的。任務佇列的大小為16384如下:

mts_slave_worker_queue_len_max= 16384;

下面是入隊的部分程式碼:

  while (worker->running_status == Slave_worker::RUNNING && !thd->killed &&
         (ret= en_queue(&worker->jobs, job_item)) == -1)
//如果已經滿了
  {
    thd->ENTER_COND(&worker->jobs_cond, &worker->jobs_lock,
                    &stage_slave_waiting_worker_queue, &old_stage);
//標記等待狀態
    worker->jobs.overfill= TRUE;
    worker->jobs.waited_overfill++;
    rli->mts_wq_overfill_cnt++; //標記佇列滿的次數
    mysql_cond_wait(&worker->jobs_cond, &worker->jobs_lock);
//等待喚醒
    mysql_mutex_unlock(&worker->jobs_lock);
    thd->EXIT_COND(&old_stage);
    mysql_mutex_lock(&worker->jobs_lock);
  }

(9)MAP_EVENT分配給工作執行緒,同上。
(10)DELETE_EVENT分配給工作執行緒,同上。
(11)XID_EVENT分配給工作執行緒,但是這裡還需要額外的處理,主要處理一些和檢查點相關的資訊,這裡關注一點如下:

ptr_group->checkpoint_log_name= my_strdup(key_memory_log_event, 
rli->get_group_master_log_name(), MYF(MY_WME));
ptr_group->checkpoint_log_pos= rli->get_group_master_log_pos();
ptr_group->checkpoint_relay_log_name=my_strdup(key_memory_log_event, 
rli->get_group_relay_log_name(), MYF(MY_WME));
ptr_group->checkpoint_relay_log_pos= rli->get_group_relay_log_pos();
ptr_group->ts= common_header->when.tv_sec + (time_t) exec_time; 
//Seconds_behind_master related .checkpoint
//的時候會將這個值再次傳遞 mts_checkpoint_routine()      
ptr_group->checkpoint_seqno= rli->checkpoint_seqno;
//獲取seqno 這個值會在chkpt後減去偏移量

如果檢查點處於這個事務上,那麼這些資訊會出現在表 slave_worker_info中,並且會出現在show slave status中。也就是說,show slave status中很多資訊是來自MTS的檢查點。下一節將詳細描述檢查點。

(12)如果上面Event的分配過程大於2分鐘(120秒),可能會出現一個日誌如下:

image.png

這個截圖也是一個朋友問的問題。實際上這個日誌可以算一個警告。實際上對應的原始碼為:

sql_print_information("Multi-threaded slave statistics%s: "
                "seconds elapsed = %lu; "
                "events assigned = %llu; "
                "worker queues filled over overrun level = %lu; "
                "waited due a Worker queue full = %lu; "
                "waited due the total size = %lu; "
                "waited at clock conflicts = %llu "
                "waited (count) when Workers occupied = %lu "
                "waited when Workers occupied = %llu",
                rli->get_for_channel_str(),
                static_cast<unsigned long>
                (my_now - rli->mts_last_online_stat),
//消耗總時間 單位秒
                rli->mts_events_assigned,
//總的event分配的個數
                rli->mts_wq_overrun_cnt,
// worker執行緒分配佇列大於 90%的次數 當前硬編碼  14746
                rli->mts_wq_overfill_cnt,    
//由於work 分配佇列已滿造成的等待次數 當前硬編碼 16384
                rli->wq_size_waits_cnt, 
//大Event的個數 一般不會存在
                rli->mts_total_wait_overlap,
//由於上一組並行有大事物沒有提交導致不能分配worker執行緒的等待時間 單位納秒
                rli->mts_wq_no_underrun_cnt, 
//work執行緒由於沒有空閒的而等待的次數
                rli->mts_total_wait_worker_avail);
//work執行緒由於沒有空閒的而等待的時間   單位納秒

因為經常看到朋友問這裡詳細說明一下它們的含義,從前面的分析中我們一共看到三個等待點:

  • “Waiting for dependent transaction to commit”

由於協調執行緒判定本事務由於last commit大於current_lwm因此不能並行回放,協調執行緒處於等待,大事務會加劇這種情況。

  • “Waiting for slave workers to process their queues”

由於沒有空閒的工作執行緒,協調執行緒會等待。這種情況說明理論上的並行度是理想的,但是可能是引數slave_parallel_workers設定不夠。當然設定工作執行緒的個數應該和伺服器的配置和負載相結合考慮,因為第29節我們會看到執行緒是CPU排程最小的單位。

  • “Waiting for Slave Worker queue”

由於工作執行緒的任務佇列已滿,協調執行緒會等待。這種情況前面說過是由於一個事務包含了過多的Event並且工作執行緒應用Event的速度趕不上協調執行緒分配Event的速度,導致了積壓並且超過了16384個Event。

另外實際上還有一種等待如下:
“Waiting for Slave Workers to free pending events”:由所謂的‘big event’造成的,什麼是‘big event’呢,原始碼中描述為:event size is greater than slave_pending_jobs_size_max but less than slave_max_allowed_packet。我個人認為出現的可能性不大,因此沒做過多考慮。可以在函式append_item_to_jobs中找到答案。

我們下面對應日誌中的輸出進行詳細解釋,如下:

指標 解釋
seconds elapsed 整個分配過程消耗的時間,單位秒,超過120秒會出現這個日誌。
events assigned 本工作執行緒分配的Event數量。
worker queues filled over overrun level 本工作執行緒任務佇列中Event個數大於90%的次數。當前硬編碼大於14746。
waited due a Worker queue full 本工作執行緒任務佇列已滿的次數。當前硬編碼大於16384。和前面第三點對應。
waited due the total size ‘big event’的出現的次數。
waited at clock conflicts 由於不能並行回放,協調執行緒等待的時間,單位納秒。和前面第一點對應。
waited (count) when Workers occupied 由於沒有空閒的工作執行緒而等待的次數。對應前面第二點。
waited when Workers occupied 由於沒有空閒的工作執行緒而等待的時間。對應前面第二點。

我們可以看到這個日誌還是記錄很全的,基本覆蓋了前面我們討論的全部可能性。那麼我們再看看案例中的日誌,waited at clock conflicts=91895169800 大約91秒。120秒鐘大約91秒都因為不能並行回放而造成的等待,很明顯應該考慮是否有大事物的存在。

四、並行回放判定的列子

下面是我主庫使用WRITESET方式生成的一段binary log片段,我們主要觀察lastcommit和seq number,通過分析來熟悉這種過程。

image.png

我們根據剛才說的並行判斷規則,即:

  • 如果last commit小於等於current_lwm表示可以進行並行回放,繼續。
  • 如果last commit大於current_lwm則表示不能進行並行回放,需要等待。
具體解析如下:

(last commit:22 seq number:23)這個事務會在(last commit:21 seq number:22)事務執行完成後執行因為(last commit:22<= seq number:22),後面的事務直到(last_commit:22 seq number:30),實際上都可以並行執行,我們先假設他們都執行完成了。我們繼續觀察隨後的三個事務如下:

  • last_committed:29 sequence_number:31
  • last_committed:30 sequence_number:32
  • last_committed:27 sequence_number:33

我們注意到到這是基於WRITESET的並行複製下明顯的特徵。 last commit可能比上一個事務更小,這就是我們前面說的根據Writeset的歷史MAP資訊計算出來的。因此還是根據上面的規則它們三個是可以並行執行的。因為很明顯:

  • last_committed:29 <= current_lwm:30
  • last_committed:30 <= current_lwm:30
  • last_committed:27 <= current_lwm:30

但是如果(last commit:22 seq number:30)這個事務之前有一個大事務沒有執行完成的話,那麼current_lwm的取值將不會是30。比如(last commit:22 seq number:27)這個事務是大事務那麼current_lwm將會標記為26,上面的三個事務將會被堵塞,並且分配(last commit:29 seq number:31)的時候就已經堵塞了,原因如下:

  • last_committed:29 > current_lwm:26
  • last_committed:30 > current_lwm:26
  • last_committed:27 > current_lwm:26

我們再考慮一下基於WRITESET的並行複製下(last commit:27 seq number:33)這個事務,因為在我們並行規則下last commit越小獲得併發的可能性越高。因此基於WRITESET的並行複製確實提高了從庫回放的並行度,但正如第16節所講主庫會有一定的開銷。


第19節結束

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

相關文章