DM 原始碼閱讀系列文章(四)dump/load 全量同步的實現

PingCAP發表於2019-04-28

作者:楊非

本文為 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 的一次完整的執行流程從主執行緒開始,主執行緒按照以下步驟執行:

  1. 解析引數。

  2. 建立到資料庫的連線

  3. 會根據 no-locks 選項進行一系列的備份安全策略,包括 long query guardlock all tables or FLUSH TABLES WITH READ LOCK

  4. START TRANSACTION WITH CONSISTENT SNAPSHOT

  5. 記錄 binlog 位點資訊

  6. less locking 處理執行緒的初始化

  7. 普通匯出執行緒初始化

  8. 如果配置了 trx-consistency-only 選項,執行 UNLOCK TABLES /* trx-only */ 釋放之前獲取的表鎖。注意,如果開啟該選項,是無法保證非 InnoDB 表匯出資料的一致性。更多關於一致性讀的細節可以參考 MySQL 官方文件 Consistent Nonlocking Reads 部分

  9. 根據配置規則(包括 --database, --tables-list 和 --regex 配置)讀取需要匯出的 schema 和表資訊,並在這個過程中有區分的記錄 innodb_tables 和 non_innodb_table

  10. 為工作子執行緒建立任務,並將任務 push 到相關的工作佇列

  11. 如果沒有配置 no-lockstrx-consistency-only 選項,執行 UNLOCK TABLES /* FTWRL */ 釋放鎖

  12. 如果開啟 less-locking,等待所有 less locking 子執行緒退出

  13. 等待所有工作子執行緒退出

工作執行緒的併發控制包括了兩個層面,一層是在不同表級別的併發,另一層是同一張表級別的併發。mydumper 的主執行緒會將一次同步任務拆分為多個同步子任務,並將每個子任務分發給同一個非同步佇列 conf.queue_less_locking/conf.queue,工作子執行緒從佇列中獲取任務並執行。具體的子任務劃分包括以下策略:

從上述的併發模型可以看出 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。我們重點分析 InitProcess 兩個 interface 的實現。

Init 實現細節

該階段進行一些初始化和清理操作,並不會開始同步任務,如果在該階段執行中出現錯誤,會通過 rollback 機制 清理資源,不需要呼叫 Close 函式。該階段包含的初始化操作包括以下幾點:

Process 實現細節

該階段的工作流程也很直觀,通過 一個收發資料型別為 *pb.ProcessErrorchannel 接收執行過程中出現的錯誤,出錯後通過 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 replicationrelay log 兩個資料同步處理單元的實現。

DM 原始碼閱讀系列文章(四)dump/load 全量同步的實現

相關文章