关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

合同上写着 100M 实际只有 3MB/s:跨境链路跑不满带宽,先算这笔带宽时延积的账

发布时间:2026-10-10

合同上写的是 100M,传一个文件却只有 3MB/s 上下。粗算一下,3MB/s 大致就是 24Mbps 的量级,也就是说你按 100M 付的钱,实际落袋的只有两成多一点。碰到这种事,绝大多数人的第一反应是「带宽买少了,加带宽」,而这一步往往是整件事里最贵、也最没用的一个动作。原因很直接:在往返时延较长的链路上,真正卡住吞吐的常常不是运营商给了你多少带宽,而是这条 TCP 连接自己的窗口有多大。窗口不够,给你 1G 的口子,单个连接该跑多少还是多少。这篇文章只谈这一件事——为什么标称带宽跑不满、为什么加带宽救不回来,以及一个能照着走的排查顺序。

第一,单条 TCP 连接的吞吐上限约等于窗口除以往返时延,跟标称带宽没有直接关系。带宽只是这条管道的理论上限,窗口和时延才决定你实际能往里塞多少东西。这一步搞反了,后面所有的优化都是白做。

第二,带宽时延积(BDP)就是「要把这条管道填满,窗口至少得开到多大」。它是带宽乘以往返时延,算出来的单位是字节,直接对应窗口大小。算不出这个数,你就没法判断到底是带宽不够还是窗口不够。

第三,窗口不够不是因为没人想到,是因为默认值和自动调优的边界就摆在那儿。窗口缩放、接收缓冲区自动调优都在起作用,但它们的默认上限是按「正常网络」设计的,遇到长时延链路就不够看了。

第四,光把缓冲区调大不一定有用。实际能发的量取的是拥塞窗口和接收窗口里更小的那个,而拥塞窗口归拥塞控制算法管。CUBIC 把丢包当拥塞信号,在「带宽大、时延长」的长肥管道上,一次丢包能把窗口砍下来,恢复又慢。

第五,排查顺序是固定的:先算 BDP,再看单连接和多连接的差别,再谈算法与内核参数,最后才考虑加带宽。顺序走反了,就会出现「带宽从 100M 加到 500M,速度一点没变」这种事。

矛盾现场:带宽标称值和实际吞吐差了一个数量级

先把现场摆清楚。一台机器的合同带宽是 100Mbps,从远端拉一个几 GB 的文件,传输工具上稳定显示 3MB/s 上下,偶尔抖一抖,但基本不往上涨。登上机器看:CPU 很闲,内存很闲,磁盘 IO 也没打满,网卡流量就是上不去。换个时间段再试,还是 3MB/s;换一个更大的文件再试,还是 3MB/s;再开一个传输任务,两个任务加起来的速度反而接近 6MB/s。

这三个现象拼在一起,答案其实已经摆在桌面上了。CPU、内存、磁盘都不是瓶颈——它们在等,不是忙不过来。速度不随时间、不随文件大小变化——说明不是对端限速策略在起作用,也不是某个抖动窗口的问题。而「开两个任务速度翻倍」这一条最要命:如果瓶颈真的是那根 100M 的管子,两个任务应该是抢同一根管子,加起来还是 100M 的上限,不可能变成两倍。它能翻倍,就说明每一条连接各自被某个东西限额了,而那个限额不是带宽。

这个「某个东西」,在 TCP 里就是窗口。下面要做的,是把这个结论从定性变成定量——用一个公式算出「这条链路理论上需要多大的窗口」,再拿它跟你机器上实际的窗口设置对一下,看看差多少。这一步省不得,没有这个数,你后面所有的调整都是在摸黑。

单连接的吞吐上限不是带宽,是窗口除以往返时延

TCP 不是发一个包等一个确认,那样太慢。它的做法是一口气发出一批数据,等确认回来了再往前推。这个「一口气能发多少还没被确认的数据」,就是窗口。发送端在任意时刻已经发出、但还没收到确认的数据量,不能超过窗口。于是有个很朴素的算术:一个往返时延内,你最多只能发出一个窗口的数据。往返时延是 200 毫秒,就意味着每 200 毫秒你才能往前推一个窗口的量。

于是吞吐上限就出来了:吞吐 ≈ 窗口 ÷ 往返时延。这个式子里压根没有「带宽」这个变量。带宽的角色是天花板——你算出来的这个值如果超过了带宽,那就取带宽;如果没超过,那你的吞吐就被窗口和时延钉死了,跟带宽多少没关系。

