MySQL · 功能分析 · 5.6 並行複製實現分析

zping發表於2017-02-27

背景

我們知道MySQL的主備同步是通過binlog在備庫重放進行的,IO執行緒把主庫binlog拉過去存入relaylog,然後SQL執行緒重放 relaylog 中的event,然而這種模式有一個問題就是SQL執行緒只有一個,在主庫壓力大的時候,備庫單個SQL執行緒是跑不過主庫的多個使用者執行緒的,這樣備庫延遲是不可避免的。為了解決這種n對1造成的備庫延遲問題,5.6 引入了並行複製機制,即SQL執行緒在執行的時候可以併發跑。

關於其背後的設計思想,可以參考這幾個worklog WL#4648WL#5563WL#5569WL#5754WL#5599,之前的月報也對並行複製原理程式了闡述,讀者朋友可以回顧下

本篇將從程式碼實現角度講述並行複製是如何做的,分析基於MySQL 5.6.26。

準備知識

binlog

binlog 是對資料庫更改操作的記錄,裡面是一個個的event,如類似下面的event序列:

Query_log
Table_map
Write/Delete/Update_row_event
Xid

關於每個event的含義可以參考官方文件

配置

並行複製提供了幾個引數配置,可以通過修改引數值對其進行調節。

slave_parallel_workers           // worker 執行緒個數
slave-checkpoint-group           // 隔多少個事務做一次 checkpoint
slave-checkpoint-period          // 隔多長時間做一次 checkpoint
slave-pending-jobs-size-max      // 分發給worker的、處於等待狀態的event的大小上限

概念術語

下面是並行複製中用到幾個概念:

MTS            // Multi-Threaded Slave,並行複製
group          // 一個事務在binlog中對應的一組event序列
worker         // 簡稱W,event 執行執行緒,MTS新引入的
Coordinator    // 簡稱C,分發協作執行緒,就是之前的 SQL執行緒
checkpoint     // 簡稱CP,檢查點,C執行緒在滿足一定條件下去做,目的是收集W執行緒執行完資訊,向前推動執行位點
B-event        // 標誌事務開始的event,BEGIN 這種Query或者GTID
G-event        // 包含分發資訊的event,如Table_map、Query
T-event        // 標誌事務結束的event,COMMIT/ROLLBACK 這種Query 或者XID

相關程式碼檔案

sql/rpl_rli_pdb.h // pdb的是 parallelized by db name簡寫WL#5563
sql/rpl_rli_pdb.cc
sql/rpl_slave.cc
sql/log_event.cc
sql/rpl_rli.h

並行執行原則

  1. 並行執行的基本模型是生產者-消費者,C執行緒將event按db插入各W執行緒的任務佇列,W執行緒從佇列裡取出event執行;
    MTS 並行複製模型
  2. 同一個group(事務)內的event都發給同一個worker,保證事務的一致性;
  3. 分發關係由包含db資訊的event(G-evnet)決定,其它event按決定好的關係進行分發;

