大带宽服务器的数据迁移真实故障排查笔记 - 20260927
故障现场
老机房 4 台存储节点要下线,合计约 40TB 数据、1200 万左右 inode,混合了 8 个业务库的 binlog 归档、几十万个 4K~64K 的图片切片,以及一批平均 2GB 的视频源文件。目标端是轻云互联的一台 10Gbps 大带宽机型,本地 NVMe 做的 RAID,硬件上不背锅,纯看迁移链路。
迁移方案很朴素,rsync daemon 模式避开 SSH 加密开销,8 个并发进程按文件清单切片:
rsync -a --whole-file --numeric-ids --no-compress --info=progress2 \
--files-from=/tmp/list.000 \
rsync://src-node1/data /data/
跑起来 4.8Gbps 就封顶,8 个进程每个都在 2Gbps 上下抖。更邪门的是第 6 小时之后速度掉到 1.2Gbps,接收端 iostat -x 1 看 %util 已经 99%,但磁盘理论上还能吃下更多。
第一步:先排除网络,发送端窗口出卖了真相
在发送端抓 ss,最直观的一刀:
ss -tmi state established '( dport = :873 )'
输出里 cwnd 已经涨到 1200 多,retrans 为 0,但 snd_wnd 周期性地从 110000 掉到 6144 甚至 0:
rtt:0.081/0.021 wscale:7,7 rto:204 retrans:0/0 cwnd:1287
send 1784320000bps snd_wnd:6144 rcv_wnd:163840
链路 10G 全双工、RTT 0.08ms,却出现接收窗口塌缩,说明瓶颈不在“管道”,而在接收端没人读 socket。拥塞控制这时候其实是背锅侠。
去接收端确认:
nstat -az | grep -iE 'ZeroWindow|Prune'
TcpExtTCPWantZeroWindowAdv 184326
TcpExtTCPFromZeroWindowAdv 184319
TcpExtPruneCalled 92160
TcpExtRcvPruned 90112
WantZeroWindowAdv 飙到 18 万,PruneCalled 9 万次说明接收缓冲区被反复裁剪。到这里定性完成:接收端应用层读得慢,导致接收缓冲区耗尽,反压回发送端。
第二步:抓 kswapd 和脏页
接收端 256G 内存,先看内存水位:
grep -E 'Dirty|Writeback|MemAvailable' /proc/meminfo
Dirty: 17842136 kB
Writeback: 1536 kB
MemAvailable: 584120 kB
18G 脏页、回写量只有 1.5M、可用内存不到 600M。再看 vmstat 1 5:
r b swpd free buff cache si so bo in cs us sy id wa
1 14 0 584120 10240 251890000 0 0 62518 4210 18200 8 34 3 55
b 列常年 12~16,sy 34%,wa 55%。14 个进程阻塞在 IO,内核态时间被 kswapd 和回写线程吃掉了。触发链条是:
- 1200 万小文件,XFS 每个文件落盘要走「写新 inode → 目录项更新 → rename」三趟元数据同步提交;
- 日志提交的写是同步的小块随机写,把 NVMe 队列塞满,
aqu-sz冲到 41,w_await14ms; - 应用层
write()返回变慢,脏页以 GB 级堆积在 page cache; - 内存回收触发 kswapd 高频扫描,抢走大量 CPU 周期;
- rsync 进程被调度延迟,读 socket 变慢,接收窗口归零。
同时确认了一下中断亲和性,这是把火浇油的一环:
grep -E 'eth0-(rx|tx)-' /proc/interrupts
33: 1283749213 0 0 0 0 0 0 0 IR-PCI-MSI eth0-rx-0
34: 102938214 0 0 0 0 0 0 0 IR-PCI-MSI eth0-tx-0
全部压在 CPU0。查了下 systemctl status irqbalance 是 inactive —— 精简镜像里默认没起。于是 CPU0 上软中断 + NAPI 轮询,和刚被 kswapd 抢占的同一批 CPU 打架,收包延迟进一步放大,形成正反馈。
第三步:动手,四层一起改
1)脏页回写:把比率换成绝对字节
内存越大的机器,dirty_ratio 越危险。256G × 30% 允许瞬间积压 76G 脏页,那就会等到悬崖边才刹车。改成绝对字节,让回写平滑:
sysctl -w vm.dirty_background_bytes=536870912 # 512M 开始回写
sysctl -w vm.dirty_bytes=2147483648 # 2G 硬刹车
sysctl -w vm.dirty_writeback_centisecs=100 # 1s 唤醒一次回写线程
sysctl -w vm.dirty_expire_centisecs=3000 # 30s 过期
注意:dirty_bytes 与 dirty_ratio 是互斥的,设了前者后者自动变 0,别两边都写进 sysctl.conf。
2)XFS 日志与分配器
这一层直接决定元数据写的队列深度。改挂载参数:
/dev/nvme0n1p1 /data xfs \
defaults,noatime,nodiratime,logbufs=8,logbsize=256k,largeio,inode64,allocsize=1m 0 0
logbsize=256k 配合 logbufs=8 把日志缓冲从默认的 32k 抬到 2M 上限,对大批量小文件创建的吞吐提升非常直接——这是从 4.8Gbps 往上走的关键一步。
3)中断、RPS/XPS
systemctl enable --now irqbalance
# 手工绑核也行,把 rx/tx 队列中断散到 CPU8-23
i=8
for irq in $(grep -E 'eth0-(rx|tx)-' /proc/interrupts | cut -d: -f1); do
echo $(printf '%x' $((1 << i))) > /proc/irq/$irq/smp_affinity
i=$(( i == 23 ? 8 : i + 1 ))
done
# RPS 兜底(多队列不够时用)
for q in /sys/class/net/eth0/queues/rx-*/rps_cpus; do echo ffffff > $q; done
for q in /sys/class/net/eth0/queues/tx-*/xps_cpus; do echo ffffff > $q; done
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for f in /sys/class/net/eth0/queues/rx-*/rps_flow_cnt; do echo 2048 > $f; done
4)迁移策略本身也要改
让 8 个大块头并发去啃 1200 万小文件,是自找麻烦。改成大小文件分流:
# 小文件走 tar 流,跳过 rsync 的逐文件元数据事务
tar -C /data -cf - --files-from=/tmp/small.list | \
mbuffer -m 2G -q -s 1M | \
ssh root@dst 'tar -C /data -xf -'
# 大文件继续 rsync,但降到 4 并发,加 --inplace 避免临时文件 + rename 的三次元数据写
rsync -a --inplace --no-compress --numeric-ids \
--files-from=/tmp/big.list rsync://src-node1/data /data/
并发从 8 降到 4 不是保守,是因为并发越高,page cache 里的脏页峰值越高,回写线程越容易被同步日志堵住。
结果
调整前:4.8 Gbps,后段衰减到 1.2 Gbps,预估 12 天
调整后:稳定 8.4~8.7 Gbps,接收端 Dirty 常年 < 800M,kswapd 基本隐身
总耗时 4 天 6 小时
最后做校验的时候别再上 rsync -c,1200 万文件全量 checksum 会再花两天。用 size + mtime 的快比对先筛,只对不一致的少量文件做 sha256sum,性价比高得多。
迁移接收端的检查清单
snd_wnd塌缩 = 接收端反压,别再去调拥塞控制;- 内存 > 64G 的机器,一律用
dirty_bytes而非dirty_ratio; - XFS 承载小文件写入,先看
logbsize和logbufs,默认值在大批量 create 下就是瓶颈; - 确认
irqbalance真的在跑,别只看ethtool -l的队列数; - 小文件别用 rsync 硬啃,tar 管道流是最省元数据事务的土办法;
- 迁移期间盯
/proc/meminfo的 Dirty 而不是iostat的%util——前者才是真正的先行指标。