这句话值得再念一遍:带宽决定的是上限,窗口和时延决定的是你能不能摸到这个上限。给一条 200 毫秒往返时延的链路配 100M 带宽,意思是「如果你把窗口开够,你最多能跑到 100M」;窗口没开够,这 100M 就是个摆设,你付的是它的钱,享受到的是窗口给你的那一丁点。

还有一层容易被忽略:这里的「窗口」是每一条连接各自的一个,不是整台机器共用的一个。这一点是后面「多开线程能跑满」的全部原因,也能解释为什么很多下载工具默认就把连接数开到 4 条、8 条甚至更多——它们不是在耍花招,是在绕开单条连接的窗口限制。

带宽时延积怎么算:把它换算成「窗口至少得多大」

把上面那个式子倒过来写,就是带宽时延积:BDP = 带宽 × 往返时延。它的物理含义特别好懂——把这条链路想象成一根水管,带宽是管子的粗细,往返时延是水从这头流到那头再回来所需的时间,那么「管子里面正在路上、还没流到的水的总量」,就是带宽乘以往返时延。要把管子填满,你手里至少得有这么多的水。换成 TCP 的说法:要把这条链路跑满,窗口至少得开到 BDP 这么大。

单位换算要注意。带宽习惯上按 bit/s 算,往返时延按秒算,两者相乘得到的是 bit,除以 8 才是字节,而窗口的单位是字节。算的时候最省事的做法是先把带宽换成「每秒多少兆字节」:100Mbps 除以 8,约等于 12.5MB/s。

拿开篇那个场景算一下。100Mbps、往返时延 200 毫秒:12.5MB/s 乘以 0.2 秒,等于 2.5MB。也就是说,要把这条 100M 的链路跑满,窗口至少得开到 2.5MB 这个量级。反过来验算:窗口如果是 2.5MB,除以 0.2 秒的往返时延,得到 12.5MB/s,正好是 100Mbps 的满速,公式自洽。

再往下推一步。假设实际的窗口只有 256KB——这个数在默认配置里一点都不夸张——那这条连接能跑到多少?256KB 除以 0.2 秒,约等于 1.28MB/s,折算成比特率大约 10Mbps 量级。窗口再小一点,比如 128KB,那就是 640KB/s、5Mbps 量级。你看,从 100Mbps 掉到 10Mbps 甚至 5Mbps,中间没有任何一个环节涉及到运营商给了你多少带宽,纯粹是窗口除以往返时延的结果。开篇那个 3MB/s,反推回去对应的窗口大概在 600KB 上下——同样是「窗口不够」,不是「带宽不够」。

必须说清楚:上面这些数字全部是按公式换算出来的量级,不是任何一次实测的结果。它们的用途是给你一个判断尺子——先算出你这条链路需要多大的窗口,再去查你机器上实际的窗口是多少,两个数一比,问题在哪一目了然。真实的数字长什么样,取决于你自己的链路、自己的内核参数和自己的对端,只能你自己测,别人的数对你没有参考价值。

窗口为什么默认不够:窗口缩放与自动调优在干什么

既然窗口这么重要,为什么默认就是不够的?这得从 TCP 头部的字段说起。当初设计 TCP 的时候,头部里留给「窗口」这个字段的位数是 16 位,最大值 65535 字节,也就是 64KB 上下。在那个年代这个数大得离谱,可放到今天的长时延链路上,64KB 除以 200 毫秒只有 320KB/s,连 3Mbps 都不到。

解决办法是窗口缩放(window scaling),在建立连接的时候协商一个缩放因子,把 16 位的窗口值左移若干位,最大可以放大到约 1GB 的量级。Linux 上这个开关默认就是打开的,所以现代系统理论上不存在 64KB 这个硬天花板。但「打开了缩放」和「实际开到多大」是两码事——缩放解决的是上限问题,不解决实际值问题。真正决定实际窗口大小的,是接收缓冲区有多大,以及内核的自动调优把它调到了多少。

这里要把两个窗口分清楚,很多人就是在这一步搞混的。一个是接收窗口,由接收端的接收缓冲区决定,意思是「我这边还有多少地方能放数据,你别发超了」;另一个是拥塞窗口,由发送端的拥塞控制算法维护,意思是「我判断这条路上现在能承受多少,我自己悠着点发」。发送端实际能发出去未确认的数据量,是这两个里面更小的那个。这条规则极其重要——它直接解释了为什么「把接收缓冲区调得很大」有时候一点用都没有:拥塞窗口那一边没上去,接收窗口开到天上去也没人用。