重要資料結構

  1. db_worker_hash_entry,db->worker 對映關係,也即分發關係,所有的分發關係快取在C的一個HASH表中(APH)

     - db               // db 名
     - worker           // 指向worker的指標,表示被分發到的W執行緒
     - usage            // 有多少正在分發的group用到這個關係
     - temporary_tables // 用於在C和W之前傳遞臨時表
    
  2. slave_job_item,worker的jobs佇列的成員

     - data          // 就是一個binlog event
    
  3. circular_buffer_queue,用DYNAMIC_ARRAY arrary實現的一個首尾相連的環形佇列,是其他重要資料結構的基類

     - Q              // 底層用到的 DYNAMIC_ARRAY
     - size           // Queue 的容量
     - avail          // 佇列尾
     - entry          // 佇列頭
     - len            // 佇列實際大小
     - de_queue()     // 出隊操作
     - de_tail()      // 尾部出隊
     - en_queue()     // 入隊
     - head_queue()   // 取佇列頭,但是不出隊
    
  4. Slave_job_group,維護一個正在執行的事務的資訊,如對應的位點資訊、事務分發到的worker、有沒有執行完等。

     - group_master_log_name      // 對應主庫的 binlog 檔名
     - group_master_log_pos       // 對應在主庫 binlog 中的位置
     - group_relay_log_name       // 對應備庫 relaylog 檔名
     - group_relay_log_pos        // 對應在備庫 relaylog 中的位置
     - worker_id                  // 對應的worker的id
     - worker                     // worker 指標
     - total_seqno                // 當前group是啟動以來執行的第幾個group
     - master_log_pos             // group中B-event的位置
     - checkpoint_seqno           // 當前group是從上次做完CP後的第幾個group
     - checkpoint_log_pos         // worker收到checkpoint訊號後更新
     - checkpoint_log_name        // 同上
     - checkpoint_relay_log_pos   // 同上
     - checkpoint_relay_log_name  // 同上
     - done                       // 這個group是否已經被worker commit掉
     - shifted                    // checkpoint 的時候shift值
     - ts                         // 時間,更新SBM
     - reset()                    // 重置上面的成員變數
    
  5. Slave_committed_queue,維護分發執行的group資訊,是circular_buffer_queue的子類,佇列裡存的時 Slave_job_group

     - lwm                        // 型別是Slave_job_group,低水位(Low-Water-Mark),表示上次CP執行到的位置
     - last_done                  // 型別是一個DYNAMIC_ARRAY,裡面存的是Slave_job_group:total_seqno,表示每個worker執行到第幾個group
     - assigned_group_index       // 正在分發的group在GAQ中的位置
     - move_queue_head()          // 做checkpoint時,把已經commit的group移出佇列
     - get_job_group()            // 返回佇列指定位置的Slave_job_group
     - en_queue()                 // 入隊一個 Slave_job_group
    
  6. Slave_jobs_queue,任務佇列,也是circular_buffer_queue的子類,佇列裡存的是slave_job_item,每個worker有一個這樣的任務佇列

     - overfill                  // 佇列滿標誌
     - waited_overfill           // 佇列滿的次數
    
  7. Slave_worker,對應一個worker,Relay_log_info 的子類

     - jobs                           // 型別是 Slave_jobs_queue,C分發過來的event都放在這裡面
     - c_rli                          // 指向C的指標
     - curr_group_exec_parts          // 型別是 DYNAMIC_ARRAY,裡面存的是當前group用到的分發關係,是指向APH成員的指標,簡寫CGEP
     - curr_group_seen_begin          // 當前所在 group 有沒有解析到 B-event
     - id                             // worker 的id標識
     - last_group_done_index          // worker上一次執行的group在GAQ中的位置
     - gaq_index                      // worker 當前執行的的事務在GAQ中的位置
     - usage_partition                // worker用到的分發關係個數
     - end_group_sets_max_dbs         // 和序列執行相關的
     - bitmap_shifted                 // CP後bitmap需要偏移的距離,用於調整 group_executed
     - wq_overrun_cnt                 // 超載多少
     - overrun_level                  // 超載指標
     - underrun_level                 // 飢餓指標
     - excess_cnt                     // 用於往mts_wq_excess_cnt累計
     - group_executed                 // 型別是 MY_BITMAP,標示CP後執行的group
     - group_shifted                  // 型別是 MY_BITMAP,計算group_executed,臨時用作中間變數
     - running_status                 // 標識 worker 執行緒的狀態,可以有 NOT_RUNNING、RUNNING、ERROR_LEAVING、KILLED
     - slave_worker_ends_group ()     // 當一個group執行完或者異常終止時會呼叫
     - commit_positions()             // group執行完是呼叫,用於更新位點和bitmap
     - rollback_positions()           // 回滾bitmap
    
  8. Relay_log_info,對應C執行緒,在MTS之前對應SQL執行緒,為了支援並行複製,在原來的基礎上又加了一些成員

     - mapping_db_to_worker           // 非常重要的成員,型別是HASH,用於快取所有的分發關係,APH(Assigned Partition Hash),目的能通過db快速找到對映關係,但HASH長度大於mts_partition_hash_soft_max(固定16)時,會對沒有使用的對映關係進行回收。
     - workers                        // 型別是 DYNAMIC_ARRAY,成員是一個個Slave_worker
     - pending_jobs                   // 一個統計資訊,表示待執行job個數
     - mts_slave_worker_queue_len_max // 每個worker最多能容納jobs的個數,目前hard code是16384
     - mts_pending_jobs_size          // 所有worker的job佔的記憶體
     - mts_pending_jobs_size_max      // 所有worker的job佔的記憶體,對應配置 slave_pending_jobs_size_max
     - mts_wq_oversize                // 標示job佔用記憶體已達上限
     - gaq                            // 非常重要的成員,程式碼註釋裡經常提到的GAQ,型別是Slave_committed_queue,存的成員是Slave_job_group,大小對應配置 slave-checkpoint-group,用於W和C互動
     - curr_group_assigned_parts      // 型別是 DYNAMIC_ARRAY,當前group中已經分配的event的對映關係,可以和Slave_worker的curr_group_exec_parts對應,簡寫CGAP
     - curr_group_da                  // 型別是DYNAMIC_ARRAY,對於還無法決定分發worker的event,先存在這裡
     - mts_wq_underrun_w_id           // 標識比較空閒的worker的id
     - mts_wq_excess_cnt              // 標示worker的超載情況
     - mts_worker_underrun_level      // 當W的任務佇列大小低於這個值的認為處於飢餓狀態
     - mts_coordinator_basic_nap      // 當work負載較大時,C執行緒sleep,會用到這個值
     - opt_slave_parallel_workers     // 對應配置 slave_parallel_workers
     - slave_parallel_workers         // 當前實際的worker數
     - exit_counter                   // 退出時用
     - max_updated_index              // 退出時用
     - checkpoint_seqno               // 上次CP後分發的group個數
     - checkpoint_group               // 對應配置 mts_checkpoint_group
     - recovery_groups                // 型別是 MY_BITMAP,恢復時用到
     - mts_group_status               // 分發執行緒所處的狀態,取值為 MTS_NOT_IN_GROUP、MTS_IN_GROUP、MTS_END_GROUP、MTS_KILLED_GROUP
     - mts_events_assigned            // 分發的event計數
     - mts_groups_assigned            // 分發的group計數
     - least_occupied_workers         // 型別是 DYNAMIC_ARRAY,從註釋將worker按從空閒到繁忙排序的一個陣列,用於先worker用,但是實際並未用到。 
     - last_clock                     // 上次做checkpoint的時間
    
  9. 其它方法

     map_db_to_worker()             // 把db對映給worker
     get_least_occupied_worker()    // 獲取負載最小的worker
     wait_for_workers_to_finish()   // 等待worker完成,併發臨時轉成序列是用到
     append_item_to_jobs()          // 把任務分發給 worker
     mts_move_temp_table_to_entry() // 用於傳遞臨時表
     mts_move_temp_tables_to_thd()  // 同上
    

