VPS数据迁移三大翻车点:PHPMyAdmin字符集变异、inode耗尽、MTU黑洞

先说句实话

网上教程都在教你 rsyncmysqldump 的命令怎么拼,但真正把生产环境搬挂的,往往是命令之外的东西。下面三个坑,都是我亲手踩过的,全部具备“报错不明显、定位靠经验”的隐蔽性。

坑一:PHPMyAdmin 导出的 SQL,字符集在你点击“导出”时就已经被改写了

症状:迁移后页面出现“锟斤拷”或方块问号。用命令行查表,字段类型和排序规则明明是 utf8mb4,数据却是乱码;更气人的是,数据库里看不出任何报错。

原因:phpMyAdmin 的导出页面默认会把“将内容转换为”设为 utf-8。如果源库表是 latin1,导出时 MySQL 服务端已经按 utf8 连接字符集做了字节转换。原本 latin1 下“你好”的字节是 0xC4E3 0xBAC3,经过连接层被当作 utf8 解码成“ä½ å¥½”,再以 utf8 字节序列写进 SQL 文件。后面你把它导入 utf8mb4 库,等于二次转码,原始字节彻底找不回。

排错命令

# 检查源库真实字符集
SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME = '你的库名';

# 检查导出文件头部
head -20 dump.sql

# 检查导出文件编码
file -bi dump.sql

在目标库上可以还原这条变异链:

SELECT CONVERT(CAST(CONVERT('你' USING latin1) AS BINARY) USING utf8mb4);

解法:不要用 phpMyAdmin 的默认导出。要么在“导出”页把“将内容转换为”选为“不转换”;要么干脆用命令行,按目标库字符集导出,比如目标库是 utf8mb4:

mysqldump -h源库IP -u用户 -p --default-character-set=utf8mb4 --single-transaction --set-gtid-purged=OFF --hex-blob 库名 > dump.sql

导入侧同样明确指定字符集:

mysql -h目标IP -u用户 -p --default-character-set=utf8mb4 库名 < dump.sql

避坑备注:如果已经导入完了,千万别再做一次“utf8 转 utf8”的补救,那会让数据再次变异。先停应用,用上述变异链脚本遍历可疑字段,确认后再决定回滚或重建。

坑二:df -h 还剩 80%,df -i 却已经 100%——小文件迁移直接把 inode 打爆

症状:rsync 同步几千张小图片后,突然报 No space left on device。用 df -h 看磁盘还剩 80%,但 df -i 已经 100%。

原因:VPS 系统盘在 mkfs 时,inode 数量与分区块大小绑定。如果你买的小鸡默认 block size 是 1024 字节或 2048 字节,5 万张小文件(几 KB 到十几 KB)能吃掉全部 inode,而磁盘容量只用了不到一小半。

排错命令

# 查看磁盘和 inode 使用率
df -h /
df -i /

# 查看文件系统块大小和 inode 数量
tune2fs -l /dev/vda1 | grep -E 'Block size|Inode count'

# 统计源机文件数量
find /data -xdev -type f | wc -l

如果 find 统计出的文件数量已经接近 inode 上限的一半,别硬迁。先打成归档包再传:

cd /data && tar -cf - ./ | ssh root@新VPS_IP 'cd /data && tar -xf -'

归档包只消耗一个 inode,比逐文件 rsync 安全得多。若目标机器还没开跑,可以在重新分区时预留 inode:

mkfs.ext4 -i 16384 -N 5000000 /dev/vdaX

-i 表示每多少字节分配一个 inode,-N 强制设定总数,仅适用新盘,别对已有数据盘执行。)

避坑备注:如果你用轻云互联的 VPS,创建工单让售后帮你确认镜像默认 inode 数,给得比一般小厂准;换机器时优先选大系统盘,这个钱不能省。

坑三:两边都是千兆,rsync 却像睡着一样,先别调并发,测 MTU

症状:从 A VPS 向 B VPS 传 50MB 文件,耗时 20 分钟;CPU 两边都处于低水位,小文件正常,大文件长时间卡住。SSH 能连上,命令能执行,就是传输不动。

原因:路径 MTU 黑洞。本机网卡 MTU=1500,对端链路(例如经过 IPsec VPN 或某种隧道封装)实际可用 MTU 只有 1450。中间的哑路由器不返回 ICMP Fragmentation Needed,大包被静默丢弃。TCP 三次握手时通过 MSS 协商的段大小是 1460,超过链路 1450,于是数据段无法到达;小包(控制报文)能过,传输就是一动不动。

排错命令:在源机执行

# 先测 1500 MTU 路径
ping -M do -s 1472 目标VPS_IP

# 逐级缩小,直到能通
ping -M do -s 1412 目标VPS_IP
ping -M do -s 1400 目标VPS_IP

-M do 的意思是“不允许分片”。-s 1472 + 28(IP头+ICMP头) = 1500,如果第一跳就不通,说明路径 MTU 小于 1500。

解法:临时让本机 TCP 发出的报文按路径 MTU 钳制,不需要改应用:

iptables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

如果方便,直接把网卡 MTU 降到 1400:

ip link set eth0 mtu 1400

注意回程方向也可能存在同样问题,目标机上的 TCPMSS 规则也要加。这个坑用 tracepath 也能确认:

tracepath 目标VPS_IP

避坑备注:曾经在老东家内网遇到过,后来换到轻云互联的美国 VPS 做异地容灾,同样的 rsync 命令能直接跑满带宽,因为它们对网络栈和路由路径做过处理,少踩一半的雷。

迁移后的“最后一公里”检查

数据搬完不等于活儿干完。以下是我每次迁移都要跑的收尾命令,按优先级排序:

  • 统计信息:MySQL 导入后索引统计信息是空的,执行 ANALYZE TABLE 表名;mysqlcheck -Ao,否则同样的 SQL 可能从走索引变成全表扫。
  • 时区SELECT NOW(), @@global.time_zone, @@session.time_zone; 确认源端和目标端一致。很多新手把 UTC 误当成 CST,业务数据时间全部偏移 8 小时。
  • 伪静态和 crontab:Nginx 的 rewrite 规则、crontab 里的绝对路径、.env 里的数据库白名单,这些不会跟着数据走。

总结

VPS 迁移最大的风险不是命令写错,而是“你以为它成功了”。字符集变异、inode 耗尽、MTU 黑洞,任何一个爆发都没有明显报错,等用户反馈才发现就晚了。开迁之前,先花十分钟做这三项巡检,比后期数据回滚香得多。