内核的自动调优负责动态地把接收缓冲区调大调小,它会根据连接的实际表现去估算合适的大小,而不是一直顶在上限。这个机制在多数场景里是好东西,省得你手工算。但它的默认上限同样是按「常见网络」定的,遇到长时延链路就可能不够;而且它调的依据是这个连接自己表现出来的行为,如果连接本身一开始就因为算法保守而没跑起来,它能观察到的样本也是偏的。所以遇到长肥管道,人工介入是有必要的。

缓冲区不是调得越大越好:自动调优与内存占用的取舍

既然接收窗口由接收缓冲区决定,那把缓冲区调到很大不就行了?这条路走得通一半,坑在另一半。

第一坑在内存。socket 的收发缓冲区是要占物理内存的,而且不是按「已用」算,是按「预留」算。缓冲区开到 4MB,一条连接在收发两个方向上就要占掉 8MB 量级的内存;一台机器上几千条连接,这个总量就不是可以忽略的了。长连接服务尤其如此,连接建起来就占着,断开了才还。把缓冲区一把拉满,最常见的后果不是变快,而是内存曲线抬上去,然后开始出现别的压力。

第二坑在自动调优被关掉。在应用程序里显式调用 setsockopt 设置 SO_RCVBUF 或 SO_SNDBUF,通常会把这个 socket 的自动调优关掉,从此它就固定在你设的那个值上,不再随连接表现动态调整。设大了浪费内存,设小了成为新的天花板——而你可能根本意识不到自己把自动调优关了。更稳妥的做法是调内核层面的 rmem 与 wmem 上限范围,让自动调优在这个范围内自己去伸缩,而不是在应用里写死一个值。

第三坑是最容易被跳过的:接收缓冲区大,不等于应用程序读得快。接收窗口反映的是缓冲区里的剩余空间,如果应用端读取不及时,数据堆在缓冲区里,剩余空间就会变小,窗口跟着往下掉。这种情况下你把缓冲区调得再大,也只是让堆积过程慢一点,最终的窗口还是被应用的读取速度压住。所以看到窗口上不去,先确认应用是不是在老老实实读数据——比如是不是卡在了业务逻辑上,是不是单线程处理不过来。

第四坑才回到前面那条规则:窗口取的是拥塞窗口和接收窗口里更小的那个。接收这边开到 8MB,拥塞窗口因为算法保守只肯长到 512KB,那实际还是 512KB。这也是为什么调缓冲区经常「没什么感觉」——你动的不是那一头。

CUBIC 把丢包当拥塞信号:长肥管道上为什么恢复慢

拥塞窗口这一头归拥塞控制算法管,而主流 Linux 发行版长期以来的默认是 CUBIC。要理解它在长时延链路上为什么难受,得先看它的基本逻辑。

CUBIC 的核心假设是:丢包等于拥塞。发送端把窗口往上抬,抬到某个点开始丢包,就判定「到顶了」,把窗口砍下来,再重新开始往上抬。这个模型在早年的网络里是成立的——那时候路由器缓冲区小,链路上真的没多少余量,一旦排队排不下就会丢包,丢包确实就是拥塞的信号。顺着这套模型设计出来的算法,行为特征就是著名的锯齿形:慢慢涨,忽然掉,再慢慢涨。

问题出在长肥管道上——就是那种带宽大、往返时延长的链路。这里有两个麻烦叠在一起。第一个麻烦是窗口恢复的速度跟往返时延绑定:窗口是靠「收到一个确认就往前推一点」往上长的,确认多久回来一次取决于往返时延。往返时延 20 毫秒和 200 毫秒,前者的窗口增长速度是后者的十倍量级。同样是丢一次包,短时延链路上零点几秒就爬回去了,长时延链路上可能要十几秒甚至更久,而在这段时间里你一直在低速跑。

第二个麻烦是长肥管道上的丢包不一定意味着拥塞。链路长、中间环节多,偶尔的误码、无线段的抖动、某个中间设备的瞬时队列溢出,都会造成少量丢包。这些丢包跟「这条链路已经饱和了」是两回事,但 CUBIC 分不清——它看到丢包就砍窗口。于是一个 100Mbps、往返时延 200 毫秒的链路,只要有零星丢包,窗口就长期被压在很低的水平上,永远爬不到 BDP 要求的那 2.5MB。表现就是:带宽明明是够的,速度就是上不去,而且看起来「很稳定地慢」。