初始化

和單執行緒SQL相比,MTS需要初始化新加的MTS變數和啟動worker執行緒。

主要是slave_start_workers()這個函式。會初始化C執行緒的MTS變數,如workers、curr_group_assigned_parts、curr_group_da、gaq等,接著呼叫init_hash_workers() 初始化HASH表mapping_db_to_worker,在這些做完後依次呼叫 slave_start_single_worker() 初始化每個worker並啟動W執行緒。worker 的的初始化包括jobs任務佇列、curr_group_exec_parts 等相關變數,其中jobs長度目前是固定的16384,目前還不可配置;worker執行緒的主函式是handle_slave_worker(),不停的呼叫slave_worker_exec_job()來執行C分配的event。

Coordinator 分發協作

分發執行緒主體和之前的SQL執行緒基本是一樣的,不停的呼叫 exec_relay_log_event() 函式。exec_relay_log_event()主要分2部分,一是呼叫next_event()讀取relay log,一是apply_event_and_update_pos() 做分發。

next_event() 比較簡單,就是不停的用 Log_event::read_log_event() 從relay log 讀取event,除此之外還會呼叫mts_checkpoint_routine() 做checkpoint,後面會詳細講checkpiont過程。

apply_event_and_update_pos()進行分發的入口是Log_event::apply_event(),如果沒有開MTS,就是原來的邏輯,SQL執行緒直接執行event,如果開了MTS的話,呼叫get_slave_worker(),這個是分發的主邏輯。

在介紹分發邏輯前,先將所有的binlog event 可以分下類(程式碼裡是這麼分的):

