MySQL 逻辑迁移:从 mysqldump 到 MySQL Shell Dump/Load
迁移窗口已经过半,新库的表数量和行数都对得上,应用冒烟也能登录。真正切流后,客服却发现少数中文客户名变成问号,一条由存储过程完成的结算链路直接报错,准备建立复制关系时又遇到 GTID_PURGED can only be set when GTID_EXECUTED is empty。团队手里明明有一份成功退出的 .sql,却回答不了三个问题:这份文件来自同一个事务时间点吗,函数和事件有没有进去,导入时到底改过目标实例的哪些全局状态?
这类事故说明,逻辑迁移交付的不是“能执行的 SQL 文件”,而是一组可证明的对象、数据与运行语义。源端读到了什么快照,目标端按什么顺序创建对象,字符如何从连接编码变成列编码,GTID 是否代表这批数据,任何一步含糊都会把故障推迟到切流后。mysqldump 依然是小中型迁移、可审阅 SQL 和精细对象选择的好工具;数据量、恢复窗口或并行度开始成为硬约束时,MySQL Shell Dump/Load 通常更合适。
先把迁移现场拆成五份证据
逻辑迁移至少有五类状态。第一类是 schema,包括库默认字符集、表、索引、视图、触发器、存储过程、函数和事件;第二类是行数据;第三类是账号、角色与授权;第四类是复制起点,例如 GTID 集或 binlog 文件位置;第五类是目标环境自己的参数、插件、时区、排序规则和容量。只检查 COUNT(*),最多证明第二类状态的一小部分相似,不能证明其余四类已经交接。
迁移链可以画成下面这样。虚线不是可有可无的文档工作,它们决定失败后能否定位到快照、文件、加载批次或切流动作。
逻辑导出不是持续增量同步。导出开始后源库仍发生的提交,不会神奇地进入已经读过的表。停机迁移需要写冻结覆盖导出到切流的窗口;在线迁移则要在快照水位之后接复制或 CDC,并对最终追平与回切另行设计。仓库中的备份与恢复讲的是长期备份、介质与恢复闭环;这里把逻辑工具用作跨实例和跨版本的数据交接,不拿一次迁移替代备份与 PITR。
建立工具与版本基线
示例以 MySQL Server 8.4 LTS 与同代或更新的客户端为操作基线,MySQL Shell 使用当前 GA 的 9.7 发布线。8.4 是长期支持服务器线,9.7 Shell 是更新更快的客户端工具线,两者不是同一种支持承诺;官方说明 9.7 Shell 可用于 GA MySQL 8.0 及以后版本。实际执行时先记录客户端和两端服务端版本,不要默认系统 PATH 中的 mysqldump 就属于目标版本。MySQL 官方的 mysqldump 手册明确把它定义为生成可重建对象与数据 SQL 的逻辑备份程序,也明确提醒它不是大数据量下快速、可扩展的恢复方案;MySQL Shell 的 Dump utilities 手册则提供并行、压缩、分块、进度、校验元数据和兼容检查。
MySQL 软件采用开源 GPL 与 Oracle 商业许可双重许可模式,Community 与 Commercial 包的功能、支持和第三方组件清单也可能不同。工具镜像进入企业制品库前,应保存下载来源、版本、包摘要和对应的 MySQL 许可入口,不要把“可免费下载”推导成任意再分发或商业嵌入都没有义务;托管服务中的可用插件、动态权限和 Shell 能力仍以服务商产品边界为准。
mysql --version
mysqldump --version
mysqlsh --version
mysql --defaults-extra-file=./source-client.cnf \
--execute="SELECT VERSION(), @@version_comment, @@character_set_server, @@collation_server, @@gtid_mode, @@log_bin"
mysql --defaults-extra-file=./target-client.cnf \
--execute="SELECT VERSION(), @@version_comment, @@character_set_server, @@collation_server, @@gtid_mode, @@log_bin"期望看到的是明确的客户端版本、源端和目标端版本与字符集,而不是某个固定数值。客户端比服务端老时,可能不认识新语法或新元数据;目标 major 较高时,还要执行官方 Upgrade Checker 或在隔离目标完整恢复,检查被移除的语法、保留字、认证插件与 SQL mode。MySQL Shell dump 只支持 GA Server,源端完整支持从 5.7 起的版本,目标端至少为 5.7;具体组合仍应由当前 requirements and restrictions判定。
尚未安装时,从 MySQL Community Downloads 获取包含 mysql、mysqldump 的客户端,从 Installing MySQL Shell 选择 Windows、Linux 或 macOS 的官方步骤。已配置 MySQL APT 仓库的 Debian/Ubuntu 可以执行 sudo apt-get update && sudo apt-get install mysql-shell;RPM 系使用官方 Yum 仓库后执行 sudo dnf install mysql-shell。安装后必须运行上面的三个 --version,确认 PATH 命中的确是计划版本。容器化工具箱也可以,但要固定镜像 digest、只读挂载凭证、把导出目录放在加密磁盘,并记录镜像中的客户端版本。
用合成库跑通 mysqldump 正向实验
下面的数据库故意同时包含中文、utf8mb4、视图、触发器、过程和事件。它能验证对象是否齐全,也能暴露连接编码和 DEFINER 问题。命令只针对本机实验实例;生产源端先盘点对象和权限,再申请变更窗口。
CREATE DATABASE migration_lab
CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE migration_lab;
CREATE TABLE customer (
id BIGINT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
balance DECIMAL(18,2) NOT NULL,
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
ON UPDATE CURRENT_TIMESTAMP(6)
) ENGINE=InnoDB;
CREATE TABLE audit_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT NOT NULL,
action_name VARCHAR(32) NOT NULL
) ENGINE=InnoDB;
DELIMITER //
CREATE TRIGGER customer_ai AFTER INSERT ON customer FOR EACH ROW
BEGIN
INSERT INTO audit_log(customer_id, action_name) VALUES (NEW.id, 'INSERT');
END//
CREATE PROCEDURE credit_customer(IN p_id BIGINT, IN p_amount DECIMAL(18,2))
BEGIN
UPDATE customer SET balance = balance + p_amount WHERE id = p_id;
END//
DELIMITER ;
CREATE VIEW positive_customer AS
SELECT id, name, balance FROM customer WHERE balance > 0;
CREATE EVENT purge_audit ON SCHEDULE EVERY 1 DAY
DO DELETE FROM audit_log WHERE id < 0;
INSERT INTO customer(id, name, balance) VALUES
(1, '示例用户甲', 100.00), (2, 'Emoji🙂', 0.00);迁移账号不应使用 root。按照官方手册,读表至少需要 SELECT,视图需要 SHOW VIEW,触发器需要 TRIGGER;不用 --single-transaction 时通常还需要 LOCK TABLES。导出 routines 与 events 还需相应对象可见性,获取复制位点需要 REPLICATION CLIENT。是否需要 PROCESS、RELOAD 或 FLUSH_TABLES 与 tablespace、GTID 选项和服务端状态相关,必须用最终命令做权限评审,不能抄一个无限授权账号。
CREATE USER 'migration_reader'@'localhost' IDENTIFIED BY '<SOURCE_PASSWORD>';
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, REPLICATION CLIENT
ON *.* TO 'migration_reader'@'localhost';
GRANT EXECUTE ON migration_lab.* TO 'migration_reader'@'localhost';
CREATE USER 'migration_loader'@'localhost' IDENTIFIED BY '<TARGET_PASSWORD>';
GRANT CREATE, ALTER, DROP, INDEX, INSERT, UPDATE, DELETE, SELECT,
CREATE VIEW, SHOW VIEW, TRIGGER, EVENT, EXECUTE, CREATE ROUTINE,
ALTER ROUTINE
ON migration_lab.* TO 'migration_loader'@'localhost';凭证通过受控 option file、mysql_config_editor 生成的 login path 或 MySQL Shell credential store 交给进程,不把密码写进仓库、工单和进程参数。下面用两份 --defaults-extra-file 表达源端与目标端入口;该选项必须紧跟程序名,文件放在仓库外且只允许执行账号读取。也可以分别建立 source-migration、target-migration login path,再把命令中的 --defaults-extra-file=... 换成 --login-path=...。
[client]
host=localhost
port=3306
user=migration_reader
password=<SOURCE_PASSWORD>
ssl-mode=REQUIRED
default-character-set=utf8mb4# target-client.cnf
[client]
host=localhost
port=3307
user=migration_loader
password=<TARGET_PASSWORD>
ssl-mode=REQUIRED
default-character-set=utf8mb4mysqldump --defaults-extra-file=./source-client.cnf \
--single-transaction --quick --hex-blob \
--routines --events --triggers \
--set-gtid-purged=OFF --no-tablespaces \
--databases migration_lab \
--result-file=./migration_lab.sql
sha256sum ./migration_lab.sql > ./migration_lab.sql.sha256
grep -E "CREATE (TABLE|VIEW|TRIGGER|PROCEDURE|EVENT)" ./migration_lab.sql--single-transaction 在导出开始时建立一致性事务快照,适合 InnoDB;它不让 MyISAM 等非事务表获得同样保证,而且导出期间的 ALTER TABLE、DROP TABLE、RENAME TABLE、TRUNCATE TABLE 可能让结果不一致或失败。--quick 让客户端逐行读取,避免把大表结果整体缓存在客户端内存;它默认随 --opt 启用,但显式写出更利于评审。--hex-blob 避免二进制列经过文本转义产生歧义。--routines --events 不是默认“肯定都有”,而触发器虽默认包含,也应显式声明意图。
--result-file 对 Windows 尤其重要:官方手册警告 PowerShell 的 > 重定向可能写出 UTF-16,而 MySQL 不能把 UTF-16 当连接字符集重新加载。--set-gtid-purged=OFF 适合这次“只迁数据、不建立 GTID 拓扑”的合成实验;生产若要把快照作为复制基线,不能照抄 OFF,必须先设计 GTID 所有权。
目标端先创建空环境,再导入并让客户端保持默认的遇错停止。mysql 只有显式加入 --force(-f)才会在 SQL 错误后继续;迁移脚本不应使用它,同时仍要捕获退出码和 stderr,导入完成后按对象与业务不变量复核。
mysql --defaults-extra-file=./target-client.cnf \
--default-character-set=utf8mb4 < ./migration_lab.sql \
2> ./migration_lab.import.stderr
mysql --defaults-extra-file=./target-client.cnf \
--database=migration_lab --batch --raw --execute="
SELECT COUNT(*) AS customer_count,
SUM(balance) AS balance_sum,
HEX(MAX(CASE WHEN id=1 THEN name END)) AS name_hex
FROM customer;
SHOW TRIGGERS;
SHOW PROCEDURE STATUS WHERE Db='migration_lab';
SHOW EVENTS FROM migration_lab;
CHECK TABLE customer, audit_log;"这组正向实验的验收证据是 customer_count=2、balance_sum=100.00,中文名称的十六进制在两端一致,且 trigger、procedure、event 各自可见;任何 ERROR、warning 或对象缺失都应让迁移保持未完成。
再用一条必然失败的语句验证客户端控制流。第一条命令保持默认行为,遇到不存在的表后停止,不应输出 after_error;第二条显式加入 --force,才会继续执行最后一个 SELECT。生产导入禁止使用第二种写法。
mysql --defaults-extra-file=./target-client.cnf --batch --raw \
--execute="SELECT 'before_error'; SELECT * FROM migration_lab.no_such_table; SELECT 'after_error';" \
> ./default-stop.stdout 2> ./default-stop.stderr
mysql --defaults-extra-file=./target-client.cnf --force --batch --raw \
--execute="SELECT 'before_error'; SELECT * FROM migration_lab.no_such_table; SELECT 'after_error';" \
> ./force-continue.stdout 2> ./force-continue.stderr
grep -q "after_error" ./default-stop.stdout && exit 1 || true
grep -q "after_error" ./force-continue.stdout反向实验:制造不一致与字符损坏
第一组反例用非事务表证明“加了 --single-transaction”不等于整个库一致。把审计表改为 MyISAM,在导出期间持续写入;customer 处在 InnoDB 快照里,audit_log 却按被读取时的即时状态导出。跨表不变量“每个 customer 插入对应一条初始审计”就可能失效。
ALTER TABLE migration_lab.audit_log ENGINE=MyISAM;
-- 在另一个会话中,于导出窗口持续执行
INSERT INTO migration_lab.audit_log(customer_id, action_name)
VALUES (1, 'CONCURRENT_WRITE');
SELECT ENGINE
FROM information_schema.tables
WHERE table_schema='migration_lab';稳定的故障证据不是等待某次恰好出现错误,而是导出前先查询所有 storage engine。一旦一致性集合中存在非 InnoDB 表,就选择短时锁表/停写、先转换存储引擎,或承认需要应用级对账;不能继续把文件标为事务一致。大型库也要在变更冻结期间禁止相关 DDL,因为事务快照保护行版本,不保护所有对象定义变化。
第二组反例故意用错误字符集加载一份包含中文的 SQL:
mysql --defaults-extra-file=./target-client.cnf \
--default-character-set=latin1 \
< ./migration_lab.sql 2> ./wrong-charset.stderr
mysql --defaults-extra-file=./target-client.cnf \
--database=migration_lab \
--execute="SELECT id, name, HEX(name) FROM customer ORDER BY id"可能出现 Incorrect string value,也可能在宽松配置或错误转码链中得到能显示却字节不同的数据。对比 HEX(name) 比肉眼看终端可靠,因为终端字体和会话编码会制造假象。正确修复不是对损坏目标继续“转码”,而是丢弃隔离目标、确认 dump 内 SET NAMES、连接字符集、库表列字符集与目标 collation,再从未损坏制品重载。
字符字节一致也不代表排序语义一致。字符集决定字符与字节的映射,collation 决定相等、大小写、重音、排序和唯一索引冲突。源库与目标库应同时保存 server、database、table 和 column 四层设置,因为库默认值只影响之后新建且未显式指定的对象,不会自动改写已有列。官方 Unicode 字符集说明指出,utf8 在 8.4 中仍是已弃用的 utf8mb3 别名,只能保存三字节 UTF-8;新对象应显式使用 utf8mb4,迁移窗口不能顺手把旧列改字符集而不评估索引长度、排序和唯一性。
SELECT @@character_set_server, @@collation_server,
@@character_set_connection, @@collation_connection;
SELECT schema_name, default_character_set_name, default_collation_name
FROM information_schema.schemata
WHERE schema_name='migration_lab';
SELECT table_name, column_name, character_set_name, collation_name
FROM information_schema.columns
WHERE table_schema='migration_lab' AND character_set_name IS NOT NULL
ORDER BY table_name, ordinal_position;
SELECT name, HEX(name),
name='A' AS equals_a,
WEIGHT_STRING(name) AS sort_weight
FROM migration_lab.customer
ORDER BY name;
INSERT INTO migration_lab.customer(id, name, balance) VALUES
(101, 'A', 0), (102, 'a', 0), (103, 'e', 0), (104, 'é', 0);
SELECT name COLLATE utf8mb4_0900_ai_ci AS comparison_key, COUNT(*)
FROM migration_lab.customer
WHERE id BETWEEN 101 AND 104
GROUP BY comparison_key ORDER BY comparison_key;
SELECT name COLLATE utf8mb4_0900_as_cs AS comparison_key, COUNT(*)
FROM migration_lab.customer
WHERE id BETWEEN 101 AND 104
GROUP BY comparison_key ORDER BY comparison_key;
DELETE FROM migration_lab.customer WHERE id BETWEEN 101 AND 104;
DELETE FROM migration_lab.audit_log WHERE customer_id BETWEEN 101 AND 104;隔离目标中的四行探针不改变列定义:ai_ci 会忽略重音和大小写,分组数应少于区分重音与大小写的 as_cs;最后两条 DELETE 同时清理客户行和触发器生成的审计行。再加入尾随空格和业务真实语言字符,重复权重查询并在副本尝试候选 collation 下的唯一索引。预期证据有两类:HEX(name) 保持一致,证明字节未变;equals_a、sort_weight、分组、排序或唯一索引创建结果可能变化,证明比较语义改变。若候选规则产生冲突,先输出冲突键和业务处置决定,再转换;不能用删除重复行让 DDL 通过。官方已明确弃用 utf8mb3,并建议使用 utf8mb4,因此发现旧编码应进入独立 schema 变更计划,而不是在导入命令中隐式修复。
GTID 不是装饰性元数据
--set-gtid-purged=AUTO 是 mysqldump 默认行为。源端启用 GTID 且 gtid_executed 非空时,dump 会包含设置 GTID_PURGED 的语句,并通常包含关闭当前会话 binary logging 的语句。重要陷阱是:写入 dump 的 GTID 集来自整个源实例的 gtid_executed,即使只导出了一个 schema,也可能包含其他 schema 的事务。把这个集合灌进目标后,拓扑比较会认为那些事务已经执行,但对应数据根本没有迁入。
grep -nE "GTID_PURGED|SQL_LOG_BIN|CHANGE REPLICATION" ./migration_lab.sql
mysql --defaults-extra-file=./source-client.cnf --batch --execute="
SELECT @@GLOBAL.gtid_executed, @@GLOBAL.gtid_purged;
SHOW BINARY LOG STATUS;"
mysql --defaults-extra-file=./target-client.cnf --batch --execute="
SELECT @@GLOBAL.gtid_executed, @@GLOBAL.gtid_purged;"若目标已执行事务,再导入含重叠或不合法集合的 GTID_PURGED,常见失败就是目标集合不为空或集合约束冲突。删除 SQL 中那一行也不是通用修复:如果目标将接成副本,缺失 GTID 会导致重复应用或无法正确跳过事务。决策必须先回答目标是独立新库、复制种子还是现有拓扑成员;只有拓扑 owner 才能批准 OFF、追加、替换或手工设置。GTID 变更和 sql_log_bin=0 需要高权限,应从普通数据加载账号拆出成短时、双人审批的拓扑动作。
在线迁移还必须把全量快照和增量起点绑定。单独在 dump 前后各执行一次 SHOW BINARY LOG STATUS 只得到两个观察值,不证明哪一个与事务快照完全相同。对 file/position 复制,使用 --single-transaction --source-data=2:官方 mysqldump 复制选项说明该组合会在建立快照的短暂全局读锁窗口取得坐标,并把 CHANGE REPLICATION SOURCE TO 作为注释写入制品。该选项要求 binary log、RELOAD 以及读取状态所需权限;值 2 只记录证据,不会在导入时擅自改复制配置。
mysqldump --defaults-extra-file=./source-client.cnf \
--single-transaction --source-data=2 --quick --hex-blob \
--routines --events --triggers --no-tablespaces \
--set-gtid-purged=OFF --databases migration_lab \
--result-file=./migration_lab.seed.sql
grep -nE "CHANGE REPLICATION SOURCE TO|GTID_PURGED|SQL_LOG_BIN" \
./migration_lab.seed.sqlGTID 自动定位不消费 file/position,但仍要保存同一批 dump metadata 中的 gtid_executed,并证明增量通道从该集合之后继续。部分 schema dump 的 GTID 集仍是整个实例集合,不能声称它精确代表该 schema。切换前先冻结写,记录最终源端 GTID 集 G_cut,等待目标 WAIT_FOR_EXECUTED_GTID_SET(G_cut, timeout) 返回 0,再比较 GTID_SUBTRACT(G_cut, @@GLOBAL.gtid_executed) 为空;超时返回 1、错误返回 NULL,都不能切流。还要同时检查复制 SQL/IO 线程、最后错误和业务对账,因为“集合已执行”不证明被过滤对象、DDL 或外部副作用正确。
-- 在源端冻结写后保存结果为 G_cut;不要把占位符原样执行。
SELECT @@GLOBAL.gtid_executed AS G_cut;
-- 在目标复制追平阶段执行。
SELECT WAIT_FOR_EXECUTED_GTID_SET('<G_cut>', 60) AS caught_up;
SELECT GTID_SUBTRACT('<G_cut>', @@GLOBAL.gtid_executed) AS still_missing;若使用非 GTID 坐标,则证据必须是同一 seed dump 注释中的 file/position、目标 SHOW REPLICA STATUS 的已执行坐标和冻结后的最终坐标三者闭环。binlog 被提前 purge、源实例 UUID 不符、过滤规则变化或目标出现 errant GTID 时,应保留源端与目标端状态并重建通道或重新取种子;不得通过重置全部 binary log 与 GTID 历史来“清空错误”,那会删除拓扑证据并可能让增量不可恢复。
大库为什么转向 MySQL Shell Dump/Load
mysqldump 的优势是一个可读、可审阅、容易筛选或修改的 SQL 流;代价是导出和恢复主路径近似单流,恢复需要解析并执行大量 SQL,数据与二级索引构建相互争抢。MySQL Shell 把 DDL、表元数据和 TSV 数据拆成文件,默认压缩并按表或分区切 chunk。dump 与 load 都能使用多个连接并行处理,进度文件还能让中断加载识别已经完成的工作。
它的正向入口先做 dry run,再执行带 checksum 的 dump。每个 threads 都是源库连接,maxRate 是每线程限速而非全局限速;bytesPerChunk 是压缩前近似尺寸,不是严格文件上限。并行数增加会同时增加源端扫描、buffer pool churn、网络出口和目标连接压力。
mysqlsh --defaults-extra-file=./source-client.cnf --js \
--execute='util.dumpSchemas(["migration_lab"], "./dump-migration-lab", {
dryRun: true,
consistent: true,
threads: 4,
chunking: true,
bytesPerChunk: "64M",
maxRate: "50M",
checksum: true,
compression: "zstd;level=1",
events: true,
routines: true,
triggers: true
})'官方说明 consistent:true 默认启用。权限最小集中的锁权限有两种合法组合:优先给账号 RELOAD,让工具短暂执行 FLUSH TABLES WITH READ LOCK;若不能授予 RELOAD,则对所有被导出表授予 LOCK TABLES,工具会逐表加锁来协调各线程。两者不是都要授予,也不能两者都没有。线程随后以 REPEATABLE READ 和 START TRANSACTION WITH CONSISTENT SNAPSHOT 建立快照,再尝试 LOCK INSTANCE FOR BACKUP;BACKUP_ADMIN 缺失时工具执行额外一致性检查,instance dump 检查失败会停止,schema/table dump 则继续但报告一致性错误。因此 schema dump 的退出状态和日志必须一并设为门禁,不能把带一致性错误的目录接入加载。把 consistent:false 当作绕过权限错误,会直接放弃一致性保证。MySQL Shell 仍只对 InnoDB 表保证数据一致性。
对本实验的 schema dump,选择其中一条授权路径即可:
-- 路径 A:短暂全局读锁
GRANT RELOAD ON *.* TO 'migration_reader'@'localhost';
-- 路径 B:不能给 RELOAD 时,覆盖本次导出的每张表
GRANT LOCK TABLES ON migration_lab.* TO 'migration_reader'@'localhost';无论选择哪条,仍需前文的 EVENT、SELECT、SHOW VIEW、TRIGGER;需要在 metadata 中记录非 GTID binlog 位点时再加 REPLICATION CLIENT。BACKUP_ADMIN 能让对象一致性保护更直接,但不应为了省掉评估而给迁移账号叠加无关全局权限。
执行时目标目录必须为空,完成标志、顶层 metadata 和 checksum 文件都应进入制品清单。文件个数增长是并行能力的来源,也是对象存储请求、inode、扫描和加密成本的来源。
mysqlsh --defaults-extra-file=./source-client.cnf --js \
--execute='util.dumpSchemas(["migration_lab"], "./dump-migration-lab", {
consistent: true, threads: 4, chunking: true,
bytesPerChunk: "64M", maxRate: "50M",
checksum: true, compression: "zstd;level=1",
events: true, routines: true, triggers: true
})'
find ./dump-migration-lab -maxdepth 1 -type f -print
sha256sum ./dump-migration-lab/@.json ./dump-migration-lab/@.done.json加载前先 dry run 检查对象冲突和配置,再正式加载。threads 每个占一个目标连接;deferTableIndexes:"all" 先装主键与数据、后建二级索引,通常缩短数据装载路径,却会把 CPU、临时空间和 redo 峰值推迟到索引阶段。analyzeTables:"on" 刷新统计信息有利于切流后计划稳定,但也增加窗口;大库可以把它列为单独阶段并保留完成证据。checksum:true 会使用 dump 生成的 checksum 核验已加载数据,但不会覆盖工具后来生成的不可见主键等派生数据。
mysqlsh --defaults-extra-file=./target-client.cnf --js \
--execute='util.loadDump("./dump-migration-lab", {
dryRun: true, threads: 4, showProgress: true,
deferTableIndexes: "all", analyzeTables: "on",
checksum: true, updateGtidSet: "off"
})'
mysqlsh --defaults-extra-file=./target-client.cnf --js \
--execute='util.loadDump("./dump-migration-lab", {
threads: 4, showProgress: true,
deferTableIndexes: "all", analyzeTables: "on",
checksum: true, updateGtidSet: "off",
progressFile: "./load-progress.json"
})'progressFile 是恢复状态,不是普通日志。重复加载时应复用与本批 dump、目标实例对应的状态文件;删除它再启动会改变幂等判断,ignoreExistingObjects 也可能把真实漂移伪装成可忽略重复。resetProgress:true 也不会替你删除已创建对象或去重,只有先确认并清空本批已加载的 schema、table、user、view、trigger、routine 和 event 后才能从头开始。Shell metadata 会记录源端 gtid_executed,有 REPLICATION CLIENT 时还可记录 binlog 位置,但 load 默认不会自动应用 GTID。若明确选择 updateGtidSet:append|replace,普通 MySQL 目标还要按官方要求结合 skipBinlog:true,且不能在目标 Group Replication 正运行时使用该选项。
从配置字段反推资源与故障
threads 不是“越大越快”。源端每个 dump 线程持有连接和一致性事务,长事务会阻止旧版本及时清理;目标端每个 load 线程消耗连接、解析、redo、buffer pool 与磁盘队列。合理值来自逐级压测:固定制品与目标规格,从低并行开始记录吞吐、连接数、CPU、磁盘延迟、redo 生成和复制延迟,增加并行直到边际收益消失或任一保护线接近。
bytesPerChunk 太大,中断后重做单位大、单文件传输尾延迟高;太小,文件与对象存储请求暴涨,元数据和调度开销吞掉收益。没有主键或唯一索引时,chunk 边界依赖估算,数据分布倾斜会产生长尾。迁移前为无主键表补主键可能改变业务写路径,应走 schema 变更治理,不能由 dump 参数偷偷代替。
compression 提高等级会用 CPU 换网络和磁盘;源端 CPU 紧张而专线充足时低等级更稳,网络昂贵或受限时才值得提高。maxRate 按线程生效,所以总读取上限近似为单线程上限乘活跃线程数。tzUtc:true 会标准化 TIMESTAMP 数据迁移路径,但 DATETIME 不具备相同时区语义;校验必须按类型区分,不能拿应用显示值代替字节和 UTC 时刻核对。
恢复错误也要按阶段分类。DDL 阶段的 Access denied 指向权限或 DEFINER;数据阶段的 Duplicate entry 指向目标非空、进度错配或源数据约束;Packet too large 与 BLOB Base64 膨胀、目标 max_allowed_packet 有关;索引阶段卡顿常见于临时空间、redo 或 I/O;checksum mismatch 则必须保留对应 chunk、表和两端查询证据,禁止直接关闭 checksum 继续切流。
对象、DEFINER 与账号不能混成一团
mysqldump 的 --all-databases 不是“实例克隆”承诺,系统 schema、账号、插件和运行参数有各自规则。MySQL Shell dumpInstance() 可以选择用户、角色和授权,但目标托管服务可能限制 SUPER、FILE、tablespace、DEFINER 或通配符 grant。迁移应用 schema 时,常见策略是由身份平台或受控 IaC 在目标预建角色,再让对象归属稳定的服务角色;不要为了让视图和 procedure 导入成功就把源端个人账号永久复制到目标。
导出前生成对象清单,导入后逐类比较定义。下面查询不会给出全部 DDL,却能快速发现“数量相等但对象类型缺失”。
SELECT 'TABLE' AS kind, COUNT(*) AS n
FROM information_schema.tables WHERE table_schema='migration_lab'
UNION ALL
SELECT 'ROUTINE', COUNT(*) FROM information_schema.routines
WHERE routine_schema='migration_lab'
UNION ALL
SELECT 'TRIGGER', COUNT(*) FROM information_schema.triggers
WHERE trigger_schema='migration_lab'
UNION ALL
SELECT 'EVENT', COUNT(*) FROM information_schema.events
WHERE event_schema='migration_lab';
SELECT table_name, engine, table_collation
FROM information_schema.tables
WHERE table_schema='migration_lab'
ORDER BY table_name;真正比对定义时使用 SHOW CREATE TABLE/VIEW/PROCEDURE/EVENT 的规范化结果,并单独审计 DEFINER 与 SQL SECURITY。把所有 DEFINER 粗暴改成加载账号,会让权限随个人生命周期漂移;保留不存在的源账号又会造成对象执行失败。安全团队应给“对象定义者”和“调用者”建立稳定角色,最小化动态权限,并在切流前以应用账号真实调用视图、触发器和过程。
项目接入、校验与切流
应用接入从配置复制开始,但不能把生产密码写进新环境配置文件。先创建目标 secret 引用和最小权限应用账号,部署只读或影子流量实例,用数据库名、server UUID 与只读探针防止误连源库。连接池总上限要给迁移线程、DDL 和人工排障留预算;若迁移期间新旧应用同时运行,连接预算按两套副本相加。
数据校验从总量逐级下钻:表级行数与空值分布用于发现大差异;按稳定主键分桶的 COUNT/SUM/MIN/MAX 用于定位区间;关键字符串比较 HEX;金额、状态机和父子关系使用业务不变量;BLOB 计算分块摘要;最后以目标应用的真实查询与写入验证索引、时区和权限。抽样只能增加信心,不能替代确定性不变量。
停机切换的可靠顺序是:宣布写冻结,确认源端业务写入降为零,完成最后导出或增量追平,运行最终差异校验,把目标设为唯一写入方,灰度切换连接并观察错误、延迟和业务指标。在线切换则要明确快照后的增量工具、最终 stop position 和重复事件处理;不能仅凭“延迟为零”推断无漏数。
回切窗口内不要允许新旧两边独立写。如果目标已接受新写入,回切前必须证明这些写入能反向同步并对账,或者由业务 owner 明确接受丢弃窗口。DNS、配置中心和连接池会缓存地址,切换后应通过 @@server_uuid、审计日志和应用指标确认真实连接,不用控制台按钮状态代替数据面证据。
清理与回滚不是直接 DROP DATABASE
实验目标可以在确认没有连接后清理;生产迁移目标要先隔离、保留证据并经过数据 owner 批准。失败恢复的首选通常是重新创建干净目标并从已校验制品加载,因为在半成功目标上继续补 SQL 很难证明没有残留对象或重复数据。
-- 仅用于 localhost 合成实验
SELECT PROCESSLIST_ID, USER, HOST, DB
FROM performance_schema.threads
WHERE PROCESSLIST_ID IS NOT NULL AND DB='migration_lab';
DROP DATABASE IF EXISTS migration_lab;
DROP USER IF EXISTS 'migration_reader'@'localhost';
DROP USER IF EXISTS 'migration_loader'@'localhost';随后删除本地 option file、load progress 和 dump 前,先确认它们是否仍属于回滚窗口或审计保留。敏感制品放在访问受控、静态加密并有生命周期策略的存储;删除要覆盖本地副本、CI artifact、对象存储临时前缀和排障附件。哈希清单可以长期保留,含行数据、账号定义、GTID、内部主机名或错误 SQL 的文件则按数据分类处理。
迁移账号在完成后撤销,临时网络规则与证书一并回收。源库不会因为切流就立刻删除:先改只读、监控是否仍有客户端访问,经过约定观察窗与回切决策后再退役。长期备份与 PITR 按既有策略继续运行,不能因为“有迁移 dump”暂停保护。
架构取舍、容量成本与团队治理
小库、一次性环境复制、需要审阅或精细修改 SQL 时选 mysqldump;恢复窗口紧、表多且大、需要并行分块和断点进度时优先 MySQL Shell Dump/Load。若数据规模使逻辑重建索引无法满足 RTO,或业务不能承担快照到切流的停写窗口,就应评估物理克隆、原生复制或 CDC,而不是继续把线程数调大。跨异构数据库时还需要类型映射与语义转换,原生 MySQL 工具不会替团队解决这些问题。
成本模型至少包含源端扫描 I/O、长事务版本保留、网络出口、临时磁盘、压缩 CPU、目标 redo 与索引空间、对象存储请求、校验计算和工程师窗口。团队应保存每次演练的“数据量—并行数—吞吐—峰值资源—恢复耗时”曲线,用自己的基线做容量计划,不使用脱离硬件和数据分布的万能速度。
职责上,数据库 owner 批准快照、权限、GTID 与目标参数;应用 owner 定义业务不变量、写冻结和回切取舍;平台团队提供固定版本工具箱、加密制品通道、日志脱敏与流水线;安全团队审批跨环境网络和临时高权限;变更负责人维护阶段状态和唯一写入方。任何人都不应同时拥有修改 dump、执行高权限导入、批准差异和销毁源库的全部能力。
最后的接管门槛是一组可重复事实:版本与对象清单完整;事务表快照一致,非事务表有单独处置;dump 与 metadata 摘要可验证;字符集、collation、时区、BLOB 和生成列有代表性校验;GTID 决策与目标拓扑一致;账号和 DEFINER 已最小化;恢复在干净目标完整走过;业务不变量和真实应用读写通过;回切窗口、唯一写入方与源库退役条件都明确。做到这些,逻辑迁移才不再是一份“看起来导入成功”的文件,而是可接管、可追责、可回退的数据交付。