这时候你去做丢包观测,会发现丢包率并不高——低到让人觉得「这么点丢包不该这么慢」。这个直觉在长肥管道上是错的:在长时延链路上,CUBIC 对少量丢包的容忍度远低于短时延链路,因为恢复成本被往返时延放大了。这也是后面 BBR 要换一套建模方式的直接动因。

队列太深也会坏事:bufferbloat 把 RTT 拉起来之后

还有一个跟丢包方向相反、但同样致命的问题:队列太深。

为了少丢包,很多网络设备的缓冲区做得很大。想法是好的——包来了排个队,慢慢发,别丢。但在 TCP 的语境下,这个「好意」会变成一个陷阱。发送端发现没有丢包,就认为还没到顶,继续抬窗口;数据越来越多地堆在队列里,队列排得越满,包在里面等待的时间就越长;等待时间变长,就是往返时延变大。

于是出现一个恶性循环:窗口抬上去 → 队列排满 → 往返时延被拉高 → 而吞吐等于窗口除以往返时延,分母变大了 → 就算窗口真的抬上去了,吞吐也未必涨,甚至可能因为分母涨得更快反而下降。同时,被拉高的往返时延还会让拥塞控制对丢包的反应变得更迟钝、恢复更慢。这种现象有个专门的名字,叫 bufferbloat,中文一般译作缓冲膨胀。它的典型症状是:带宽看着跑满了,但交互类的操作卡得要命,同机上的短连接请求排队等半天。

队列调度(qdisc)在这里起作用。Linux 上常见的两个选择是 fq 与 fq_codel。fq 大致做的是把不同连接的数据流分开排队、轮流发送,避免一条大流量的连接把整个队列占满、把别的连接饿死;fq_codel 在公平排队的基础上加了主动的队列管理,它盯着包在队列里待了多久,超过阈值就主动丢弃或标记,用「早点丢」换「队列别排满」,从而把时延压住。两者解决的都不是「怎么让带宽更大」,而是「怎么让队列别把时延顶上去」。这一点经常被误解——它们不提速,它们防止时延恶化,间接地让吞吐的分母稳定下来。

顺带一提,BBR 在 Linux 上一般建议配合 fq 使用,就是因为 BBR 的发送节奏需要队列调度配合,队列太深会干扰它对瓶颈带宽和最小往返时延的估计。可用性上,这两个队列调度器在较新的主流发行版内核里通常都已经编进去了,具体有没有、当前用的是哪个,用 tc qdisc 相关命令看一眼就清楚,别凭印象判断。

BBR 换了套建模方式:它看的是瓶颈带宽和最小 RTT

BBR 的思路跟 CUBIC 完全不同。它不再把丢包当作主要信号,而是去直接估两个量:这条路径上的瓶颈带宽是多少,以及这条路径的最小往返时延是多少。琢磨一下就会发现,这两个量正好就是算 BDP 需要的那两个输入——BBR 本质上是在反过来推这条链路的 BDP,然后按这个 BDP 去设置发送量。

瓶颈带宽怎么估?靠观测发送速率和确认回来的节奏:一段时间内发出了多少数据、这些数据的确认在多长时间内回来,两者相除就得到一个交付速率的估计,取一段时间里的最大值当作瓶颈带宽。最小往返时延怎么估?持续测量每个包的往返时间,取一段时间内观测到的最小值——之所以取最小,是因为最小值对应的是「队列没排队、链路空着」的时刻,排了队的测量值会偏大,不能代表链路的真实传播时延。BBR 会周期性地主动降速一下再探一次,就是为了重新确认最小往返时延有没有变化,否则一旦它估错了,后面所有计算都会偏。

这么做的直接好处非常明显:既然不把丢包当主要信号,那么在有一定丢包、往返时延较长的链路上,BBR 不会像 CUBIC 那样一丢包就把窗口砍下来再慢慢爬。只要它估出来的瓶颈带宽和最小往返时延是准的,它就能把发送量维持在接近 BDP 的水平上。这也是为什么在跨境、长时延、偶有丢包的链路上,换成 BBR 常常能拿到高得多的吞吐——不是带宽变大了,是终于把窗口用到位了。