B-event   // BEGIN(Query) 或者 GTID
G-event   // 包含db資訊的event,Table_map 或者 Query
P-event   // 一般放在G-event前的,如int_var、rand、user_var等
R-event   // 一般放在G-event後的,如各種Rows event
T-event   // COMMIT/ROLLBACK(Query) 或者XID

分發邏輯是這樣的:

  1. 如果是B-event,表明是事務的開始,mts_groups_assigned 計數加1,同時GAQ中入隊一個新的group,表示一個新的事務開始,然後把event放入curr_group_da,因為B-event沒有db資訊,還不知道分發給哪個worker;
  2. 如果是G-event,event裡包含db資訊,就需要按這個db找到一個分發到的worker,worker選擇機制是map_db_to_worker()實現。呼叫map_db_to_worker()時,有2個引數比較重要,一個是dbname,這個就是分發關係的key,一個是last_worker,表示當前group中event上一次分發到的worker(last_assigned_worker);
    • 在當前group已經用到的對映關係(curr_group_assigned_parts CGAP)中找,如果有同db的對映關係,就直接返回last_worker;如果找不到,就去APH中按db名搜尋;
    • 如果APH中搜到的話,分3種情況,a) 這個對映關係沒有group用到,就直接把db對映為last_worker,如果last_worker為空的主話,就找一個最空閒的worker,get_least_occupied_worker() b) 這個對映關係有group用,並且對應的worker和last_worker一樣,就用last_worker,對映關係引用計數加1 c) 如果對映關係對應的worker和last_worker不一樣,這表示有衝突,就需要等到引用這個對映關係的group全部執行完,然後把db對映為last_worker;
    • 如果沒搜到的話,就新生成一個對映關係,key用db,value用last_worker,如果last_worker為空的話,選最空閒的worker,get_least_occupied_worker(),並把新生成的對映插入到APH中,如果HASP表長度大於 mts_partition_hash_soft_max 的話,在插入前會對APH做一次收縮,從中去除掉沒有被group引用的對映關係;
    • 把選擇的對映關係插入到 curr_group_assigned_parts 中。
  3. 如果是其它event,worker直接用last_assigned_worker。

什麼時候切換為序列?
如果G-event包含的db個數大於MAX_DBS_IN_EVENT_MTS(16個),或者更新的表被外來鍵依賴,那麼就需要序列執行當前group。序列固定選用第0個worker來執行,在分發前會等待其它worker全部執行完,在分發後會等待所有worker執行完。gropu執行完後自動切換為並行執行。

worker 確定好了,下一步就是分發event了,入口函式 append_item_to_jobs()。這個函式的作用非常明確,就是把event插入到worker的jobs佇列中,在插入前會有對event大小有檢查:

  1. 如果event大小已經超過了等待任務大小的上限(配置slave-pending-jobs-size-max ),就報event太大的錯,然後返回;
  2. 如果event大小+已經在等待的任務大小超過了slave-pending-jobs-size-max,就等待,至到等待佇列變小;
  3. 如果當前的worker的佇列滿的話,也等待。

Worker 執行

W執行緒執行的主邏輯是 slave_worker_exec_job():

  1. 從自己的job佇列裡取出event;
  2. 根據event的資訊,來更新worker中的變數,如curr_group_exec_parts(CGEP)、future_event_relay_log_pos、gaq_index等;
  3. 執行event,do_apply_event_worker(),最終呼叫每個event的do_apply_event()方法,和單執行緒下一樣;
  4. 如果是T event,呼叫 slave_worker_ends_group(),表示一個事務已經執行完了,a) 更新位點,通過commit_positions(),更新事務在GAQ中對應的Slave_job_group,這樣C就知道W執行到哪了,另外還會更新W的bitmap資訊(如果是xid event,在apply_event中就會呼叫commit_positions) b) 清空 curr_group_exec_parts,將對映關係中的引用數減1;
  5. 更新C的佇列統計資訊,如等待執行任務數pending_jobs,等待執行任務大小mts_pending_jobs_size等;
  6. 更新 overrun 和 underrun 狀態。

