作者:楊非
本文為 DM 原始碼閱讀系列文章的第四篇,上篇文章 介紹了資料同步處理單元實現的功能,資料同步流程的執行邏輯以及資料同步處理單元的 interface 設計。本篇文章在此基礎上展開,詳細介紹 dump 和 load 兩個資料同步處理單元的設計實現,重點關注資料同步處理單元 interface 的實現,資料匯入併發模型的設計,以及匯入任務在暫停或出現異常後如何恢復。
dump 處理單元
dump 處理單元的程式碼位於 github.com/pingcap/dm/… 包內,作用是從上游 MySQL 將表結構和資料匯出到邏輯 SQL 檔案,由於該處理單元總是執行在任務的第一個階段(full 模式和 all 模式),該處理單元每次執行不依賴於其他處理單元的處理結果。另一方面,如果在 dump 執行過程中被強制終止(例如在 dmctl 中執行 pause-task 或者 stop-task),也不會記錄已經 dump 資料的 checkpoint 等資訊。不記錄 checkpoint 是因為每次執行 mydumper 從上游匯出資料,上游的資料都可能發生變更,為了能得到一致的資料和 metadata 資訊,每次恢復任務或重新執行任務時該處理單元會 清理舊的資料目錄,重新開始一次完整的資料 dump。
匯出表結構和資料的邏輯並不是在 DM 內部直接實現,而是 通過 os/exec
包呼叫外部 mydumper 二進位制檔案 來完成。在 mydumper 內部,我們需要關注以下幾個問題:
-
資料匯出時的併發模型是如何實現的。
-
no-locks, lock-all-tables, less-locking 等引數有怎樣的功能。
-
庫表黑白名單的實現方式。
mydumper 的實現細節
mydumper 的一次完整的執行流程從主執行緒開始,主執行緒按照以下步驟執行:
-
解析引數。
-
會根據
no-locks
選項進行一系列的備份安全策略,包括long query guard
和lock all tables or FLUSH TABLES WITH READ LOCK
。 -
如果配置了
trx-consistency-only
選項,執行UNLOCK TABLES /* trx-only */
釋放之前獲取的表鎖。注意,如果開啟該選項,是無法保證非 InnoDB 表匯出資料的一致性。更多關於一致性讀的細節可以參考 MySQL 官方文件 Consistent Nonlocking Reads 部分。 -
如果沒有配置
no-locks
和trx-consistency-only
選項,執行 UNLOCK TABLES /* FTWRL */ 釋放鎖。
工作執行緒的併發控制包括了兩個層面,一層是在不同表級別的併發,另一層是同一張表級別的併發。mydumper 的主執行緒會將一次同步任務拆分為多個同步子任務,並將每個子任務分發給同一個非同步佇列 conf.queue_less_locking/conf.queue
,工作子執行緒從佇列中獲取任務並執行。具體的子任務劃分包括以下策略:
-
開啟
less-locking
選項的非 InnoDB 表的處理。- 先將所有
non_innodb_table
分為num_threads
組,分組方式是遍歷這些表,依此將遍歷到的表加入到當前資料量最小的分組,儘量保證每個分組內的資料量相近。 - 上述得到的每個分組內會包含一個或多個非 InnoDB 表,如果配置了
rows-per-file
選項,會對每張表進行chunks
估算,對於每一張表,如果估算結果包含多個 chunks,會將子任務進一步按照chunks
進行拆分,分發chunks
數量個子任務,如果沒有chunks
劃分,分發為一個獨立的子任務。 - 注意,在該模式下,子任務會 傳送到
queue_less_locking
,並在編號為num_threads
~ 2 *num_threads
的子執行緒中處理任務。less_locking_threads
任務執行完成之後,主執行緒就會 UNLOCK TABLES /* FTWRL */ 釋放鎖,這樣有助於減少鎖持有的時間。主執行緒根據conf.unlock_tables
來判斷非 InnoDB 表是否全部匯出,普通工作執行緒 或者 queue_less_locking 工作執行緒每次處理完一個非 InnoDB 表任務都會根據non_innodb_table_counter
和non_innodb_done
兩個變數判斷是否還有沒有匯出結束的非 InnoDB 表,如果都已經匯出結束,就會向非同步佇列conf.unlock_tables
中傳送一條資料,表示可以解鎖全域性鎖。- 每個
less_locking_threads
處理非 InnoDB 表任務時,會先 加表鎖,匯出資料,最後 解鎖表鎖。
- 先將所有
-
未開啟
less-locking
選項的非 InnoDB 表的處理。 -
InnoDB 表的處理。
- 與未開啟
less-locking
選項的非 InnoDB 表的處理相同,同樣是 按照表分發子任務,如果有chunks
子任務會進一步細分。
- 與未開啟
從上述的併發模型可以看出 mydumper 首先按照表進行同步任務拆分,對於同一張表,如果配置 rows-per-file
引數,會根據該引數和錶行數將表劃分為合適的 chunks
數,這即是同一張表內部的併發。具體表行數的估算和 chunks
劃分的實現見 get_chunks_for_table
函式。
需要注意目前 DM 在任務配置中指定的庫表黑白名單功能只應用於 load 和 binlog replication 處理單元。如果在 dump 處理單元內使用庫表黑白名單功能,需要在同步任務配置檔案的 dump 處理單元配置提供 extra-args 引數,並指定 mydumper 相關引數,包括 --database, --tables-list 和 --regex。mydumper 使用 regex 過濾庫表的實現參考 check_regex
函式。
load 處理單元
load 處理單元的程式碼位於 github.com/pingcap/dm/… 包內,該處理單元在 dump 處理單元執行結束後執行,讀取 dump 處理單元匯出的 SQL 檔案解析並在下游資料庫執行邏輯 SQL。我們重點分析 Init
和 Process
兩個 interface 的實現。
Init 實現細節
該階段進行一些初始化和清理操作,並不會開始同步任務,如果在該階段執行中出現錯誤,會通過 rollback 機制 清理資源,不需要呼叫 Close 函式。該階段包含的初始化操作包括以下幾點:
-
建立
checkpoint
,checkpoint
用於記錄全量資料的匯入進度和 load 處理單元暫停或異常終止後,恢復或重新開始任務時可以從斷點處繼續匯入資料。 -
應用任務配置的資料同步規則,包括以下規則:
Process 實現細節
該階段的工作流程也很直觀,通過 一個收發資料型別為 *pb.ProcessError
的 channel
接收執行過程中出現的錯誤,出錯後通過 context 的 CancelFunc
強制結束處理單元執行。在核心的 資料匯入函式 中,工作模型與 mydumper 類似,即在 主執行緒中分發任務,有多個工作執行緒執行具體的資料匯入任務。具體的工作細節如下:
-
主執行緒會按照庫,表的順序讀取建立庫語句檔案
<db-name>-schema-create.sql
和建表語句檔案<db-name>.<table-name>-schema-create.sql
,並在下游執行 SQL 建立相對應的庫和表。 -
主執行緒讀取
checkpoint
資訊,結合資料檔案資訊建立 fileJob 隨機分發任務給一個工作子執行緒,fileJob 任務的結構如下所示 :type fileJob struct { schema string table string dataFile string offset int64 // 表示讀取檔案的起始 offset,如果沒有 checkpoint 斷點資訊該值為 0 info *tableInfo // 儲存原庫表,目標庫表,列名,insert 語句 column 名字列表等資訊 } 複製程式碼
-
在每個工作執行緒內部,有一個迴圈不斷從自己
fileJobQueue
獲取任務,每次獲取任務後會對檔案進行解析,並將解析後的結果分批次打包為 SQL 語句分發給執行緒內部的另外一個工作協程,該工作協程負責處理 SQL 語句的執行。工作流程的虛擬碼如下所示,完整的程式碼參考func (w *Worker) run()
:// worker 工作執行緒內分發給內部工作協程的任務結構 type dataJob struct { sql string // insert 語句, insert into <table> values (x, y, z), (x2, y2, z2), … (xn, yn, zn); schema string // 目標資料庫 file string // SQL 檔名 offset int64 // 本次匯入資料在 SQL 檔案的偏移量 lastOffset int64 // 上一次已匯入資料對應 SQL 檔案偏移量 } // SQL 語句執行協程 doJob := func() { for { select { case <-ctx.Done(): return case job := <-jobQueue: sqls := []string{ fmt.Sprintf("USE `%s`;", job.schema), // 指定插入資料的 schema job.sql, checkpoint.GenSQL(job.file, job.offset), // 更新 checkpoint 的 SQL 語句 } executeSQLInOneTransaction(sqls) // 在一個事務中執行上述 3 條 SQL 語句 } } } // worker 主執行緒 for { select { case <-ctx.Done(): return case job := <-fileJobQueue: go doJob() readDataFileAndDispatchSQLJobs(ctx, dir, job.dataFile, job.offset, job.info) } } 複製程式碼
-
dispatchSQL
函式負責在工作執行緒內部讀取 SQL 檔案和重寫 SQL,該函式會在執行初始階段 建立所操作表的checkpoint
資訊,需要注意在任務中斷恢復之後,如果這個檔案的匯入還沒有完成,checkpoint.Init
仍然會執行,但是這次執行不會更新該檔案的checkpoint
資訊。列值轉換和庫表路由也是在這個階段內完成。-
列值轉換:需要對輸入 SQL 進行解析拆分為每一個 field,對需要轉換的 field 進行轉換操作,然後重新拼接起 SQL 語句。詳細重寫流程見 reassemble 函式。
-
庫表路由:這種場景下只需要 替換源表到目標表 即可。
-
-
在工作執行緒執行一個批次的 SQL 語句之前,會首先根據檔案
offset
資訊生成一條更新 checkpoint 的語句,加入到打包的 SQL 語句中,具體執行時這些語句會 在一個事務中提交,這樣就保證了斷點資訊的準確性,如果匯入過程暫停或中斷,恢復任務後從斷點重新同步可以保證資料一致。
小結
本篇詳細介紹 dump 和 load 兩個資料同步處理單元的設計實現,對核心 interface 實現、資料匯入併發模型、資料匯入暫停或中斷的恢復進行了分析。接下來的文章會繼續介紹 binlog replication
,relay log
兩個資料同步處理單元的實現。