但这里必须把前提讲明白,否则就成了玄学。BBR 的估计依赖于「瓶颈带宽」和「最小 RTT」这两个量在一段时间内是相对稳定的,它靠周期性探测来跟踪变化。如果这条链路的时延本身频繁大幅波动、或者带宽被上游以某种方式频繁调整,那么它的估计就会一直在追,收敛需要时间,效果也就没那么确定。这就是为什么「换 BBR 能快多少」这个问题没有通用答案——它取决于你那条链路的这两个量稳不稳定,只能在你自己的链路上做前后对比。

BBR 不是万能的:前提、代价和几个还没定论的争议

把 BBR 当成万能开关是不行的,它有明确的前提和明确的代价。

第一,内核版本决定了你能用什么。BBR 进入 Linux 主线内核是在 4.9 版本,低于这个版本就没有;而后续版本里的 BBR 实现还有过若干次修正与调整。至于 BBR 的第二版、第三版,它们的推进与合入状态在不同发行版之间并不一致,能不能用、叫什么名字,得看你手上这个内核的实际情况,别照抄别人文章里的模块名。查法很简单:看当前内核版本,再列出系统里可用的拥塞控制算法列表,列表里有什么就是什么。

第二,它对队列和限速方式敏感。前面说过,BBR 在 Linux 上一般配 fq 使用。如果队列调度没跟上,或者链路上存在某种令牌桶式的限速、某种整形策略,BBR 估出来的瓶颈带宽就可能跟真实情况对不上,表现反而不如原来。所以换算法之前,先看一眼当前用的是哪个队列调度,这一步不该跳。

第三,公平性是有争议的。BBR 和 CUBIC 混在同一条瓶颈链路上时,两者抢带宽的行为方式不同,谁占得多、是否公平,在社区里讨论了很多年,并没有一个能让所有人点头的结论。对运营者来说,这个争议的实际含义是:你在自己的机器上换成 BBR,可能会影响到同一链路上其他流量的分配;反过来,你对端的算法是什么,也会影响你这边能看到的效果。这属于「需要在自己的环境里验证」的事项,不适合照抄结论。

第四,往返时延频繁变化的场景要自己验。BBR 靠一段时间内观测到的最小往返时延来判断链路的传播时延,如果往返时延本身剧烈波动,或者路径发生切换导致真实传播时延变了,它需要时间重新收敛。在这个过渡期里,表现可能不如预期。这类场景包括路径不稳定的移动网络、频繁切换的链路等——不是说它一定差,而是说结论必须由你自己测出来。

第五,它是个需要观测的调整,不是一劳永逸的开关。换完之后要看的东西很明确:实际的吞吐有没有变、往返时延有没有被顶起来、重传率有没有变化。只看「快没快」是不够的,因为吞吐涨了但同时把队列顶满、把时延拉爆,对交互类业务是灾难。任何参数改动都要在自己的链路上做前后对比,不要照抄网上的参数表——那些参数是别人在别人的链路上、别人的流量模型下调出来的,对你大概率不适用。

排查顺序:先算 BDP,再看单连接,再谈算法,最后才是加带宽

把上面的东西串起来,就是一个可以照着走的排查顺序。顺序本身比每一步的细节更重要,因为走错了顺序,你会把钱花在没用的地方。

第一步,算 BDP,判断这是不是窗口问题。测出这条链路的往返时延,乘上你的带宽,除以 8,得到需要的窗口量级。然后去看系统当前的窗口与缓冲区设置。两个数一比:如果实际窗口远小于 BDP,那基本可以断定是窗口或者算法的问题,带宽不是瓶颈,加带宽没有意义。

第二步,用「单连接 vs 多连接」这个对比来确诊。单条连接跑不满、开多条连接之后总吞吐明显上去了,甚至接近标称带宽——这就是单连接的窗口限制,诊断到此已经明确。该做的是调窗口、开多流、或者换算法,而不是去买更大的带宽。反过来,如果多连接也跑不满,总吞吐卡在某个值上不动,那说明瓶颈在别处,继续往下走。

第三步,多连接也跑不满时,查丢包、队列和对端。看重传情况,看队列有没有排满把往返时延顶上去,看对端是不是有能力或者策略上的限制——比如对端的应用读取慢、对端的上游做了限速。到这里还要把问题分段定位:排队和丢包可能发生在本机(队列调度、网卡 ring buffer、CPU 软中断打满)、接入这一段(上联口、本机防火墙或 NAT 的处理能力)、中间的跨境段(这一段的行为受很多因素影响,不要凭印象下结论)、也可能发生在对端(对端机器、对端的上游)。每一段都要用自己能观测到的证据去判断,不要在没有数据的情况下猜是哪一段。

