
半夜两点接到电话说线上一个千万级的 MySQL 实例要加只读从库。数据量接近 900G业务还在持续写入窗口只有四个小时。第一反应是完了是不是又要锁表。很多人在这种场景下会下意识走老路在主库上FLUSH TABLES WITH READ LOCK拿到一个一致性快照然后把数据文件拷到新机器再记录 binlog 位点启动复制。这套流程在十年前的单机小库时代没问题但在今天只要执行全局读锁写入队列就会立刻开始积压。慢查询从偶尔变成常态业务侧能明显感知稍大一点的库一个锁拿下去可能就是几分钟甚至几十分钟的写入中断。真正的问题不是“要不要锁表”而是怎么在不长时间阻断主库写入的前提下得到一个一致性快照并让这个快照和后续的 binlog 事件正确衔接。MySQL 做主从复制从库追的是日志不是文件本身。只要能解决快照和复制位点的衔接问题从库完全可以做到无感加入。这篇文章就围绕这个场景展开MySQL 数据持续写入时怎么在线加从库才能既不停业务也不锁主库。我会把思路、工具选型、关键参数、实操步骤和踩坑点一次写清楚。1. 为什么“加个全局锁再拷数据”越来越不现实1.1 锁表不是备份工具的问题是复制位点的衔接问题先厘清一个概念。很多人以为备份工具是罪魁祸首觉得换个“不锁表”的工具就能解决。其实不是。mysqldump 加--single-transaction在某些场景下确实可以避免长时间锁表但 MySQL 复制真正难的不是“拿到一份数据”而是“拿到一份能和主库 binlog 对得上、且落盘时不产生冲突的数据”。如果只用mysqldump --single-transaction备份一个 InnoDB 大库然后在从库上source恢复整个过程虽然不锁主库但有两个隐患恢复阶段是单线程导入。900G 的逻辑备份导入可能要好几个小时窗口根本不够。备份出来的 SQL 里自带事务和表结构如果原表数据量太大恢复时的 binlog 放大效应非常明显从库同步起点很容易被拉后。所以在生产环境重点不是“用什么工具”而是“怎么设计一套不需要靠锁表来保证一致性的迁移链路”。1.2 单机小库时代可行不等于生产环境可行以前加从库常见做法是锁表、打包数据目录、拷贝到新服务器、改配置、启动复制。因为当时数据量小几十 G 已经算大库锁几分钟业务也能忍。但现在的生产库动辄几百 G甚至上 T业务压力大写入几乎不会停。锁表时间一旦超过阈值连接数会堆积主从延迟会拉大更麻烦的是很多监控系统会误报警。更重要的是数据量大之后物理文件拷贝的耗时不一定比逻辑备份短。拷完之后还要解决版本差异、配置文件路径、ibdata 文件大小、目录权限等问题每一步都可能让“锁表时间”继续往后延。所以现在加从库的主流思路是使用并行逻辑备份工具在可接受的资源开销下拿到一致性快照然后通过 GTID 自动衔接后续 binlog。这样主库全程只承受“读负载”和“binlog 写入”不会被锁阻塞。注意下面提到的命令和参数是基于我实践中比较常用的版本和配置写的。具体落地前请先确认你手上的 MySQL 版本、Mydumper 版本、操作系统和磁盘类型不要照搬所有参数。2. 不锁表加从库真正靠的是 GTID 和一致性快照2.1 GTID 让主从复制从“对坐标”变成“对集合”在 GTID 出现之前MySQL 主从复制靠的是MASTER_LOG_FILE和MASTER_LOG_POS。只要备份快照和 binlog 位点之间有一丁点偏差从库就可能丢数据或重复执行事务。GTID 改变了这个问题的处理方式。每个事务都有一个全局唯一标识主库执行完一个事务这个事务的 GTID 会记录到gtid_executed集合里。从库连接主库时不再需要精确告诉它“从哪个文件的哪个位置开始”只需要说“我这边的 GTID 集合是什么请你把差集发给我”。这意味着只要备份过程中我们能拿到主库当时的gtid_executed集合并把数据导入从库时也把那个集合写进去后续复制就是自动补齐不需要人肉对齐坐标。这也是为什么现在很多生产实践都建议还没开启 GTID 的实例如果准备做在线扩展先把 GTID 打开再考虑加从库。GTID 开启本身对现有复制的影响取决于在线开启方式和版本但比起每次迁移都手工对点位GTID 带来的收益明显更高。2.2 Mydumper 并行备份把迁移从“顺序读”变成“并行读”工具选型上我更建议用 Mydumper而不是 mysqldump。原因有两点Mydumper 支持多线程并行导出默认会按照表拆分成多个任务小表一张一个线程大表可以按行范围拆分。Mydumper 的--kill-long-queries思路不是锁表而是通过检测并跳过长时间运行的查询让备份在业务压力可控的前提下进行。当然它本身也是一个逻辑备份工具也需要通过--single-transaction或类似机制拿到 InnoDB 快照。但因为它会并发读取多个表整体备份时间可以大幅缩短。此外Mydumper 导出的文件是“每个表一个 SQL 文件 元数据文件”的结构恢复时也可以用多线程并行导入这一步对缩短从库构建时间非常关键。如果你用的是 MySQL 8.0还可以考虑官方 MySQL Shell 的 util.dumpInstance 工具它对 InnoDB 和 GTID 的支持也做得比较完善。但 Mydumper 在兼容性和灵活性上有更长的实战历史团队里无论谁接手都更容易理解和维护。2.3 为什么选择事务导入而不是直接 source备份完只是第一步从库恢复时最忌讳的是单线程执行。900G 的逻辑备份如果直接用source可能跑一晚上都不一定能结束。恢复阶段的基本思路是分两条路先把表结构、存储过程、函数、触发器等对象建好。再把大表的 SQL 用多条并行流导入。Mydumper 导出的结构文件和数据文件是分离的正好可以这样操作。恢复时优先恢复所有建表语句再启动多个并行任务导数据。整个过程要确保目标从库的 binlog 关闭或至少不做无谓记录否则导入操作本身会产生大量 binlog浪费时间也影响从库后续追主库日志的性能。不过要注意关闭 binlog 只是恢复阶段的做法。恢复完成后正式启动复制前需要开启 binlog否则从库自身后续也可能承担新的从库角色或者是发生了主从切换后它要变成主库那时候没有 binlog 就会很被动。3. 生产实操一套最小但不缺步的演练流程3.1 操作前必须确认的五件事不要上来就执行备份命令。生产环境操作前先把下面五项确认清楚每项都可能决定你后面是否白忙活。磁盘空间。备份目录要有足够空间。按经验逻辑备份体积大约是原库实际数据量的 50% 到 80%不同引擎和数据类型差异比较大宁可多留一倍余量。恢复端同样要检查因为逻辑导入过程中会存在临时文件、排序文件和 binlog。CPU 和 IO 负载。Mydumper 并行备份对磁盘 IO 和 CPU 有一定压力。如果主库是业务高峰期建议先降低线程数或者用--compress压缩备份文件减少磁盘写入量。权限。备份用户至少需要SELECT、RELOAD、PROCESS、SHOW VIEW、EVENT权限。从库恢复用户需要CREATE、INSERT、DROP、ALTER等权限。用于复制的用户至少需要REPLICATION SLAVE或REPLICATION CLIENT权限。GTID 状态。先执行SHOW VARIABLES LIKE gtid_mode确认是ON。如果是OFF_PERMISSIVE或ON_PERMISSIVE需要先完成在线切换不要在半开状态下做迁移。防火墙和安全组。新从库要能访问主库的 3306 端口。很多线上环境为了安全会限制内网 IP 访问加从库前要先确认白名单和防火墙规则。上面的每一项都不是形式检查。第一个决定备份会不会中途失败第二个决定主库会不会因为备份被拖垮第三个决定你是不是最后还要回主库再刷一遍权限第四个决定你后面能不能直接使用CHANGE MASTER TO ... MASTER_AUTO_POSITION 1第五个决定复制是否能建立成功。3.2 Mydumper 备份命令与参数理解下面是一段常见的主库备份命令具体线程数和压缩选项可以按需调整mydumper \ --userbackup_user \ --passwordYourPassword \ --host主库IP \ --port3306 \ --outputdir/data/backup/mysql-backup \ --threads8 \ --compress \ --triggers \ --events \ --routines \ --single-transaction \ --less-locking \ --kill-long-queries解释几个关键参数--less-locking适合 InnoDB 表在备份过程中尽量减少对 MyISAM 表的长时间锁等待。--kill-long-queries如果检测到长时间运行的查询会尝试杀掉目的是避免FLUSH TABLES WITH READ LOCK等待太久。生产环境要不要用这个参数取决于业务是否允许强制 kill 慢查询如果你不确认不要默认打开。--compress备份文件压缩后体积小传输快但恢复时多一步解压也会增加恢复端 CPU 开销。备份结束后进入输出目录找到metadata文件它里面会记录备份时刻的 binlog 文件、pos 点以及 GTID 集合。这个文件是后续启动复制的核心依据。实际操作时我建议先拿一个小库跑一遍备份和恢复确认工具版本、参数路径、目录权限都正常再动主库。不要在主库上直接试错。3.3 从库恢复并启动 GTID 复制备份文件拷到从库后先导入表结构再导入数据。如果你用了--compress得先解压。Mydumper 对 GNUparallel支持得比较好但如果你不确定也可以先手动解压再用多个备份目录并行导入。导入表结构mysql --userrestore_user --passwordYourPassword --host从库IP --port3306 /data/backup/mysql-backup/schema.sql导入数据for f in /data/backup/mysql-backup/*.sql; do mysql --userrestore_user --passwordYourPassword --host从库IP --port3306 库名 $f done只看上面这段代码你会发现它是顺序执行的。真正做大数据量恢复时我会把这个循环改成 xargs 或 parallel控制并发数量同时把所有日志重定向到文件里方便定位哪张表导入失败。数据导入完成后启动 GTID 复制。这里有两种做法。第一种数据里已经包含了主库备份时的 GTID 集合直接在从库上执行CHANGE MASTER TO MASTER_HOST主库IP, MASTER_USERrepl_user, MASTER_PASSWORDYourPassword, MASTER_PORT3306, MASTER_AUTO_POSITION1; START SLAVE;第二种如果恢复过程因为某种原因导致事务没有完全对应需要手动把备份出来的 GTID 集合写入SET GLOBAL GTID_PURGED 主库metadata文件里的GTID集合;GTID_PURGED设置完成后再执行上面的CHANGE MASTER TO ... MASTER_AUTO_POSITION 1。这里最容易出问题的地方是如果目标从库上已经执行过一些本地事务GTID 集合不为空直接设置GTID_PURGED会报错。所以恢复前要确保从库是个干净的实例或者先手动清掉本地 GTID。注意SET GLOBAL GTID_PURGED不是日常操作。如果从库中已经存在和主库冲突的 GTID先考虑丢弃该实例重新搭建而不是强行往里灌数据。强行处理可能把从库搞成数据不一致的“坏库”。启动复制后马上执行SHOW SLAVE STATUS\G需要关注几个值Slave_IO_Running、Slave_SQL_Running是否为YesSeconds_Behind_Master是否在慢慢变小Last_Errno是否为 0Retrieved_Gtid_Set和Executed_Gtid_Set是否在增长。4. 数据校验不要因为没锁表就跳过这件事4.1 前三分钟的延迟监控最要紧刚启动复制时从库需要从备份位点开始追日志。如果主库写入压力大延迟可能会先冲高再回落。这时候不要慌看的是趋势而不是瞬时值。前 3 分钟重点监控Seconds_Behind_Master是否有明显回落趋势。Slave_SQL_Running是否持续为Yes。Last_SQL_Error是否出现主键冲突或表不存在。主库的 binlog 磁盘占用是否正常。如果延迟持续上升通常是主库写入量太大而备份位点离当前时间太远。这种情况可以等一段时间观察如果延迟超出预期比如超过半小时还没降下来就需要考虑备份时选择的时间点是不是太早或者从库的 IO 能力是不是瓶颈。4.2 用 mysqldump 或 count(*) 做抽样校验很多人觉得有mysqlchecksum插件或者 pt-table-checksum就一定能做全量校验。但要注意生产环境在持续写入时全量校验是很难做到“完全一致”的。因为主库和从库永远存在一个微小的追日志窗口实时校验任何表都可能在时间点上不一致。更稳妥的做法是先确认复制延迟降到接近 0。把校验窗口放在业务低峰期。使用 pt-table-checksum 或 mysqldump 导出一部分核心表做对比关注行数和关键字段的校验值。不要试图在校验工具里加太多条件校验工具本身也会给主库和从库带来负载。第一次校验选几张核心业务表和最大的几张表就够了剩下的可以放到下一个低峰期分批完成。4.3 演练一次主从切换离开前再离搭建从库的最终目的要么是读扩展要么是容灾切换。很多团队在从库搭好之后只看了Slave_IO_Running是 Yes 就走了从没做过切换演练。等到真正需要切换时才发现从库的复制账号权限不对、只读参数没配好、VIP 指向没有改甚至 GTID 集合有问题。所以我建议如果加班费允许尽量在搭建完成后做一个简单的“切换演练”比如手动把从库提升为可写状态跑几条 DML 验证基本功能再切回去或者单独在一套测试环境里模拟。这样做之后你会对方案更有把握而不是只靠SHOW SLAVE STATUS的结果来安慰自己。5. 常见问题与排查链路5.1 报错排查顺序IO、权限、版本、位点如果从库复制报错按下面顺序排查先看Last_IO_Error。如果是连接主库失败检查主库防火墙、端口、账号权限。再看Last_SQL_Error。如果是 SQL 执行失败后台打印SHOW SLAVE STATUS\G的完整错误信息不要只看最后一行。确认主从版本是否一致或兼容。比如 MySQL 5.7 的二进制日志在还原到 8.0 时可能因为默认字符集、sql_mode 或 binlog 格式兼容问题失败。确认备份时点之后主库是否执行过 DDL。逻辑备份还原到从库后主库又执行了ALTER TABLE从库按旧表结构继续追日志时也容易报错。一个比较隐蔽的问题从库导入数据后gtid_executed里包含了备份阶段的所有事务但如果你在从库执行过任何手动 SQL哪怕是改了权限表GTID 集合也可能出现额外项。此时即使MASTER_AUTO_POSITION1能连上主库也可能因为事务集合存在不属于主库的 GTID 导致复制无法继续。遇到这种情况最靠谱的方案是重建从库不要在不干净的实例上反复重试。5.2 Mydumper 导入数据时的 binlog 放大效应如果你在从库恢复数据时目标从库的 binlog 是开启的那么导入数据的每个操作都会记录 binlog数据本身加上 binlog 写入磁盘 IO 会翻倍。建议做法是恢复数据前临时关闭从库的 binlog 或者使用sql_log_bin0。恢复完成后再启用 binlog。但有一点要提醒如果你的主从复制链路本身要求从库继续级联复制那么导入阶段关闭 binlog 不会影响后续CHANGE MASTER建立的复制因为它通过 GTID 从主库拉取日志而不是依赖从库自身的 binlog。这个操作只影响从库作为更下游主库时的复制能力不影响当前链路。5.3 备份完再开 GTID 没有意义如果你为了这次迁移临时在主库开启 GTID开始备份前就要确认已经开启完成。如果备份都跑完了再去打开 GTID那么备份文件里没有 GTID 信息之后还是要手工对位点。MySQL 在线开启 GTID 可以通过修改gtid_mode从 OFF 切到 OFF_PERMISSIVE再切到 ON_PERMISSIVE最后切到 ON同时设置enforce_gtid_consistencyON。这个过程要监控有无新事务产生异常也需要一些时间。如果应急时不允许在线开关也可以考虑用老式位点方式搭建从库只是后续维护成本更高。6. 这套方案的适用边界与更底层的工作流6.1 哪些场景值得用哪些场景不该硬套这套方案非常适用于以下场景主库数据量大业务持续写入窗口紧。团队已经使用 GTID复制管理经验较丰富。需要从 0 搭建一个用于读扩展或容灾的从库。主库硬件支持多线程备份IO 和 CPU 有一定余量。不适合硬套的场景也要说清楚数据量很小只有十几 G用 mysqldump 可能更快没必要上并行工具。主库磁盘 IO 已经很高再跑并行备份可能拖垮业务此时要考虑从备份机或延迟备库导数据。从库机器性能太弱恢复阶段并行导入也可能变成瓶颈。如果只是临时拉一个数据快照做分析不需要建立持续复制那用 mysqldump 更省事。方案没有绝对优劣关键是先判断场景再选工具。6.2 真正沉淀下来的是“可验证的迁移流程”加从库这件事一次跑通不难难的是每次都稳定跑通。回头总结我建议团队里把下面几个步骤固化成标准操作流程预检检查版本、空间、权限、GTID 状态、业务写入负载。备份Mydumper 并行导出控制线程数保留元数据。传输与恢复先传文件再恢复结构再并行导入数据。启动复制确认 GTID 集合执行CHANGE MASTER观察复制状态。校验与演练延迟归零后做数据抽样校验完成一次切换演练。这个流程的本质是把“对复制位点的人肉记忆”转成“对 GTID 集合和一致性快照的自动衔接”。主库不再需要牺牲写入来换取一致性从库的搭建也从“夜间战战兢兢的操作”变成了“白天也可以做的常规变更”。下次再接到加从库的工单可以先把上面这套流程在测试环境走一遍。只要预检、备份、恢复、复制、校验五步都能在测试环境里稳定复现生产环境大概率也能在窗口内完成。真正的从容从来不是靠某个工具锁不锁表而是靠一套你自己验证过、记录过、知道失败点在哪里的完整流程。