长肥管道跑不满:跨洋链路TCP调优实测

「日本到新加坡的线路,100M 带宽,跨洋传输只有 3MB/s。」这句话我在过去一年里至少听过五次,每次的现场情形都差不多:一台海外 VPS,用 scp 拖一个几百兆的备份包,进度条慢得像在爬,去测速网站跑一下单线程 TCP 也是四百多兆比特每秒,于是结论就是「线路不行」或者「供应商超售」。换了一家 VPS,还是这样,于是换回来,来回折腾两个月。

真相通常不在线路上,而在 TCP 自己。长肥管道(Long Fat Network,简称 LFN)这个名词是老概念,指的是「带宽大、延迟高」的链路,日本到新加坡的往返延迟在 65~80 毫秒之间,配上 100M 以上的带宽,正好落在 LFN 的定义范围内。这类链路的问题不是传不动,而是默认参数下根本传不满,而且它一点都不报错,只是慢。

这篇按四个层次讲:先讲为什么长肥管道跑不满(窗口与带宽时延积),再讲哪些内核参数真正影响它,然后是拥塞控制算法在丢包场景下的实际差异,最后讲应用层的并发策略为什么往往比调内核更能解决问题。全程只讲能验证的东西,每条结论都给出可复现的观察方法。

一、为什么带宽有、速度没有:带宽时延积这笔账

TCP 的吞吐量有一个硬上限,公式非常简单:

吞吐量上限 = 窗口大小(bytes) / 往返时延(seconds)

这个公式是从 TCP 的确认机制直接推出来的:发送方在没有收到确认之前,最多只能发出「窗口大小」这么多未确认的数据;一个往返时延内能发多少,就是吞吐上限。

代入具体数字。日本到新加坡往返 70 毫秒,如果接收窗口是 64KB(这是很多老系统的默认值),那么:

65536 bytes / 0.07 s = 936 KB/s ≈ 7.5 Mbps

也就是说,这条链路在接收窗口 64KB 的情况下,物理上不可能超过 7.5 兆比特每秒,无论你买的是 100M 还是 1G。这就是为什么测速网站(多连接并发)能跑到四百多兆,而单线程 scp 只有 3MB/s——测速网站开了八到十六个连接,每个连接各自吃一份窗口。

「带宽时延积」(Bandwidth-Delay Product, BDP)是这个公式的另一种说法:链路上「在途」的数据量最多是多少。100M × 70ms ≈ 875KB,也就是这条链路上同时能装 875KB 的数据,要让管道装满,收发两端的窗口都得能撑到这么宽。

怎么量出这条链路的真实上限

不用猜,可以直接算。三个数字,两个在本地就能拿到:

  • 往返时延:ping -c 20 目标IP 取平均 RTT,注意看是否有丢包;
  • 当前实际吞吐:用 scp 拖一个足够大的文件(建议 500MB 以上,短传输会被慢启动阶段稀释);
  • 反推有效窗口:实际吞吐 × RTT。如果得到 60~70KB,那基本就是卡在默认窗口上了。

我在一台默认配置的 Debian 11 上做过这个测量,跨洋链路 RTT 72ms、单线程吞吐 6.9Mbps,反推窗口 62KB,和 cat /proc/sys/net/ipv4/tcp_rmem 的默认中值对得上。这种情况下换 VPS 毫无意义,调参能提升一个数量级。

二、真正影响长肥管道的四个内核参数

网上关于 TCP 调参的文章很多,但大多数抄的同一份清单,里面一半是对 LFN 无效的。下面四个是实际有效的,按影响从大到小排。

1. tcp_rmem 与 tcp_wmem:自动调优的上限

Linux 从 2.6.17 开始默认开启接收窗口自动调优,tcp_rmem 有三个值,分别是「最小值、默认值、最大值」:

net.ipv4.tcp_rmem = 4096 87380 6291456
net.ipv4.tcp_wmem = 4096 16384 4194304

自动调优只在最小值与最大值之间伸缩,最大值是天花板。默认 6MB 的接收上限理论上是够的(6MB / 0.07s ≈ 685Mbps),但发送端的上限 4MB 对应 457Mbps,加上自动调优是渐进的、需要多个往返才能爬到上限,短连接根本爬不上去。

千兆内网或跨洋大文件传输,建议把最大值放宽到 16MB 级别。注意同一个参数在接收方和发送方都要看:接收窗口由接收方决定,发送缓冲由发送方决定,两头都得够。只调自己这一端,效果减半。

2. tcp_window_scaling:不能关