第四步,才轮到算法与内核参数。先确认内核版本支持什么算法、当前用的是哪个队列调度,再决定要不要换、换成什么。换之前留一份基线的观测数据,换之后对比同样的数据。没有前后对比的调整等于没调。另外,动参数的顺序建议是:先动窗口和缓冲区这一类「把天花板抬高」的动作,再考虑换算法——因为算法解决的是「愿不愿意长到那么大」的问题,天花板不抬起来,算法再激进也没地方长。

第五步,加带宽。走到这一步的前提是:窗口开够了、算法换过了、多连接也试过了、丢包和队列都排除了,总吞吐仍然稳定地贴着带宽上限。只有到了这里,加带宽才是有效的动作。而现实是,绝大多数「带宽跑不满」的案子,走不到第五步就已经解决了。

不同链路场景下的 BDP 与所需窗口量级

下面这张表把常见的几种场景按公式换算了一遍。用法是:找到跟你最接近的那一行,看「需要的窗口量级」这一列,拿它跟你自己机器上实际的窗口对比。再强调一次,表里的数字全部是按 BDP 公式换算出来的量级,用于估算和判断,不是任何实测结果,实际数值以你自己在自己链路上的测试为准。

链路场景 带宽 × 往返时延 需要的窗口量级 窗口不足时的吞吐估算 优先动作
同机房或同城内网互传 1Gbps × 1ms 量级 约 125KB 量级 默认窗口基本够用,吞吐接近标称值 先确认有没有限速策略,不要动窗口
国内跨省、时延较短 100Mbps × 30ms 量级 约 375KB 量级 窗口仅 64KB 时约 2MB/s(约 17Mbps)量级 开窗口缩放与自动调优,检查 rmem/wmem 上限
跨境、时延中等 100Mbps × 100ms 量级 约 1.25MB 量级 窗口仅 256KB 时约 2.5MB/s(约 20Mbps)量级 抬高接收缓冲区上限,评估多流并发
跨境、时延较长(开篇场景) 100Mbps × 200ms 量级 约 2.5MB 量级 窗口仅 256KB 时约 1.3MB/s(约 10Mbps)量级 先算 BDP 定尺子,再动窗口、多流与算法
跨境长时延且有零星丢包 100Mbps × 300ms 量级 约 3.75MB 量级 窗口 512KB 时约 1.7MB/s 量级,遇丢包还会再降 确认内核支持后评估 BBR,并配好 fq 类队列调度
大带宽叠加长时延 1Gbps × 200ms 量级 约 25MB 量级 窗口仅 2MB 时约 10MB/s(约 80Mbps)量级 必开大窗口加多流,同时核算 socket 缓冲区内存占用

避坑:带宽跑不满时最容易做错的四件事

第一坑:不看窗口直接加带宽。这是最贵也最常见的一坑。为什么坑:在长时延链路上,单连接吞吐由窗口除以往返时延决定,跟标称带宽无关;窗口没开够的时候把带宽从 100M 加到 500M,那条连接该跑多少还是多少,你只是多付了一份钱。怎么避:动手之前先算一次 BDP,把需要的窗口量级和实际的窗口数值摆在一起对比,两个数差着一个数量级,加带宽就一定无效。

第二坑:拿单连接的测试结果去判断整条链路。为什么坑:单条连接被窗口限住不代表链路被限住,你用单线程测出来的「3MB/s」,很可能开四条并发就能到 12MB/s——这两个数说的是两件不同的事。怎么避:测的时候同时做单连接和多连接两组,两组结果的差别本身就是最重要的诊断信息。单连接慢、多连接快,指向窗口;两组都慢,才指向链路本身。

第三坑:照抄网上的内核参数表。为什么坑:那些参数是在特定的带宽、时延、丢包和流量模型下调出来的,换一条链路就不成立;而且有些参数之间存在相互影响,单独抄一个进来可能带来副作用——比如把缓冲区调得很大,既占内存又可能关掉自动调优。怎么避:任何改动都遵循「先记基线、改一项、再对比」的流程,一次只动一个变量,对比同样时间段、同样对端的数据,看到明确改善再保留,看不出改善就回滚。

