800GB 云数据库迁移:mysqldump 跑了 9 小时,InnoDB Clone 只用了 22 分钟——页级 chunk 并行、redo 归档溢出与 LSN 断点续传的底层拆解
先把一个反直觉的事实摆在桌上
同一份 800GB 的 MySQL 8.0 数据,同一个源库,同一个目标端,带宽都是 10Gbps 内网:
- 方案 A:
mysqldump --single-transaction --set-gtid-purged=OFF,管道进mysql,全流程 9 小时 12 分。 - 方案 B:
CLONE INSTANCE FROM ...,全流程 22 分 40 秒。
很多人第一反应是"物理备份不用走 SQL 解析所以快"。这句话对,但只对了一半,而且是最不重要的那一半。真正的差距在 IO 模式和可断点性上。搞不清这两点,你调一辈子 net_write_timeout 和 max_allowed_packet 也压不下迁移窗口。
逻辑迁移慢在哪:B+ 树的逻辑序 ≠ 物理序
mysqldump 在 RR 隔离级别下开的是一致性快照,读的是逻辑行。它按主键顺序把每一行取出来,序列化成 INSERT 语句或 tab 分隔文本。这里有两个不可回避的代价:
代价一:随机 IO 被打包成了"看起来顺序"
InnoDB 的聚簇索引,PK 顺序在逻辑上是递增的,但在物理页上完全不是。一棵经历过大量 UPDATE、DELETE、页分裂的 B+ 树,PK=1000 的行可能落在 space_id=42 的第 9001 页,PK=1001 落在第 12 页。你按 PK 扫一遍,等价于把整个表空间的页随机访问一遍。
而物理迁移按 (space_id, page_no) 顺序读——这是真正的顺序 IO。在 NVMe 上,顺序读和随机读的差距是 4~8 倍,在机械盘上差 50 倍以上。
代价二:二级索引必须从零重建
逻辑导入时,每一条 INSERT 都要走完整的 B+ 树插入路径,包括:
-- 逻辑导入时每行都要付的成本
1. 聚簇索引定位插入页(可能触发页分裂)
2. 每个二级索引做一次 B+ 树查找 + 插入
3. undo 写入
4. redo 写入(如果没关 doublewrite / 没调 innodb_flush_log_at_trx_commit)
5. change buffer 维护
一个 10 个二级索引的表,逻辑导入的写放大至少是 11 倍。物理迁移是把页面直接落到磁盘,索引结构原样搬过去,写放大约等于 1。
物理页级迁移的核心:页是自描述的、可寻址的、可并行的
InnoDB 表空间本质上就是一个页数组。page_no 是绝对偏移,页头自带 FIL_PAGE_LSN、FIL_PAGE_PREV/NEXT、校验和。这意味着:
- 可寻址:给
(space_id, page_no)就能直接定位,不依赖任何 SQL 语义。 - 可并行:页之间在传输层面无依赖,可以随便切块并行拉取。
- 可断点:断线后只要知道哪些页已落盘、哪些没有,接着传就行,不需要重放事务。
MySQL 8.0.17 引入的 Clone Plugin 就是把这套东西工程化了。它不用 binlog,不用 SQL 层,走的是自己的协议。
Clone 的完整工作流:三个 LSN 决定一切
阶段一:donor 获取快照点并启动 redo 归档
donor 拿到备份锁(BACKUP_ADMIN 权限对应的 MDL),记录当前 LSN 作为 CLONE_START_LSN。从这个 LSN 开始,所有新产生的 redo 会被额外归档到 donor 数据目录下的 #clone 目录。注意,这一步不阻塞普通 DML,但会阻塞 DDL。
阶段二:并行 chunk 传输
表空间被切成固定大小的 chunk,默认 1MB。多线程并行拉取,落到 recipient 的 DATA DIRECTORY。
阶段三:apply redo 到一致点
页全部落地后,recipient 把归档的 redo 应用一遍,把数据推到与 donor 一致的 LSN。然后 restart。
操作命令(关键部分)
-- ============ donor 端 ============
INSTALL PLUGIN clone SONAME 'mysql_clone.so'; -- 8.0.17+ 默认已装
CREATE USER 'cloner'@'10.0.1.%' IDENTIFIED BY 'StrongPass!';
GRANT BACKUP_ADMIN ON *.* TO 'cloner'@'10.0.1.%';
-- 查看 donor 的 redo 容量,这是踩坑重灾区
SHOW VARIABLES LIKE 'innodb_redo_log_capacity'; -- 8.0.30+
SHOW VARIABLES LIKE 'innodb_log_file_size'; -- 8.0.29 及以前
-- ============ recipient 端 ============
INSTALL PLUGIN clone SONAME 'mysql_clone.so';
SET GLOBAL clone_valid_donor_list = '10.0.1.10:3306';
CLONE INSTANCE FROM 'cloner'@'10.0.1.10':3306
IDENTIFIED BY 'StrongPass!'
DATA DIRECTORY = '/data/mysql_8032';
调优的第一性原理:chunk 大小 × 并发 / RTT
Clone 的传输是"发一个 chunk,等对方 ACK/落盘,再发下一个"的流式模型。在单条流上,理论吞吐近似:
有效吞吐 ≈ clone_chunk_size × clone_max_concurrency / RTT
代入一组真实数字。跨可用区 RTT = 2ms,默认 chunk 1MB,并发 8:
1MB × 8 / 0.002s = 4000 MB/s ← 理论值,会被带宽和磁盘先卡住
换成跨地域 RTT = 46ms:
1MB × 8 / 0.046s = 174 MB/s ← 这时候 RTT 就是瓶颈,不是带宽
所以在长肥管道上,第一件事是把 chunk 拉大:
SET GLOBAL clone_chunk_size = 16777216; -- 16MB,最大 1GB
SET GLOBAL clone_max_concurrency = 16; -- 默认 0 = 1.5×CPU核数,上限 32
注意 chunk 不是越大越好。chunk 越大,单个 chunk 的传输时间越长,失败重传的粒度就越粗;而且 recipient 需要持有更大的内存缓冲。跨洋场景 8~32MB 是甜点区。
如果迁移的两端是同一区域内的 NVMe 实例,RTT 通常在 0.2~0.5ms,chunk 甚至不需要调——默认 1MB 就能打满带宽。我们在轻云互联同机房的两台 NVMe 数据库实例之间做 clone,RTT 稳定在 0.3ms 上下,16 并发直接跑满 25Gbps 内网,chunk 大小保持默认也没成为瓶颈。反而是在这种低 RTT 场景下,磁盘的 fsync 能力才是真正的天花板。
最容易翻车的地方:redo 归档溢出
这是 Clone 迁移失败率最高的一类原因,而且报错信息很短,容易被忽略:
ERROR 1086 (HY000): Donor's redo log overflow, cannot complete clone operation
原理很直白:donor 在 clone 期间要持续归档从 CLONE_START_LSN 开始的全部 redo,而归档能用的空间是有上限的——就是 redo 容量本身。算一笔账:
数据量 800 GB
全量传输耗时 22 分钟
donor 写入速率 60 MB/s(约 5000 TPS 的 OLTP)
归档 redo 总量 = 60 MB/s × 1320 s ≈ 79 GB
donor redo 容量:
MySQL 8.0.29 默认 innodb_log_file_size=48M × 2 = 96 MB ← 差 800 倍,必炸
MySQL 8.0.30 默认 innodb_redo_log_capacity=100M ← 同样必炸
结论:默认 redo 容量下,clone 只适合几十 GB 以内、且写入量很低的数据。超过这个量级必须提前动手:
# my.cnf,8.0.30+
innodb_redo_log_capacity = 16G
# 8.0.29 及以前
innodb_log_file_size = 4G
innodb_log_files_in_group = 4
调整 redo 容量本身需要重启。这是迁移前必须做的前置检查,不是迁移时再想办法的事。
三条降低 redo 归档压力的实操手段
- 压低 donor 写入速率:迁移窗口内把批量任务、报表导出、定时 job 全部停掉。写入速率从 60MB/s 降到 5MB/s,归档量直接降到 6.6GB。
- 分批迁移:按库或按大表拆成多轮 clone,每轮只搬一部分。注意 clone 是实例级的,拆表要靠
innodb_directories+ 传输表空间(FLUSH TABLES ... FOR EXPORT)另想办法,clone 本身不支持选表。 - 分离 redo 盘:redo 和 datadir 放同一块盘时,归档写入会和页传输抢 IO。redo 单独挂 NVMe 能显著降低 donor 侧的整流压力。
监控:迁移不是黑盒,四张表看穿
-- recipient 端执行,实时进度
SELECT
stage, state,
CAST(estimate AS UNSIGNED) AS estimate_bytes,
CAST(data AS UNSIGNED) AS transferred_bytes,
TIMESTAMPDIFF(SECOND, begin_time, NOW()) AS elapsed_s
FROM performance_schema.clone_progress;
stage 的取值按顺序是:DROP DATA → FILE COPY → PAGE_COPY → REDO_COPY → FILE_SYNC → RESTART。
经验值是:PAGE_COPY 应该占总耗时的 85% 以上。如果 REDO_COPY 占了很久,说明你在迁移期间 donor 还在猛写,归档 redo 太多,apply 阶段吃掉了大量时间——这时候该反思的不是 clone,是你的迁移窗口编排。
-- 查看最终结果和 GTID 位点,这个字段后面会用到
SELECT state, begin_time, end_time,
CAST(data_size AS UNSIGNED)/1024/1024/1024 AS data_gb,
binlog_file, binlog_position, gtid_executed
FROM performance_schema.clone_status\G
断点续传:LSN 才是那个断点
Clone 天然支持中断恢复,因为它记录的是 CLONE_START_LSN 和一个页位图。网络抖动、recipient 重启,只要在 clone_donor_timeout_after_network_failure(默认 5 分钟)内重来,就能从已落盘的页继续,而不是从零开始。
-- 8.0.20+ 支持自动 resume,配置好后不需要人工干预
SHOW VARIABLES LIKE 'clone_donor_timeout_after_network_failure'; -- 默认 300
-- 手工强制重启(换过 donor / 超过超时窗口后)
CLONE INSTANCE FROM 'cloner'@'10.0.1.10':3306
IDENTIFIED BY 'StrongPass!'
DATA DIRECTORY = '/data/mysql_8032'
RESTART;
但要注意:断点续传续的是页,不是 redo 归档。如果 donor 在中途重启过,原来的归档 redo 会失效,整个 clone 必须从头来。所以迁移窗口内别动 donor 的 innodb_redo_log_capacity,也别重启。
最后一公里:clone 完成后你还差一步
Clone 是实例级的物理复制,不走 SQL 层,所以 recipient 上的 gtid_executed 是空的。如果你直接建复制链路,会立刻撞上:
ERROR 1236 (HY000): The slave is connecting using CHANGE MASTER TO
MASTER_AUTO_POSITION = 1, but the master has purged binary logs
containing the GTIDs required for this slave.
正确姿势是从 clone_status.gtid_executed 里取位点,手工补齐:
-- 1. 从 clone_status 拿到 gtid_executed,假设是
-- 3f2a...:1-99887732
STOP REPLICA;
RESET MASTER; -- 清掉 recipient 上 clone 后自己产生的那点 GTID
SET GLOBAL gtid_purged = '3f2a9b1c-6f7d-11ee-9c8a-005056b1f2a3:1-99887732';
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = '10.0.1.10',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl',
SOURCE_PASSWORD = '...',
SOURCE_AUTO_POSITION = 1,
SOURCE_SSL = 1,
SOURCE_CONNECT_RETRY = 10;
START REPLICA;
SHOW REPLICA STATUS\G
补齐 gtid_purged 之后,增量追平通常只需要几分钟。真正切换窗口就是从 START REPLICA 到 Seconds_Behind_Source = 0 的那一小段,而不是全量那 22 分钟。这才是 clone 相比逻辑迁移最大的价值:把停机窗口和全量窗口解耦。
三个写进 checklist 的硬性前置条件
- 只搬 InnoDB:MyISAM、MEMORY、CSV 等非 InnoDB 表不会进 clone。迁移前必须先
SELECT table_schema, table_name, engine FROM information_schema.tables WHERE engine <> 'InnoDB';扫一遍,有的话提前转引擎或单独走逻辑迁移。 - 版本必须 recipient ≥ donor:8.0.17 起支持跨小版本,但方向只能是从低到高。反过来直接拒绝。
- donor 上不能有长事务和 DDL:clone 需要拿到 MDL,一个跑了 40 分钟的
ALTER TABLE会让 clone 卡在FILE COPY阶段干等,直到lock_wait_timeout超时。迁移前先SELECT * FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 5;清场。
一句话总结
逻辑迁移是在"用 SQL 重新描述数据",物理迁移是在"搬运自描述的页"。前者慢在随机 IO 和索引重建,后者慢在 redo 归档容量这个隐藏上限上。把 innodb_redo_log_capacity 提前拉到 16G,把 clone_chunk_size 按 RTT 调大,把 gtid_purged 补齐——800GB 的数据,22 分钟搬完,切换窗口 30 秒。