TCP 头部里窗口字段只有 16 位,最大 65535 字节,这在 1981 年设计时是完全够用的。后来为了支持大窗口,RFC 1323 引入了窗口缩放因子(window scale),在三次握手时协商,可以把窗口按 2 的 n 次方放大到 1GB。

这个选项在现代内核里默认开启,但某些精简系统或经过「安全加固」的机器上会被关掉。后果是一旦关闭,窗口被硬限制在 64KB,长肥管道直接死掉。检查方法:sysctl net.ipv4.tcp_window_scaling 应该是 1。如果是在容器里跑,要注意宿主机的 net 命名空间参数会继承下来。

3. tcp_slow_start_after_idle:空闲后不要回到起点

这个参数的默认值是 1,意思是:一条连接如果空闲超过一个 RTO(重传超时,通常 200ms 以上),拥塞窗口会被重置到初始值,重新走慢启动。

对于「保持长连接、偶尔传一次数据」的场景,这是灾难性的:每次传数据都要从 10 个 MSS 起步,慢慢指数增长,传到上限要四五个往返,也就是三百多毫秒的爬坡期。如果是每秒一次的小请求,等于每次都在爬坡。

把它设为 0,代价是在链路状况可能已经变化的情况下,继续按旧的窗口发送,有轻微的风险;对稳定的国际链路,收益远大于风险。

4. 拥塞控制算法:别只看名字

这一条单独讲,因为它是最容易被误解的。很多人把 net.ipv4.tcp_congestion_control 改成 BBR 就以为解决了问题,实际情况要分场景。

三、丢包与拥塞控制:为什么 BBR 在跨洋链路上有效

传统拥塞控制(CUBIC、Reno)把「丢包」当作拥塞信号:检测到丢包就把窗口减半。这个假设在有线网络上大体成立,但在现代互联网上有两个问题。

第一个问题是浅缓冲(bufferbloat 的反面):中间设备的队列很短,一堆连接同时打满时会有随机丢包,但链路本身并没有拥塞。CUBIC 会把这种随机丢包误判为拥塞,于是所有连接一起减窗,吞吐在「打满—减半—再打满」之间锯齿震荡,平均下来只能用到链路容量的七成左右。

第二个问题是长肥管道放大误差:跨洋链路的 BDP 很大,从丢包到窗口恢复要走很多个往返,一旦掉速,恢复要几秒钟。而跨洋链路上丢包率哪怕只有 0.1%,也意味着每秒都在掉速。

BBR 的思路换了:它不再把丢包当信号,而是测量——持续探测链路的瓶颈带宽与最小往返时延,用这两个测量值来定发送速率。表现出来就是:在有一定随机丢包的链路上,BBR 的吞吐通常显著高于 CUBIC,而且更稳定。

但 BBR 不是万能的,三个真实的边界

边界一:带宽探测会导致队列堆积。BBR 为了持续估计瓶颈带宽,会周期性地把速率抬高 25% 试一下,这在瓶颈队列很浅的链路上没问题,但在队列很深的设备上会导致排队延迟上升。多人共享的国际链路上,如果对端也在跑 BBR,公平性问题一直有争议(v1 尤其明显,v2 改善了共存性)。

边界二:只在发送端生效。拥塞控制是发送方的行为,你在服务器上改成 BBR,只影响从这台服务器往外发的流量。如果是你在本地下载服务器上的文件,起作用的是你本地的算法,改服务器没用。这个细节经常被忽略,导致改完以后「一点效果都没有」。

边界三:丢包类型很关键。如果丢包是链路错误(光缆质量、跨海段落),任何算法都救不回来。判断方法:看 netstat -s 里的重传率,健康值是 0.1% 以下,超过 0.5% 就要怀疑物理层;同时用 mtr 看每一跳的丢包分布,如果某一跳开始持续丢包且后续跳也丢,那一段就是问题所在。注意中间设备可能对 ICMP 限速,造成假丢包,判断依据要看 TCP 层的重传率而非 ICMP。

四、应用层:并发往往比调参更有效

上面讲的都是单条连接的极限。但在实际场景里,单条连接优化到极致,也未必比「四条连接」快。原因很简单:单连接受制于窗口和拥塞控制,多连接相当于并行地占多份窗口。

这一点在工具层面已经有成熟的实现,问题在于很多人不知道或者没打开:

rsync 与 scp

scp 是单流的,没有并发选项。想加速跨洋大文件传输,换成 rsync 加 --compress 有时能快一倍,但真正有效的是先分块再并行:把大文件切成固定大小的块,起 N 个 scp 或 rsync 进程同时传,传完再合并校验。这看起来很土,但在跨洋链路上是最可靠的加速方式。

rclone 的 --transfers 与 --multi-thread-streams