第四坑:只看吞吐,不看时延和重传。为什么坑:吞吐涨了但同时把队列顶满,往返时延被拉起来,交互类请求全部变卡,整体的业务体验反而下降;重传率飙升也是同理,字节是发出去了,但一大半是白发的。怎么避:换算法或调队列之后,同时看三个量——实际吞吐、往返时延、重传情况。三者一起改善才算真的改善,只涨吞吐而时延和重传恶化,那多半是把压力换了个地方,不是解决了问题。

跨境带宽跑不满的时候,最该先查的几个问题

合同写的 100M,跑到 3MB/s,是不是运营商没给够带宽?不一定,而且多数情况不是。先把 3MB/s 折成比特率,大概 24Mbps 量级,然后算这条链路的 BDP:如果往返时延是 200 毫秒,跑满 100M 需要 2.5MB 量级的窗口,而 24Mbps 对应的窗口只有 600KB 上下——差了四倍,正好对得上。所以更可能的解释是窗口没开够,不是带宽没给够。要确诊很简单:开四条并发再测一次,如果总吞吐明显上去了,那就是单连接窗口的问题,跟运营商给的带宽无关。

往返时延该怎么测,用 ping 可以吗?可以用,但要注意它测的是什么。ping 走的是 ICMP,测出来的是到那个地址的往返时间,作为链路往返时延的参考是够用的,而且它不建立 TCP 连接,反映的是「链路空着的时候」的时延——这一点恰好跟 BDP 计算需要的口径比较接近。但 ICMP 和 TCP 在中间设备上的处理优先级可能不一样,拥塞时两者的表现会有差别。更贴近实际的办法是在 TCP 连接上直接读内核观测到的往返时延,用 ss -i 这类工具就能看到每条连接的 rtt 与 cwnd。测的时候多采一段时间取稳定值,别抓一个瞬时值就用。

怎么知道我现在用的是哪个拥塞控制算法、窗口有多大?算法方面,看内核参数里当前生效的拥塞控制算法是哪个,同时列出系统支持的算法清单,两相对照就知道能不能换。窗口方面,用 ss -i 看具体连接,它会给出拥塞窗口、接收窗口以及内核观测到的往返时延,这几个数凑起来就能跟 BDP 对比。队列调度用 tc qdisc 相关命令看。这些命令的输出格式在不同内核版本上略有差别,按你自己机器上的实际输出去读。

把接收缓冲区调到很大就能跑满吗?不一定,而且经常不能。原因有两条。第一条是前面反复讲的那条规则:实际能发的量取拥塞窗口和接收窗口里更小的那个,接收这边开到 8MB,拥塞窗口因为算法保守只肯长到 512KB,那实际还是 512KB。第二条是缓冲区调大本身有代价:它按预留占内存,连接一多总量可观;在应用里写死 SO_RCVBUF 还会关掉这个 socket 的自动调优。更合理的做法是提高内核层面 rmem 与 wmem 的上限,让自动调优在更大的范围内伸缩。

开了多条连接速度就上去了,是不是说明多开线程总是对的?能上去说明瓶颈确实是单连接的窗口,多开是对的。但要明白它的本质:每条连接各自有一个窗口,开 N 条就是把窗口的上限乘以 N,这是绕开限制,不是消除限制。代价也要算:连接多了,两端的内存和 CPU 开销都上去,每条连接都要独立走一遍慢启动,对端如果做了每连接的限速,多开也未必有效。而且多开会把队列占得更满,可能加剧时延问题。所以多流是个有效的手段,但不是「越多越好」,开多少条要用自己的数据去定。

换了 BBR 就一定更快吗?不一定。BBR 的优势场景是「有一定丢包、往返时延较长」——它不把丢包当主要信号,所以在 CUBIC 被零星丢包反复打压的链路上常常能拿到更高吞吐。但它有前提:需要较新的内核支持、需要合适的队列调度配合、它对限速方式敏感、跟 CUBIC 混跑存在公平性争议、在往返时延频繁变化的场景要自己验证。如果你的链路时延短、本身没什么丢包,窗口也开够了,那换过去可能看不出任何差别。结论只能来自你自己的前后对比。

排队和丢包到底发生在哪一段,怎么定位?把链路拆成四段去看:本机、接入这一段、中间的跨境段、对端。本机这一段最容易查——队列调度、网卡队列、软中断是不是打满了 CPU,都能在本机上观测到。接入和跨境段能做的观测有限,通常用分段测时延的方式来看时延是在哪一段被抬起来的,不要在没有数据的情况下断言是某一段的问题。对端这一头最容易被忽略:对端应用的读取速度、对端机器的窗口设置、对端的上游限速策略,都会让你这边跑不满。定位的顺序建议是从两端往中间推,两端都排除了,再看中间。