分發和執行邏輯可以用下圖簡單表示:
MTS 分發邏輯
C執行緒在GAQ中插入group,標示一個要執行的事務,接著確定分發關係(從CGAP或者APH中,或者生成新的),然後按對映關係把event分發給對應worker的job佇列;worker在執行event過程中更新自己的CGEP,在執行完整個group後,根據CGEP中的記錄去更新APH中引用關係的計數,同時把GAQ中的對應group標示為done。

checkpoint 過程

如前所述,C執行緒會在從relaylog讀取event後,會嘗試做checkpoint,入口函式是mts_checkpoint_routine()。checkpoint的作用是把worker執行完的事務從GAQ中去除,向前推進事務完成點。

有2個條件會觸發checkpoint:

  1. 當前時間距上次checkpoint已經超過配置 mts-checkpoint-period,這時會嘗試做一次checkpoint,不管有沒有向前推進事務;
  2. 上一次checkpoint後分發的事務數已經到達checkpoint設定上限(slave-checkpoint-group),這時會強制做checkpoint,如果一次checkpoint沒成功,會一直重試,直至成功。

GAQ中的事務推進通過 Slave_committed_queue::move_queue_head() 實現,從前向後掃描GAQ中的group:

  1. 如果當前group已經完成(通過標誌Slave_job_group.done標誌確認),就把這個group出隊,同時把這個出隊的group資訊賦給低水位lwm,向前推進;
  2. 如果遇到沒有完成的group,就是遇到一個gap,表示對應worker還沒執行完當前group,checkpoint不能再向前推進了,到此結束,返回值就是退出前已經推進的group個數。

MTS checkpoint邏輯

slave 停止

類似單執行緒複製,stop slave 命令會終止C執行緒和W執行緒的執行。

C執行緒收到退出訊號後,會先呼叫slave_stop_workers()終止W執行緒,過程如下:

  1. 依次把每個執行中的 worker 的 runnig_status 設定Slave_worker::STOP,同時設定worker執行終止位置rli->max_updated_index
  2. C執行緒等待所有W執行緒終止(w->running_status == Slave_worker::NOT_RUNNING);
  3. 呼叫mts_checkpoint_routine(),做一次checkpoint;
  4. 釋放資源,如APH、GAQ、CGDA(curr_group_da)、CGAP(curr_group_assigned_parts)等。

W執行緒在pop_jobs_item()中會呼叫set_max_updated_index_on_stop(),會檢查2個條件 1) job佇列是空的,2) 當前worker執行的事務在GAQ中的位置,是否已經超過rli->max_updated_index;任一條件滿足就設定狀態 running_status 為 Slave_worker::STOP_ACCEPTED,表示開始退出。

從上面的邏輯可以看出,在收到stop訊號後,worker執行緒會等正在執行的group完成後,才會退出。

異常退出

W被kill或者執行出錯

  1. slave_worker_exec_job() 進入錯誤處理邏輯,呼叫Slave_worker::slave_worker_ends_group(),給C執行緒發KILL_QUERY訊號,然後做相關變數的清理,把job佇列的任務全部清理掉,最終把running_status置為Slave_worker::NOT_RUNNING,表示結束;
  2. C執行緒收到kill訊號後,停止分發,然後進入slave_stop_workers()邏輯,給活躍的W執行緒傳送STOP訊號;
  3. 其它W執行緒收到STOP訊號後,會處理job佇列中所有的event;
  4. 和stop slave不同的是,C執行緒最後不會做checkpoint。

C被kill

C被kill的處理邏輯和stop slave差不多,不同之處在於等worker全部終止後,不會做checkpoint。

恢復

Slave執行緒重啟(正常關閉或者異常kill)後,需要根據Coordinator和每個Worker的記錄資訊來進行恢復,推進到一個一致狀態後再開始並行,詳細過程我們下期月報再分析。

存在的問題

5.6 的MTS是按db來進行分發的,分發粒度太大,如果只有一個db的時候,就沒有併發性了,所有group都分給一個worker,就變成單執行緒執行了。一個簡單的優化改進是改成按table來分發,只需要把分發的key從dbname改成dbname + tablename,整體分發邏輯不需要變動。再進一步,如果遇到熱點表更新呢,這時候binlog裡記錄的event都是針對一個表的更新,又會變成序列執行。這個時候就需要變化一下分發測略嘍,如按事務維度進行分發,這個策略對原始碼的改動就會比較大些,有需要的同學可以試試:-)

相關文章