MyDumper并行重建MySQL只读副本实战:从3小时到25分钟
发布时间:2026/9/19 3:50:00 作者:尧图编辑部 阅读量:1,286

上个月帮客户把一个约 800GB 的业务库从主库重建一个只读副本第一版方案用 mysqldump单线程导出硬生生跑了 3 个多小时还没算恢复时间。后来临时切到 MyDumper8 线程并行导出25 分钟完成myloader 导入 40 分钟整个重建链路从“要留一个通宵维护窗口”变成了“一顿午饭的时间”。这篇就把我这次实战里关于权限、参数、metadata 解析、复制配置、常见坑的完整记录写出来给准备用 MyDumper 重建 MySQL 副本的同学一个可以直接抄的作业。1. 先说结论重建副本时 MyDumper 比 mysqldump 强在哪1.1 mysqldump 在重建副本时的三个痛点用 mysqldump 重建副本大家最常走的套路是mysqldump --single-transaction --master-data2 -A all.sql然后在从库上 all.sql导入。这个流程在几十 GB 的小库上问题不大但库一大三个痛点非常致命。第一导出是单线程的。mysqldump 无论表有多少都是顺序一个个导出单线程跑满一个 CPU 核心其他核心全部闲着。第二导入也是单线程的。生成的是一个巨型 SQL 文件在从库执行时同样是单连接顺序执行尤其每条 INSERT 都带多行 VALUES 时解析和回放都慢。第三不方便做数据表级拆分和压缩。mysqldump 也可以压缩但基本都是整个文件一起压断了没法续单表出问题要整库重来。1.2 MyDumper 的并行模型为什么适合副本重建MyDumper 的理念很简单导出时按表或按行数把任务拆成多个 chunk每个 worker 线程各自连接数据库并行把数据拉下来。导入时 myloader 同样开多个线程并行执行多个文件。这样导出、导入都是多线程整体时间能压缩一个数量级。它的工作模型大致是主线程负责协调和锁一致性worker 线程负责实际导表。每个表的 dump 又可以选择按固定行数拆分拆成多个文件。这不仅是为了并行更是为了恢复时的粒度控制——一个文件失败不用整库重来单独处理那个文件即可。1.3 一个真实的对比数据我这次这个 800GB 实例大概是 2000 多张表最大的单表 1.2 亿行。mysqldump 导出耗时 3 小时 10 分导出文件约 420GB未压缩。后来换成 mydumper8 线程导出单表按 200 万行拆分耗时 26 分钟用-c压缩后只有 120GB。myloader 在目标实例上以 8 线程导入耗时 45 分钟。这个差距在需要频繁做新副本、做环境交付、做 staging 环境同步的场景下体验是完全不同的。提示如果你只是想偶尔备一个小库mysqldump 完全够用没必要为了用而用。但如果你的库超过 200GB、表数量上千或者经常要重建副本MyDumper 值得纳入工具箱。2. 动手前的检查清单权限、GTID、参数一页纸2.1 备份账号的权限最小集MyDumper 需要从主库读取数据、拿到 binlog 坐标、执行 FLUSH TABLES WITH READ LOCK如果不用一致性快照则不需要 FTWRL。所以备份账号的权限建议这样给权限作用SELECT读取表数据SHOW VIEW导出视图定义TRIGGER导出触发器定义若有RELOAD执行 FLUSH TABLES WITH READ LOCKREPLICATION CLIENT执行 SHOW MASTER STATUS / SHOW REPLICA STATUSBACKUP_ADMIN部分版本需要用于获取一致的位点LOCK TABLES早期版本 FTWRL 需要MySQL 8.0 里如果主库开了caching_sha2_password认证MyDumper 0.12 以上版本能正常支持低版本可能会报认证插件不支持建议直接装新版。CREATE USER backup_user% IDENTIFIED BY YourStrongPass; GRANT SELECT, SHOW VIEW, TRIGGER, RELOAD, REPLICATION CLIENT, BACKUP_ADMIN ON *.* TO backup_user%; FLUSH PRIVILEGES;2.2 优先使用 GTID 复制如果主库已经开启 GTIDgtid_modeON强烈建议重建副本时走 GTID 方式。原因很简单MyDumper 的 metadata 文件里会记录当前已执行或已导出的 GTID 集合恢复完成后直接把从库的gtid_purged设置好再 start replica 即可不用关心 binlog 文件名和 position 的配对问题。如果主库还没开 GTID需要用传统的MASTER_LOG_FILE和MASTER_LOG_POS方式那就要在导出期间拿到准确的点并保持一致性。MyDumper 的 metadata 文件里都有记录但要确保导出时不是通过--trx-consistency-only绕过 FTWRL 导致点不一致。建议的检查命令SHOW VARIABLES LIKE gtid_mode; SHOW MASTER STATUS;2.3 常用参数速查我在实际操作中常用的参数组合mydumper \ -h 主库IP -P 3306 \ -u backup_user -p 密码 \ -B 需要备份的库名 \ -o /data/backup/my_dump \ -r 2000000 \ -c \ -t 8 \ --trx-consistency-only \ --kill-long-queries \ --long-query-guard 120 \ --regex ^(?!(mysql\.|sys\.|information_schema\.|performance_schema\.)) \ -L mydumper.log \ -v 3解释一下几个关键参数-r 2000000每个数据文件最多 200 万行超过就拆成下一个文件。这个值直接决定恢复时的并行粒度。-c输出时用压缩格式myloader 导入时自动解压。-t 8导出线程数一般按 CPU 核数和主库负载情况选。--trx-consistency-only不开 FTWRL用 InnoDB 事务一致性读拿到快照对在主库还跑着业务的情况更友好。--regex排除系统库避免把mysql.user这种权限表也导出来造成元数据混乱。3. 全量备份实战一条 mydumper 命令跑出一个完整副本3.1 备份工作目录和数据文件布局执行 mydumper 后输出目录里大概是这样的结构/data/backup/my_dump/ ├── metadata ├── mydb.schema.sql ├── mydb.table1.sql ├── mydb.table1.00001.sql ├── mydb.table1.00002.sql ├── mydb.table2.sql ├── mydb.table2.00001.sql其中mydb.schema.sql是建库语句mydb.table1.sql是建表语句schemamydb.table1.00001.sql是数据文件。如果表行数小于-r指定的值则只会有一个数据文件。3.2 metadata 文件到底记录了什么metadata 是重建副本最关键的参考文件我每次都要打开看一下cat /data/backup/my_dump/metadata内容大致类似下面这样Started dump at: 2025-01-12 10:30:01 SHOW MASTER STATUS: Log: mysql-bin.000128 Pos: 44356621 GTID:1e7d5e1a-3f3c-11ee-9ff2-0242ac120002:1-43921, 2b1f6ff2-4d4e-11ef-a9a4-0242ac110003:1-1003 Finished dump at: 2025-01-12 10:31:55注意几个字段的含义Started dump at导出开始的时间也是这个一致的快照时间点。Log和Pos传统复制的 binlog 坐标从这个位置之后主库产生的 binlog 是从库需要继续追的增量。GTIDGTID 集合表示导出数据包含了这些事务之后需要追的是集合之外的新事务。3.3 备份期间主库 DDL 的处理MyDumper 在导出单表时如果主库恰好对这张表做了 DDL比如加列、删索引最典型的表现是某个 worker 线程报Table definition has changed, please retry transaction。这种错误通常只影响个别表我的处理方式是先重新单独备份那一张表或者如果时间允许干脆在业务低峰期重跑一次全量。另外--trx-consistency-only依赖 RR 隔离级别下的START TRANSACTION WITH CONSISTENT SNAPSHOT所以 InnoDB 表的快照是一致的但 MyISAM 表在备份期间会被锁这也是一个要提前确认的点。现在业务库基本都是 InnoDB影响不大但如果你库里还混着 MyISAM 表要么提前迁移要么做好锁表时间可能拉长的准备。3.4 备份时的日志怎么看mydumper 的日志如果开了-L指向文件导出过程中可以实时观看进度。关键指标是每个表的 dump 是否完成以及是否有 warningtail -f mydumper.log如果你看到类似这样的一行** Message: 10:31:02 [INFO]: Queue 8, Table: mydb.big_table, Rows: 23405000, Filename: mydb.big_table.00001.sql说明big_table已经拆了第一个文件200 万行数据已经先落盘继续往下拆。如果某张表的文件一直没有更新往往说明它正在等待 DML 锁或主库查询资源紧张。4. 在新实例上还原myloader 导入和副本装配4.1 新实例的前置准备还原之前目标实例要先初始化好。我的习惯是版本尽量保持和主库一致至少大版本一致比如主库是 8.0.36从库也装 8.0.x别拿 5.7 去恢复 8.0 的数据。server-id必须和主库不同这是复制配置的基础。如果是走 GTID先把gtid_mode参数正确配置如果主从都已开启 GTID一般步骤是在配置文件中设置gtid-modeON等参数然后重启实例。如果是从一个已运行过的实例来恢复最好先RESET MASTER确认该实例可以被重置时避免孤儿 GTID 干扰。4.2 myloader 导入命令myloader \ -h 新从库IP -P 3306 \ -u restore_user -p 密码 \ -d /data/backup/my_dump \ -t 8 \ -B mydb \ -o \ --purge-mode1-d指定备份目录。-t线程数建议不要超过备份目录里的文件数否则部分线程空闲。-o覆盖已有表。-B只恢复指定库如果你的备份目录里有多个库而只想先恢复其中一个。--purge-mode1表示在导入前删除目标库中已存在的同名表相当于“重建目标表”适合副本重建场景。导入过程中可以通过另一个终端监控目标库的负载myloader 的日志也会显示每个线程处理了哪些文件。tail -f /tmp/myloader.log如果某个文件导入失败myloader 通常会把 worker 线程继续处理其他文件最后返回一个状态码。你需要回头单独导入失败文件。这种情况多数是因为表结构冲突或主键冲突解决后再补一次即可。4.3 恢复后的清理工作导入完成后有几个事情强烈建议做一下检查库表数量是否和主库一致对比SHOW TABLES数量、每张表的行数。检查触发器、视图、存储过程是否齐全mydumper 默认会导出这些逻辑对象但如果权限不够可能被静默跳过日志里会有 warning。如果需要设置复制账号提前建好repl_user。CREATE USER repl_user% IDENTIFIED BY ReplPass123; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO repl_user%;4.4 拿到复制起点这一步要分两种情况。GTID 模式登录主库执行SHOW MASTER STATUS;把返回的Executed_Gtid_Set记下来或者直接查看备份目录下 metadata 文件里的 GTID 字段。从库恢复完数据后需要设置RESET MASTER; SET GLOBAL gtid_purged...备份时导出的GTID集合...;注意gtid_purged的值必须包含你导入数据中所有已存在的事务。如果你之前在从库上有写入或误操作gtid_purged可能因为 GTID 冲突而设置失败这时需要更精细地清理。传统 binlog 位置模式直接使用 metadata 文件里的Log和Pos值。前提是主库的 binlog 还没被清除binlog_expire_logs_seconds覆盖范围内否则从库会报Got fatal error 1236。5. 配置复制并追上主库从 Seconds_Behind_Source 说起5.1 用 CHANGE REPLICATION SOURCE 指定主库MySQL 8.0 官方推荐的新语法是CHANGE REPLICATION SOURCE TO老版本语法CHANGE MASTER TO在 8.0 里仍然兼容但新版本我更建议直接用新写法。GTID 模式的配置CHANGE REPLICATION SOURCE TO SOURCE_HOST主库IP, SOURCE_PORT3306, SOURCE_USERrepl_user, SOURCE_PASSWORDReplPass123, SOURCE_AUTO_POSITION1;传统坐标模式的配置CHANGE REPLICATION SOURCE TO SOURCE_HOST主库IP, SOURCE_PORT3306, SOURCE_USERrepl_user, SOURCE_PASSWORDReplPass123, SOURCE_LOG_FILEmysql-bin.000128, SOURCE_LOG_POS44356621;启动复制START REPLICA; SHOW REPLICA STATUS\G5.2 判断追平进度的正确姿势很多人只盯Seconds_Behind_Source但这个值在刚启动复制时经常是 0 或者 NULL很容易误判。实际判断追平与否要看两个Retrieved_Gtid_Set是否已经包含主库当前最新的 GTID 集合。Seconds_Behind_Source稳定且接近 0并且SQL_Delay为 0。更直接的确认方法是在主库执行一些小的写入比如在一个测试表上 insert 一行然后在从库查同样数据是否出现延迟能控制在秒级就算追平。SHOW REPLICA STATUS\G重点看这几个字段字段期望值Replica_IO_RunningYesReplica_SQL_RunningYesRetrieved_Gtid_Set持续向主库的最新 GTID 靠拢Seconds_Behind_Source越小越好稳定为 0 最佳Last_IO_Error空Last_SQL_Error空5.3 追平后的数据校验副本重建完不能光看复制状态就上线还要做一次数据校验。我的做法是抽几张大表做行数和部分列对比再随机抽几张小表做全表校验。有 Percona Toolkit 环境的可以直接用pt-table-checksum做主从数据一致性校验pt-table-checksum --host主库IP --userchecksum_user --passwordxxx --databasesmydb --replicatepercona.checksums然后到从库上查percona.checksums里差异结果是0的表。如果某个表差异不为 0优先检查该表最近的 DML 是否因为正则排除、触发器等导致漏数据或重复数据。绝大多数情况在正常使用 MyDumper 的一致性备份下全量完成后校验都是一致的。6. 踩坑集这几个问题我都在生产环境里遇到过6.1 权限不足导致触发器、视图被跳过第一次用 MyDumper 时备份账号我少给了一个TRIGGER权限结果导出的 schema 文件里建表语句都正常但触发器一个都没有。日志里有一堆 warning当时没注意恢复完去查触发器数量才发现少了。建议在备份前就用SHOW TRIGGERS和SHOW FULL TABLES WHERE Table_type LIKE %VIEW%先确认主库上有多少这类对象导出后再对比一次。6.2--trx-consistency-only和长事务的关系--trx-consistency-only其实不需要 FTWRL而是依赖事务一致性读。但前提是主库上没有长时间未提交的事务。如果主库有一个跑了半小时的大事务MyDumper 在导这张表时会一直等待这个事务释放行锁表现为日志里某张表的文件长时间不更新整体备份时间被拖长。我遇到过极端情况主库一个事务跑了一个多小时MyDumper 一直卡在某张表上。解决方案是结合--kill-long-queries和--long-query-guard限制超过阈值的长查询但这个参数要慎用小心把正常业务的大查询杀掉。我的建议是非业务高峰期做全量备份不要把--kill-long-queries开得那么激进如果高峰期必须备份可以把超时时间调大避免误杀。6.3 认证插件导致连接失败MySQL 8.0 默认的认证插件是caching_sha2_password如果 mydumper/myloader 编译时链接的客户端库比较老连接时可能报ERROR 2061 (HY000): Authentication plugin caching_sha2_password cannot be loaded解决方式有两个一是升级 MyDumper 到 0.12 以上版本并配套新版 libmysqlclient二是对备份账号指定mysql_native_password认证ALTER USER backup_user% IDENTIFIED WITH mysql_native_password BY YourStrongPass;但要注意 MySQL 8.4 起mysql_native_password默认被禁用所以长期方案还是升级工具版本。6.4 备份文件过多导致 inode 耗尽我第二次用 MyDumper 时表特别多又按 10 万行拆了一次结果整个备份目录生成了接近 50 万个文件。拷贝到从库时速度慢不说还差点把磁盘分区 inode 耗尽。建议用df -i提前检查备份目录所在分区的 inode 数量。单表行数很大的表才需要拆成多文件小表不需要拆也无法拆。-r值不要设得太小我一般 100 万到 300 万行一个文件比较平衡。6.5 部分库备份恢复后复制起点不完整用-B指定备份某个库时metadata 里记录的是主库当时的全局 binlog 位置和 GTID。恢复单库后如果用这个全局位置去配置复制从库去重放 binlog 时会听到主库上其他库的写入比较容易出现主键冲突或“上一次事务从库已经存在”之类的问题。如果确实只需要某个库的副本建议在主库上对该库单独从业务层面确认没有其他库的写入依赖或者干脆在从库复制链路里加上CHANGE REPLICATION FILTER (REPLICATE_DO_DB (mydb))这样的过滤。不过过滤规则要小心毕竟复制过滤会影响级联复制下的行为生产环境尽量避免。6.6 myloader 导入时主键冲突常见原因从备份恢复到停写的测试库一般不会有主键冲突。但在一个还有业务的实例上恢复或者之前导入过一次只覆盖了部分表就容易出现ERROR 1062 (23000): Duplicate entry xxx for key PRIMARY我的处理建议是导入前先明确目标实例要处于“纯净”状态--purge-mode1配合使用。如果做副本重建新实例里不应该有任何业务数据。如果确实要保留部分表请用-B配合--only-schema、--skip-triggers等参数按需导入而不是盲目整库覆盖。7. 实例再大一档时的调优思路和自动化扩展7.1 调整线程数与-r的配合线程数的选择不是越大越好。我在 16 核的备机上做过测试8 线程和 16 线程导出时间差别不大但 16 线程对主库造成的压力明显更高尤其是大量随机读容易打高 IOPS。常见的经验值是 4 到 8 线程起步观察主库SHOW PROCESSLIST里的StateSystem lock和磁盘 IO 再往上调。-r的值如果设得太小文件数量会爆炸设得太大恢复时线程容易因为一个大文件而出现“尾巴”——比如一个 5000 万行的表被拆成 20 个文件其中最后一个文件只有几十行恢复时某些线程提前空闲整体时间仍被最大文件拖住。我用 200 万行左右做基准再根据表行数分布微调。7.2 备份完成后立刻做一次快速校验除了全量结束后用 pt-table-checksum我还会在导入阶段完成后做一个非常快的校验对比主库和从库的information_schema.tables里每张表的行数。可以用一条 SQL 完成SELECT table_name, table_rows FROM information_schema.tables WHERE table_schemamydb ORDER BY table_name;注意table_rows是估算值不是精确值只适合用来发现明显异常比如某张表完全没导入或行数差了一个数量级。真正要精确校验还是要COUNT(*)或pt-table-checksum。7.3 把整个流程脚本化如果重建副本的频率不低建议把整个流程扣成脚本备份、拷贝、初始化从库、导入、配置复制、校验。流程里的可变量用参数传递。我自己的脚本里核心步骤大概是主库执行 mydumper保留 metadata 文件。通过 rsync 增量同步备份目录到目标机器只传新增文件。目标机器初始化 MySQL 实例。myloader 导入全量备份。根据 metadata 读取 GTID 或 binlog 坐标。配置复制并 START REPLICA。循环检查追平状态。追平后执行 pt-table-checksum 并输出报告。脚本跑起来后副本重建就变成一个可以随时触发的跑批任务而不是每次都要人肉盯一天。7.4 最后分享一个小技巧如果你担心备份文件在传输过程中被改动或者磁盘故障建议加上--checksum参数让 mydumper 在备份时同时生成校验文件。恢复完成后在备份目录里执行校验脚本可以快速确认目录内文件是否完整。这个习惯帮我避免过至少两次因为 NFS 传输丢文件而导致的导入失败。用 MyDumper 重建 MySQL 副本本质上就是用“并行 拆分”的思路把传统单线程的大库迁移改造成可横向扩展的流水线。配合 GTID、metadata 分析和复制配置对几百 GB 甚至 TB 级的实例来说操作体验和维护成本都会有一个明显的提升。