ARCHIVE NO. 004
跨机房备份只跑到 12 MB/s:一次 TCP 接收窗口的排查
现象:链路是空的,备份就是快不起来
跨机房的对象存储要做一次全量 rsync,两端之间是一条 2 Gbps 的专线。按容量估算, 20 TB 的数据应该十几个小时跑完,实际排了将近三天。
在两端的监控上看,磁盘都很闲,专线利用率常年停在 6% 上下。真正有意思的是 单条 rsync 的速率——它不是忽快忽慢,而是几乎钉死在同一个数上:
rsync -av --info=progress2 /srv/objects/ backup@10.20.0.7:/srv/objects/
# 19,742,113,280 62% 12.41MB/s 0:30:12稳定本身就是线索。 拥塞、丢包、磁盘抖动都会让速率起伏;只有一种情况会让它 贴着一条直线走——发送方一直在等接收方开窗,而窗口的大小由两端内核的参数决定, 与链路当前有多空无关。
先把磁盘和 rsync 自己排除掉
iostat 显示源端盘阵的 %util 不到 8%,await 在 2 ms 以内;rsync 的
--info=progress2 也没有显示任何单个大文件的读取卡顿。为了确认瓶颈不在应用层,
直接用 iperf3 打一次:
# 目的端
iperf3 -s
# 源端,单流,跑 30 秒
iperf3 -c 10.20.0.7 -t 30
# [ ID] Interval Transfer Bitrate Retr
# [ 5] 0.00-30.00 s 44.6 MBytes 12.5 Mbits/sec 0Retr 是 0——没有重传,链路是干净的。12.5 Mbits/sec 换算过来正好是
1.5 MB/s,比 rsync 还慢,因为 iperf3 的默认缓冲比 rsync 更小。
到这里可以下结论了:问题不在磁盘,不在重传,也不在链路容量,而在窗口。
一个除法就能解释:窗口 ÷ RTT
TCP 的吞吐在一个没有丢包、没有拥塞的路径上由两个量决定:
其中 是任一时刻在途、尚未被确认的字节数,也就是有效窗口; 是一个往返的耗时。要打满一条容量为 的链路, 在途数据必须至少填满这条管道,这个量叫带宽时延积:
这条专线是 2 Gbps,两端的实测 RTT 是 40 ms,于是:
要在 40 ms 的 RTT 上跑满 2 Gbps,必须有 10 MB 的数据同时在途。 这个数字是整篇文章的关键:在同一个机房里 RTT 是 0.2 ms, 同样打满 2 Gbps 只需要 50 KB——两个场景差了两个数量级, 这也是为什么”局域网里从没遇到过这个问题”完全不能作为跨机房的参考。
反过来,用实测速率倒推当前的有效窗口:
496 KB。这个数不是随机的,它是 512 KB 减去协议头与排队抖动之后的余量。
窗口缩放没问题,卡在内核的接收缓冲
第一个要排除的是窗口缩放(window scaling):TCP 头里的窗口字段只有 16 位, 不做缩放时上限就是 65535 字节。496 KB 远大于 64 KB,说明缩放选项协商成功了—— 如果中间设备把 SYN 里的 WS 选项剥掉,吞吐会被压在 64 KB / 40 ms ≈ 1.6 MB/s, 那会是一个更惨的数字。
ss -ti 能看到连接的实时状态,其中 wscale 是协商出来的缩放因子,
rwnd_limited 则表示发送方被接收窗口卡住了:
ss -ti dst 10.20.0.7
# cubic wscale:7,9 rto:401 rtt:39.8/0.9 ato:40 mss:8948
# rcvmss:536 advmss:8948 cwnd:1024 ssthresh:512
# bytes_acked:... bytes_received:... segs_out:...
# rwnd_limited:2013 (12.8%) ...wscale:7,9 表示发送方的缩放因子是 、接收方是 ,缩放本身工作正常。
真正的问题在接收侧的内核参数:
sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.core.rmem_max
# net.ipv4.tcp_rmem = 4096 131072 524288
# net.ipv4.tcp_wmem = 4096 16384 4194304
# net.core.rmem_max = 212992tcp_rmem 的第三个值是自动调优的上限:接收缓冲最大只能涨到 524288 字节,
也就是 512 KB。上一步倒推出来的 496 KB 正好顶在天花板上。
自动调优(autotuning)本身是好的——它按 RTT 与实测速率动态放大窗口, 不需要人工算 BDP。但它只能在管理员给的上限内活动,而这两个机器的上限是 按局域网标准配的。这就是整件事的根因一句话版: 自动调优没有失灵,是它撞到了墙。
修复
把接收与发送的上限都放到 BDP 之上
目标是让窗口能涨到 10 MB 以上。留两倍余量,把上限设到 32 MB:
sudo tee /etc/sysctl.d/99-tcp-bdp.conf >/dev/null <<'CONF'
# 接收侧:min / default / max
net.ipv4.tcp_rmem = 4096 131072 33554432
# 发送侧:min / default / max
net.ipv4.tcp_wmem = 4096 65536 33554432
# 自动调优自身的上限,必须不低于上面两个 max
net.core.rmem_max = 33554432
net.core.wmem_max = 33554432
CONF
sudo sysctl --system有三点容易踩空:
net.core.rmem_max必须一起放。 它约束的是setsockopt能设到的最大值, 而自动调优的放大过程受它钳制。只改tcp_rmem而漏掉它,往往看不出变化。- 改完只对新连接生效。 已有连接沿用它建立时的参数,rsync 必须重跑。
- 32 MB 不是越大越好。 每个连接都可能占住这个缓冲,并发上千条时 这是实打实的内存。按 BDP 的两倍给,不要按”越大越稳”给。
单连接仍然不够,用并发把管道填满
即使窗口放开了,单条 TCP 还要受拥塞控制算法的爬升速度限制。 在 40 ms 的 RTT 下,从 涨到 10 MB 需要相当长的时间, 而一次 rsync 传输里大部分文件都很短命,根本等不到窗口涨满。
务实的做法是开多条连接,让每条各自填一小段管道:
# 每个大文件起一条独立连接;小文件仍走同一条,避免建连开销
rsync -av --info=progress2 \
--max-size=256m --whole-file \
/srv/objects/ backup@10.20.0.7:/srv/objects/--whole-file 在跨机房场景下通常反而更快:rsync 默认的增量算法要两端
各自算一遍校验和,在 RTT 40 ms 时这笔往返开销比多传一遍还贵。
多连接之后,瓶颈回到专线本身,实测稳定在 190 MB/s 上下,接近 2 Gbps 的线速。
复盘
- 跨机房的任何传输问题,先算 BDP。 一个乘法就能知道”窗口够不够”, 比一项项排除磁盘、交换机、驱动快得多。
ss -ti是这条路径上最有信息量的命令:wscale告诉你缩放有没有协商成功,rwnd_limited直接指出是不是被接收窗口卡住,rtt给出算 BDP 需要的那个数。- 局域网经验会骗人。 同一个参数在 0.2 ms 的 RTT 下绰绰有余, 在 40 ms 下就是瓶颈;差的是 200 倍。
- 内核默认值是为通用场景配的,不是为长肥管道(LFN)配的。
凡是跨机房的批量传输,
tcp_rmem/tcp_wmem的上限都值得单独审一遍。 - 顺手记一条巡检项:
net.core.rmem_max应当不低于net.ipv4.tcp_rmem的 第三个值。两者不一致时,自动调优的天花板由较小的那个决定, 而配置看起来完全正常——这是最容易怀疑错方向的一种。