如果备份在对象存储上,rclone 的两组参数非常关键。--transfers 控制并发传输的对象数(小文件多的时候调大),--multi-thread-streams 控制单个大文件被切成多段并发下载的段数(大文件多的时候调大)。这两个参数配合,一百兆以下的单文件不要多线程(切分开销大于收益),一兆以上的大文件可以开四到八段。

再配合 --buffer-size,把每次读写的缓冲调大一点,能减少系统调用次数。实测在一台 100M 的跨洋 VPS 上,默认配置同步两万个中小文件需要四十多分钟,调成 transfers 8、buffer 32M 之后降到九分钟——这些提升跟内核参数无关,纯粹是并发与缓冲。

HTTPS 下载:HTTP/2 与多路复用

如果传的是网站内容,走 HTTP/2 多路复用时,一个连接上有多个流,但是所有流共用一个拥塞窗口,所以它并不能绕过 BDP 的限制。真正的加速来自「多个连接 + 每个连接各自爬窗口」,浏览器对同一域名默认开 6 个连接,就是基于这个道理。这也解释了为什么很多 CDN 会用多个域名来分发静态资源。

五、一份可复现的验证流程

调参这件事最容易犯的错误是「改完感觉快了」。下面这套流程是为了避免主观判断,每一步都能停下来核对。

第一步:建立基线

改动之前先跑三轮传输,用同一个文件、同一个方向,记录耗时。文件要够大,让传输持续两分钟以上,否则慢启动阶段占比太大,结果没有参考价值。同时记录 ss -ti 里这条连接的 cwnd 与 rtt。

第二步:一次只改一个参数

同时改四个参数,最后不知道哪个起了作用,而且出问题时无法定位。建议的顺序是:先看 tcp_window_scaling 是否为 1(这是开关,不是调优),再放宽 tcp_rmem/tcp_wmem 的最大值,然后试 tcp_slow_start_after_idle,最后才考虑换拥塞控制算法。

每改一次,重跑三轮,记录耗时与 cwnd。如果耗时变化在 10% 以内,说明这个参数不是瓶颈,可以回滚,减少长期维护的变量。

第三步:注意参数生效范围

sysctl 改的是内核全局值,但对已经建立的连接无效。要验证效果必须重新建立连接。而且如果你在容器里跑服务,容器有自己的 net 命名空间,宿主机的 sysctl 不一定生效,需要用 docker exec 进去看 /proc/sys/net/ipv4/tcp_rmem 的实际值。

第四步:写下来

这是最容易被跳过的一步。我见过太多人三个月后又遇到同样的问题,完全不记得自己调过什么,于是重来一遍。我的做法是在 /etc/sysctl.d/99-network.conf 的每一行上面加一行注释,写清楚改动日期、原因和预期效果。文件本身就是文档。

六、什么情况下不要再折腾 TCP

调参的收益是有上限的。下面这些情况,说明瓶颈根本不在 TCP:

CPU 已经打满。加密与压缩都吃 CPU,scp 的 AES 加密在低配 VPS 上能吃掉一个核。判断方法:传输时看 top 里 sshd 的 CPU 占用,如果接近 100%,关掉压缩(-o Compression=no,压缩在 CPU 上往往比省下来的带宽贵)或者换更快的方式。

磁盘是瓶颈。传大文件时如果 iostat -xz 2 里某块盘的 %util 接近 100%,慢的是读盘不是网络。这时候最有效的做法是先把文件读进内存(内存够大的话)或者提高读缓冲,而不是改 TCP。

对端窗口太小。如果对端是一台老系统或者一个配置保守的存储服务,它的接收窗口上限可能只有几百 KB。你在发送端把 tcp_wmem 调到 16MB 完全无效——发送端不能超过接收方通告的窗口。这种情况唯一的办法是让对端也调,或者改走多连接。

链路本身有物理问题。重传率长期在 1% 以上、mtr 显示某几跳稳定丢包,这说明链路质量真的有问题,该找供应商就找供应商。这时候调参是在给坏链路打止痛药。

写这篇的原因很简单:跨洋传输慢,是所有海外 VPS 用户都会遇到的问题,而大多数人的第一反应是「线路不行,换一家」。换三家的成本,远高于花半小时量一下 RTT 和窗口、确认一下自己卡在哪一层。我自己的判断顺序一直是:先量 RTT 与实际吞吐,反推有效窗口;再确认 tcp_window_scaling 与 rmem 上限;然后才是拥塞控制;最后才考虑通道质量。这四步走完,能用调参解决的占七成,剩下的才真的是钱和线路的问题。

Last modification:October 25th, 2026 at 09:25 pm

Leave a Comment