DM 源码阅读系列文章(四)dump/load 全量同步的实现丨TiDB 工具

本文详细介绍了DM中的dump和load处理单元,dump单元使用mydumper导出上游MySQL的数据,不记录checkpoint,依赖完整重跑确保一致性。load单元则负责解析和执行dump的SQL文件,使用checkpoint跟踪导入进度,支持断点续传和错误恢复。文章探讨了并发模型、Mydumper的执行流程以及列值转换和库表路由策略。
摘要由CSDN通过智能技术生成

本文为 TiDB Data Migration 源码阅读系列文章的第四篇, 《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 guard和 lock 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-locks和 trx-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。我们重点分析 Init和 Process两个 interface 的实现。

Init 实现细节

该阶段进行一些初始化和清理操作,并不会开始同步任务,如果在该阶段运行中出现错误,会通过 rollback 机制 清理资源,不需要调用 Close 函数。该阶段包含的初始化操作包括以下几点:

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 语句中 ,具体执行时这些语句会 在一个事务中提交 ,这样就保证了断点信息的准确性,如果导入过程暂停或中断,恢复任务后从断点重新同步可以保证数据一致。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
json.dump和json.load是Python中用于进行JSON数据的序列化和反序列化的方法。json.dump用于将dict类型的数据转换为str,并将其写入到json文件中,可以使用with open()的方式或者直接使用json.dump()将数据写入文件。例如: ```python import json data = {'name': 'John', 'age': 30, 'city': 'New York'} filename = 'data.json' # Solution 1 json_str = json.dumps(data) with open(filename, 'w') as f: f.write(json_str) # Solution 2 json.dump(data, open(filename, 'w')) ``` 而json.load则用于从json文件中读取数据,将其解析为Python数据结构。例如: ```python import json filename = 'data.json' # Solution 1 with open(filename, 'r') as f: json_str = f.read() data = json.loads(json_str) # Solution 2 data = json.load(open(filename, 'r')) ``` 这样就可以将json文件中的数据读取出来并转换为Python的dict类型了。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [json.load json.dump 和 json.loads json.dumps 全解析](https://blog.csdn.net/cl965081198/article/details/94135502)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [json.dumps()和json.loads()、json.dump()和json.load()的区分](https://blog.csdn.net/weixin_62848630/article/details/124637501)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Python基础详解(十五):json.dump()、json.dumps()、json.load()、json.loads()](https://blog.csdn.net/zhu_rui/article/details/123025943)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天读点书学堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值