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

作者:楊非

本文爲 DM 源碼閱讀系列文章的第四篇,上篇文章 介紹了數據同步處理單元實現的功能,數據同步流程的運行邏輯以及數據同步處理單元的 interface 設計。本篇文章在此基礎上展開,詳細介紹 dump 和 load 兩個數據同步處理單元的設計實現,重點關注數據同步處理單元 interface 的實現,數據導入併發模型的設計,以及導入任務在暫停或出現異常後如何恢復。

dump 處理單元

dump 處理單元的代碼位於 github.com/pingcap/dm/mydumper 包內,作用是從上游 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/loader 包內,該處理單元在 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 兩個數據同步處理單元的實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章