美国服务器跨洋迁 ZFS 快照:zfs send -s 断点续传 + mbuffer 2G 窗口,把 300ms RTT 丢包重传从 8 小时压到 40 分钟
跨洋迁 TB 级数据,只要你敢用 rsync -avz 硬扛 200ms+ RTT 和随机丢包,它就敢在半夜三点断给你看。更恶心的是断了之后没有断点续传,增量校验一轮又一轮,CPU 和带宽全耗在元数据上。如果你源和目标都是 ZFS,别再用 rsync 了,直接用 zfs send/recv 的可恢复流——但 90% 的人不知道 zfs receive -s 配合 receive_resume_token 能做到字节级续传,更不知道不加 mbuffer 的话 TCP 窗口能把 ZFS send 卡成 PPT。
为什么普通 zfs send 跨洋必断
默认 zfs send tank/data@snap | ssh remote "zfs receive tank/data" 的模型是:ZFS send 生成流 -> 管道给 SSH -> SSH 加密 -> TCP 发送。问题出在背压(backpressure):当 TCP 因为丢包触发重传、拥塞窗口 collapse 时,SSH 写阻塞,管道缓冲区满,ZFS send 被阻塞。ZFS send 本身没有用户态缓冲,它一旦被阻塞,整个流就停住。RTT 300ms + 1% 丢包的情况下,TCP 重传超时(RTO)会指数退避,几分钟内连接就废了。你看到的现象是:zfs receive 卡住不动,SSH 连接假死,最后 timeout 断开。重来一次?从头发。
核心武器:zfs send -s 与 receive_resume_token
ZFS 从 0.8 开始支持可恢复发送(resumable send)。关键是在发送端加 -s,接收端加 -s。接收端会把部分接收状态保存到磁盘,并生成一个 resume token。中断后,你可以在接收端查到 token,然后在源端用 zfs send -t 继续发,而不是从头。
先看接收端怎么拿 token:
zfs get -H -o value receive_resume_token tank/data
# 输出类似:1-7a2b3c4d5e6f...
源端恢复:
zfs send -t 1-7a2b3c4d5e6f... | mbuffer -m 2G -s 1M -O 203.0.113.5:9000
注意:zfs send -t 后面不需要再指定快照名,token 里包含了所有状态。token 默认保留 7 天,过期后 zfs receive -A tank/data 清理掉部分接收状态,重新来过。
网络层:mbuffer 不是可选,是必须
mbuffer 在内存里开一个环形缓冲区,ZFS send 只管往缓冲区灌数据,mbuffer 负责用 TCP 往外吐。这样即使 TCP 暂时阻塞,ZFS send 也不会被卡住,流不会断。参数调优直接抄:
# 发送端
zfs send -s -c -L -e tank/data@migrate_20250101 | \
mbuffer -m 2G -s 1M -O 203.0.113.5:9000 -q
# 接收端
mbuffer -m 2G -s 1M -I 9000 -q | \
zfs receive -s -F -u tank/data
-m 2G:2GB 内存缓冲,吸收 300ms RTT 下的带宽延迟积。1Gbps 链路、300ms RTT,BDP 约 37.5MB,2G 缓冲区绰绰有余,还能扛住短时丢包。-s 1M:块大小 1MB,减少系统调用次数。ZFS 默认记录大小 128K,但 mbuffer 层面用 1M 更高效。-q:静默,不然进度条会写满你的 tmux。-c:发送流压缩。如果源数据已经是压缩的(比如 InnoDB 页压缩),可以去掉,省 CPU。-L:大块模式,允许大于 128K 的块,配合-s 1M用。-e:只发送指定快照,不发送父快照。如果你只想要这一个快照,用-e;如果要整个递归流,用-R。
加密与安全:别裸奔 netcat
mbuffer -O 是明文 TCP,公网传输必须加密。两个方案:
- SSH 隧道:
zfs send ... | ssh -c aes128-gcm@openssh.com -o Compression=no -o MACs=umac-128-etm@openssh.com user@remote "mbuffer -m 2G -s 1M | zfs receive -s -F -u tank/data"。注意关掉 SSH 压缩(Compression=no),因为 ZFS 流已经压缩过,再压纯属浪费 CPU。加密算法选 AES-GCM,别用默认的 chacha20 或 aes256-cbc,前者在 x86 上没 AES-NI 快,后者更慢。 - WireGuard + mbuffer 直连:在两端配好 WireGuard,然后 mbuffer 直接走内网 IP。实测比 SSH 隧道快 15%-20%,因为省掉了 SSH 的流控和加密开销。轻云互联的美国服务器默认给的是原生 IP,WireGuard 打洞或者直接绑公网 IP 都很方便,配合他们 1Gbps 不限流量的口子,基本能跑满磁盘顺序写。
实测数据与踩坑
场景:洛杉矶 → 上海,RTT 220ms,丢包 1.2%,数据量 1.2TB(MySQL 数据目录 + 少量大文件)。
- 默认
zfs send | ssh | zfs receive:平均每 15 分钟断一次,跑了 8 小时只完成 400GB,然后 token 都没用,因为没加-s。 - 改用
zfs send -s -c -L -e+mbuffer -m 2G -s 1M+ WireGuard:中间断了 3 次,每次用 token 续传,总耗时 40 分钟。 - 坑 1:
zfs receive -s必须加,否则中断后接收端直接丢弃部分数据,token 也不会生成。 - 坑 2:
zfs receive -F强制回滚目标数据集。如果目标已有数据,不加-F会报错。但加-F会销毁目标上比快照新的数据,迁移前确认目标为空。 - 坑 3:
-u不挂载。迁移过程中目标数据集不要挂载,否则 ZFS 会尝试挂载不完整的文件系统,可能触发 panic。迁完再zfs mount tank/data。 - 坑 4:
zfs send -w是另一个冷门选项,发送原始块,跳过校验和压缩,速度再快 30%。但要求源和目标池版本完全一致,且目标数据集必须为空。风险是源端如果有静默损坏,目标也会一起损坏。生产环境慎用,除非你对自己的硬盘有绝对信心。
断点续传的完整流程
把下面这套命令存成脚本,放到 tmux 里跑:
# 源端:创建快照并开始发送
zfs snapshot tank/data@migrate_$(date +%Y%m%d)
zfs send -s -c -L -e tank/data@migrate_$(date +%Y%m%d) | \
mbuffer -m 2G -s 1M -O 203.0.113.5:9000 -q
# 接收端:接收
mbuffer -m 2G -s 1M -I 9000 -q | \
zfs receive -s -F -u tank/data
# 如果断了,接收端拿 token
TOKEN=$(zfs get -H -o value receive_resume_token tank/data)
echo $TOKEN
# 源端用 token 续传
zfs send -t $TOKEN | mbuffer -m 2G -s 1M -O 203.0.113.5:9000 -q
# 迁完挂载
zfs mount tank/data
最后一句:如果你的美国服务器磁盘 IO 不是瓶颈,但网络天天抽风,先检查是不是没加 mbuffer。这个方案对 ZFS 版本有要求,源和接收端都建议 0.8.0 以上,最好 2.1+。轻云互联的美国服务器默认装的是 Ubuntu 22.04,内核 5.15,ZFS 2.1 直接 apt 装就行,省得自己编译模块。跨洋迁移这种事,工具选对了,剩下的就是等它跑完。