选型上,想跑满带宽应该优先看机器的什么?三样东西。第一是内核版本,它直接决定你能用哪些拥塞控制算法、能用哪些队列调度——版本太旧,BBR 和 fq 这一套根本没得用,后面所有调优都无从谈起。第二是网卡与队列,多队列网卡能把软中断分散到多个核上,避免单核打满成为新的瓶颈,这一项在大带宽机器上尤其重要。第三是内存,socket 收发缓冲区是按预留占内存的,你要开大窗口就得预留出这部分,连接数乘上单连接的缓冲区大小,就是这台机器需要的内存量级。把这三样按这个顺序排,再去横向比选机型,比单纯盯着带宽数字有意义得多。

带宽跑不满的时候,先算账再动手

回到开头那个数字:合同 100M,实际 3MB/s。我的立场很明确——这种情况下第一步永远是算一次带宽时延积,不是提交一份加带宽的申请。BDP 会直接告诉你这条链路需要多大的窗口,拿它跟实际窗口一比,问题是不是出在窗口和算法上,一眼就能看出来。接下来的诊断更简单:单连接慢而多连接快,就是单连接的窗口限制,该做的是调窗口、开多流、换算法;多连接也慢,才轮到查丢包、查队列、查对端。加带宽是这一串动作的最后一步,只有在窗口开够了、算法试过了、多流也上了、丢包和队列都排除了、吞吐仍然贴着上限的时候,它才是有效的。反过来,顺序走错了就是纯烧钱:带宽从 100M 加到 500M,那条连接该跑 3MB/s 还是 3MB/s。换算法这件事也别神化,BBR 估的是瓶颈带宽和最小往返时延,在长时延加零星丢包的链路上常常有优势,但它需要新内核、依赖队列调度、对限速方式敏感、跟 CUBIC 混跑的公平性还有争议,而且任何参数改动都必须在你自己的链路上做前后对比,照抄别人的参数表是最省事也最容易出事的做法。真到了要选机器那一步,按内核版本、网卡队列、内存这个顺序去比——像一万网络这样深耕 IDC 19 年(成立于 2007 年)的服务商,机型档位覆盖比较全,可以拿这三项去横向比选,具体机型与价格需实时询价,以签约时的最新报价为准。

本文关于 TCP 窗口、带宽时延积与拥塞控制算法的技术出处

本文涉及的 TCP 窗口机制、窗口缩放选项与「发送端未确认数据量受接收窗口与拥塞窗口共同限制」这一规则,出自 IETF 关于 TCP 窗口缩放与拥塞控制的 RFC 文档;带宽时延积的定义与「单连接吞吐上限约等于窗口除以往返时延」这一关系,出自 TCP 拥塞控制与长肥管道相关的 RFC 文档与经典教材;CUBIC 算法以丢包为主要拥塞信号及其在长往返时延链路上恢复较慢的特性,出自 CUBIC 算法的相关论文与 RFC 文档;缓冲膨胀的成因与 fq、fq_codel 一类队列调度器的设计目标,出自 CoDel 与 FQ-CoDel 的相关论文以及 Linux 内核关于队列调度的文档;BBR 通过估计瓶颈带宽与最小往返时延来建模、不把丢包作为主要信号的思路,出自 Google 发表的 BBR 相关论文与公开技术资料;BBR 进入 Linux 主线内核的版本信息,出自内核的官方变更记录;Linux 下 socket 缓冲区自动调优、rmem 与 wmem 相关参数的说明,出自内核官方文档中关于网络参数的说明部分。文中所有数字均为按公式换算的量级估算,用于说明判断方法,并非实测结果,实际数值以你在自己链路上的测试为准;所述场景均为典型部署思路,并非特指某一真实客户案例。涉及机型、配置与价格的部分需向服务商确认,具体以实时询价与签约时的最新报价为准。更多行业技术内容可参见 https://www.idc10000.net/ 。


上一篇:给数据盘加静态加密之前先想清楚密钥丢了谁来救:性能代价远没有救援流程致命

下一篇:CPU 才用了两成接口却慢了十倍:Node 服务的瓶颈多半不在 CPU,在事件循环被堵住