上个月把手上一台跑着老业务的机器往新机房搬,数据目录里有 300 多万个小文件图片、缓存碎片、日志切片、用户上传的小附件,加起来 780G。我先按老习惯起了 rsync -avz,看着进度条爬了两个小时,才 12%。差点当场把任务 kill 掉重买带宽。后来才发现,我 780G 的数据里,真正在跑的大概只有 40G,剩下的时间全耗在元数据上stat、readdir、chown、setutime,一个文件一次系统调用,300 万个文件就是 300 万次起步。这篇把我最后跑通的那套方案完整写下来:换机器、跨机房、300 万小文件,怎么在 40 分钟内搬完。
如果你正打算把旧机器的数据整体搬到新机房,或者要给大批量小文件做一次跨机房同步,先看看这几家在大容量传输线路和存储上的配置说明再动手:dmit(服务器与主机,CN2 GIA 大容量互联)跨机房搬运最怕的是回程绕路,这条 CN2 GIA 通道就是冲着跨境大流量设计的;另一家可以看 fiberstate(企业级裸机与 IP 传输,99.9% 可用性),裸金属方案在源端做 tar 流时不吃 IO 调度器的亏。
别一上来就怪带宽。我第一次怀疑的是链路:源机在香港,目标机在新加坡,ping 看着才 3ms,往返一趟也不丢包,怎么就慢了?后来用 iostat -x 盯着源机看了十分钟,结论是 CPU 的 %sys 长期在 70% 以上,w_await 几百毫秒磁盘是 SSD,不是慢盘,是慢在"每次 stat 都要等一次 IO"。
小文件搬运的本质问题,是每个文件的元数据操作都要单独走一次系统调用和一次磁盘读。一个 4KB 的图片,真实数据 4KB,但它的 inode、权限、时间戳、目录项,加起来可能触发 5 到 8 次额外的 IO。300 万个文件乘下来,就是上千万次系统调用。就算空口带宽跑到 500Mbps,真正用来传数据的部分也不到两成。

我现在的习惯是搬之前先跑三个数。第一个是文件数:find /data -type f | wc -l。第二个是平均文件大小:du -sb /data 的字节数除以文件数,就知道是不是典型的小文件场景。第三个是元数据本身要占多少:du -sh --apparent-size /data 和 du -sh /data 一比,差值就是块对齐浪费的空间。
我这台机器的结论是:300 万文件、平均 260KB、apparent-size 780G、实际占盘 1.1T光块浪费就吃掉 320G。这条数据直接决定了后面所有的动作:既然瓶颈是元数据,就该把"逐个文件处理"改成"打包成流再处理"。
思路很简单既然文件多那就别一个个传,先在源机把它们打成几个大包,再当成大文件传。核心命令长这样:
tar -cf - -C /data . | split -b 2G - /data/pack_,然后对每个分片用 rsync 或 scp 往外推,并行度开到 4 路左右。为什么是 2G 一片?太小了包头开销占比高,太大了失败重传代价高,2G 是个比较稳的经验值,重传一次也就丢两分钟。
传完之后在目标机解包:cat /data/pack_* | tar -xf - -C /data。注意解包的时候要保证磁盘空间够打包本身在源端会产生同样的临时占用,我这次是先在另一块盘上打包,打完再删包,尽量不让源机出现"峰值翻倍"的尴尬。

并行这一块有个细节值得说:我用的是 xargs -P 4 -I{} rsync -aP {} user@target:/data/,把分片列表喂给 xargs 开 4 路并发。4 路是压测出来的开到 8 路的时候,目标机的磁盘写入被打满了,IO 等待反而让整体时间不降反升。迁移是 IO 密集型操作,不是网络密集型,并发拉太高只会互相抢磁盘。
跨机房 780G,谁也不敢保证链路一次不抖。tar 流有个天然的坑:中断了就是从头再来,前面传的那半个包全白干。所以我用的是"先打包、再传输"的解耦思路包本身是本地文件,rsync 对单个大文件的断点续传支持是原生的,断了重跑同一条命令,它会自动 seek 到断点位置续传,不用重传整个分片。
另外我把整个流程写成了个脚本,加了日志和失败重试:某一包传失败就单独重试那一条,成功后再合并到下一批。整个 780G 分成 390 个 2G 的包,并行推,最后清理中间文件。这套流程第二次跑(补传上次失败的)只花了 6 分钟。

最怕的不是失败,是"以为成功了"。我搬完做了三层校验。第一层是数量对账:源端 find /data -type f | wc -l 和目标端同样一条命令,数字必须完全一致,我这次两边都是 3,014,882。第二层是体积对账:du -sb 的字节数对比。第三层是抽样内容校验,随机挑几百个文件用 md5sum 对比。
三层都过了我才敢切流量。如果只做第一层,会漏掉"文件在但内容被截断"的情况;只做第二层,会漏掉"大文件被截断成一半"的情况。这三层加起来不到 20 分钟,但它挡住的是一次线上事故。

权限和属主。tar 打包时把属主一起带上,跨机解包后权限才不会乱。如果源端有 uid 500 的应用用户,目标机没这个用户,解包完属主会变成 nobody,程序就起不来了。我这次是先在目标机建好同名用户再解。
硬链接和稀疏文件。tar 打包会把硬链接打成两份,数据量凭空翻倍;稀疏文件则会被补成实心文件。我这批文件没这两类问题,但如果你搬的是虚拟机镜像或者数据库数据目录,一定要用 tar --sparse --hard-dereference 先看真实占用。
中途别在源机改数据。我最后悔的一点:打包打到一半,业务侧还在往目录里写新文件,结果包里就混进了时间点不一致的两批数据。正确做法是停写、或者用快照先冻结一个时间点再打包。这一步省下的 9 小时,比什么优化技巧都值。
最后再补一个提速点。如果你的链路支持,用 pv -L 200M 给管道限速,避免单条流把整条链路打满,反而拖累 ACK 回来。这是我从一次"越传越慢"的困惑里学到的。
整套下来,300 万小文件、780G 有效数据,从最初 rsync 预估 25 小时,到最后 tar 分片并行 40 分钟传完、20 分钟校验完,差了将近 40 倍。而这中间真正的变量只有一个:别再用"逐个文件"的思路处理"海量文件"的问题。先把文件合成流,再让网络去传它擅长的东西,这就是全部的秘密。