聊到TCP拥塞控制很多人的第一反应是“哦就是慢启动嘛”。可真在业务线上被折磨过几次就会明白拥塞控制这套机制的设计深度和踩坑复杂度远不是背几个术语就能搞定的。它直接决定了你的长肥链路能不能跑到带宽上限、决定了弱网场景下视频会不会卡成幻灯片、决定了机房里的海量连接会不会把彼此带宽活活挤爆。我印象很深的一次是调一个跨地域文件传输任务业务方反复反馈“带宽明明还有富余但传输速度就是上不去”。tcpdump抓包看了一圈全是Dup ACK和重传根源就是传统的丢包反馈式拥塞控制算法在高效能链路上表现欠佳。后来调整了初始拥塞窗口、试了BBR、再配合BBR的 pacing 参数同一台机器、同一段链路传输耗时直接降了四成。那次之后我对拥塞控制的认知才算真正落地。这篇东西想做的就是把TCP拥塞控制从原理到实操完整串一遍它到底在解决什么问题、业内有哪些重量级算法、Linux上怎么真实地开启和调参、又怎么用tcpdump加Wireshark确认你的调优有没有效果。适合刚学完TCP/IP基础的学生也适合做传输优化、视频调度、网关开发、嵌入式协议栈的工程师参考。我会尽量少用教科书式的说法尽量把每一步背后的“为什么”也讲清楚。1. 拥塞控制的本质它到底在管什么1.1 拥塞的根源网络不是无限带宽的水管先说一个最容易被新手忽略的事实TCP的发送方并不知道网络路径上有多少带宽可用更不知道瓶颈路由器现在排队了多少数据包。它唯一能依赖的信号是什么ACK。发出去的包被确认了说明路是通的收不到ACK或者收到重复的ACK说明大概率丢了包。但“丢包”这件事本身有很多种可能链路误码、路由器队列溢出、接收端缓冲区不够。拥塞控制要回答的问题就一句话我该用多大的速率去发数据才能既不压垮网络又不浪费带宽这里必须区分两个容易混淆的概念流量控制和拥塞控制。流量控制是接收端告诉发送端“你慢点我来不及收了”通过接收窗口rwnd实现是点对点的。拥塞控制是发送端自己猜测“网络上现在别的主机可能也在抢路我得收敛一下”通过拥塞窗口cwnd实现是整个网络状态的隐式感知。两者是独立叠加的关系最终的发送速率取决于 cwnd 和 rwnd 中较小的那个。可以这样理解你开车上班路况就相当于网络。流量控制是你副驾上的人不停说“开慢点我晕车”拥塞控制则是你自己根据前方堵不堵车决定油门深浅。前者是内部协商后者是对外部环境的自适应。如果只认流量控制而不做拥塞控制就好比副驾不晕车就一路地板油——市中心不堵你才怪。1.2 拥塞窗口、RTT和带宽延迟积一个铁三角拥塞控制的直接产物就是拥塞窗口 cwnd它表示“在没有收到确认之前最多可以发出多少个数据包”。cwnd 越大同一时刻在网络上的在途数据越多链路利用率就越高。但 cwnd 不是越大越好因为网络里能容忍的在途数据总量是有限制的这个上限就是带宽延迟积BDP, Bandwidth-Delay Product。BDP 的计算公式很朴素BDP 瓶颈带宽 × 往返时延(RTT)。打个比方一条管子每秒能流过 10 升水你在管子这头喊话到那头听到回声需要 2 秒那管子里最多同时存在的水量就是 20 升。如果 cwnd 换算成字节数小于 BDP链路就“吃不饱”带宽被白白浪费如果 cwnd 远大于 BDP多余的数据包只能在路由器队列里堆积排队时延暴涨甚至直接溢出丢包。传统拥塞控制的一个核心痛点就在这里它只有一个数字 cwnd却想同时优化两个目标——填满 BDP 和避免排队。这本质上是矛盾的。你要么选择激进地增长 cwnd忍受排队和丢包要么选择保守地逼近带宽牺牲部分初始吞吐。理解了这个铁三角后面看CUBIC和BBR的取舍就会很清楚CUBIC 本质上在用丢包判断是否逼近了 BDPBBR 则试图显式地估算 BDP 本身。1.3 拥塞控制评价的三个维度效率、公平、稳定为什么拥塞控制算法有那么多代际演进因为评价一处算法好不好业内基本看三个维度。第一是效率utilization)能否快速、稳定地利用可用带宽。慢启动阶段按指数增加 cwnd就是为了“快”。但指数增长快过头了就会瞬间打爆瓶颈队列。第二是公平性fairness多个TCP流共享同一条瓶颈链路时每个流应该大致平均分配带宽。早期算法里RTT 短的连接更容易抢占带宽这是“RTT不公平”问题。第三是稳定性stabilitycwnd 不应该剧烈振荡。振荡带来时延抖动对音视频这类实时业务极不友好。这三者经常互相打架。追求效率就要激进增长容易丧失公平追求公平就要频繁探测可能引发振荡。从 Reno 到 CUBIC 再到 BBR设计者们本质上是在这三个维度之间寻找新的平衡点。我自己做优化时也始终带着这三个尺子去评估一个算法适不适合当前业务游戏长连接要稳定视频点播要高效率公有云出口则要公平。2. 核心算法拆解从经典四件套到BBR2.1 经典Reno/NewReno慢启动、拥塞避免、快速重传、快速恢复无论算法怎么演化慢启动Slow Start、拥塞避免Congestion Avoidance、快速重传Fast Retransmit、快速恢复Fast Recovery这四件事是所有现代实现的地基。先把它们啃透后面看新算法才不会迷失。慢启动的逻辑每次收到一个ACKcwnd 加 1 MSS最大报文段大小。因为ACK是逐段确认的等效下来 cwnd 每经过一个 RTT 翻倍。从 10 个 MSS 起步10 个 RTT 后就能涨到 5120 个 MSS这就是指数增长的威力。慢启动这个名字其实是历史的误会——它慢在起点快在过程。真正精巧的是拥塞避免阶段每经过一个 RTTcwnd 只加 1 MSS。为什么是线性因为此时已经逼近估算的链路容量再指数增长只会猛踩油门冲进拥堵区。快速重传和快速恢复是针对丢包信号的不同反应。早期TCP实现Tahoe一遇到丢包就回到慢启动cwnd 重置为 1这在随机丢包环境下非常吃亏。Reno/NewReno 的改进是发送方收到 3 个重复 ACK就认为某个包丢了但既然后续包还能到达接收端说明网络还没完全堵死于是 cwnd 减半而不是归零然后进入快速恢复阶段继续发送。NewReno 又修复了 Reno 在恢复期间每丢一个包就要再等一次超时的问题改为在一个恢复窗口内处理多个丢包。经典算法的核心假设是丢包等于拥塞。这个假设在早期互联网基本成立因为当时链路误码率低、瓶颈都在路由器队列。但到了无线网络时代信号波动、信道误码造成的丢包和拥塞丢包完全无法区分经典算法就力不从心了。这是我后面要讲 BB 的铺垫。2.2 CUBICLinux默认算法的另辟蹊径如果只记住一个现代算法那必然是 CUBIC。它是Linux内核自2.6.19以来的默认拥塞控制算法也是目前互联网上使用最广泛的实现。CUBIC 的聪明之处在于它不沿用 AIMD加法增乘性减的线性窗口增长而是用三次函数来描述 cwnd 随时间的演化。我把 CUBIC 的核心简化一下。假设发生丢包时 cwnd 被记为 WmaxCUBIC 会先快速恢复到接近 Wmax 的位置然后进入高原期plateau缓慢试探。三次函数的形状让 cwnd 先在 Wmax 附近“爬坡”一旦越过 Wmax就进入更激进的探测阶段尝试找到新的容量上限。这样的好处有两个一是和RTT解耦窗口增长主要依赖时间而非ACK到达速率短RTT连接不再碾压长RTT连接公平性提升二是在高带宽长延迟网络上它能比Reno更快地重新探满带宽。CUBIC 也不是没有短板。它的设计仍然依赖丢包作为拥塞信号所以面对随机丢包无线链路易丢包时仍然会被误伤。另外CUBIC 在队列占满之后才收敛这意味着它倾向于填满路由器缓冲区天然带来较高的排队时延。这也是为什么后来Google又推出了 BBR——它从根本上质疑了“丢包即拥塞”这个前提。2.3 BBR用模型替代信号绕开丢包陷阱BBRBottleneck Bandwidth and Round-trip propagation time是2016年Google开源并部署到内部网络和YouTube边缘节点上的算法。它的思路完全换了一个赛道不去被动响应丢包而是主动测量两个物理量——瓶颈带宽BtlBw和最小往返时延RTprop然后直接让发送速率逼近瓶颈带宽。BBR 的状态机分为四个阶段Startup启动阶段类似慢启动但更激进以2倍速率增长、Drain排空阶段把启动时堆积在队列里的多余包发完、ProbeBW带宽探测阶段周期性以 1.25 倍速率探测是否有了更多可用带宽、ProbeRTT时延探测阶段定期把 cwnd 收缩一下刷新最小RTT。它不再是“加性增、乘性减”的窗口调整而是显式地用 Pacing 速率去控制发包节奏。BBR 的实际效果我自己的测速数据是在丢包率1%的模拟链路下传统CUBIC吞吐可能掉到带宽的30%BBR 依然能维持90%左右。因为BBR 不把丢包当成必须退避的信号它认为丢包后只要瓶颈带宽还在就应该继续按 BtlBw 发送。这种设计特别适合移动网络、卫星链路、跨洋长肥网络。代价是BBR 容易比 CUBIC 抢占更多带宽和传统流共存时公平性差点意思而且对于浅队列瓶颈它也会有排队时延偏大的情况。所以生产环境里要不要上 BBR需要实测不能只看网上评测。2.4 算法选型对照谁的场景适合谁算法拥塞信号窗口增长方式适合场景主要局限Reno/NewReno丢包加性增、乘性减低丢包有线网络高BDP链路利用率低、RTT不公平CUBIC丢包三次函数时间依赖Linux默认、通用互联网依赖丢包信号队列占用偏高BBR带宽与时延模型速率跟踪高丢包、长Fat链路、移动网络公平性弱、浅队列场景可能排队BBRv2带宽时延丢包模型适度退让数据中心、混合业务参数多需实测调优Westwood带宽估计根据ACK速率调整无线链路、大带宽时延兼容性一般内核支持少选型的经验之谈如果不是做传输优化或音视频调度默认 CUBIC 别乱动如果发现高丢包率导致吞吐上不去先试 BBR但要在隔离环境里测试对现有业务的互斥影响数据中心内部则可以考虑 DCTCP 这类依赖ECN的算法但它对交换机配置有要求普通场景用不上。3. 实操Linux下开启、观测与调优拥塞控制3.1 查看和切换当前拥塞控制算法现代Linux内核把所有算法做成了可加载模块。先看当前系统支持哪些、正在用哪个# 查看当前生效的算法输出类似 cubic sysctl net.ipv4.tcp_congestion_control # 查看内核编译支持的所有算法 sysctl net.ipv4.tcp_available_congestion_control # 查看模块是否加载输出类似 tcp_bbr lsmod | grep tcp_bbr如果输出里没有 bbr需要先加载模块再切换modprobe tcp_bbr sysctl -w net.ipv4.tcp_congestion_controlbbr想要永久生效在/etc/sysctl.conf或/etc/sysctl.d/下加一行net.ipv4.tcp_congestion_controlbbr然后sysctl -p重载。需要注意这个 sysctl 改的是全局默认值只对新建TCP连接生效已经建立的连接不受影响。所以线上调整算法要配合连接迁移或滚动重启服务不能指望一条sysctl -w就让存量连接丝滑切换。3.2 用ss和netstat观察窗口与重传算法调没调对不能只靠感觉要有数据。最直接的观测入口是ss# 查看当前所有TCP连接的收发队列、cwnd和rtt ss -tin # 只看某个目标端口的连接详情 ss -tin sport :8080输出里会有cwnd:30、rtt:10.2、bytes_acked:xxx、retrans:0之类的字段。cwnd 是当前拥塞窗口大小rtt 是当前往返时延retrans 是重传次数。我平时看一个新连接从建起到下载稳定会连续打几秒ss -tin观察 cwnd 是否从初始值快速爬升、是否出现骤降。如果 cwnd 周期性出现“陡增后骤降”多半是撞到了拥塞避免和快速恢复交替工作如果 cwnd 长期趴在很小值不动上游大概率在随机丢包。netstat -s看的是累计统计适合看整体趋势netstat -s | grep -E segments retransmited|timeouts|fast retransmits|loss重传率大致可以用重传段数 / 发送段数估算。正常有线网络重传率在0.1%以下超过1%就要警惕无线链路可以放宽到3%左右如果重传率动辄5%以上那就是拥塞控制信号被严重污染需要重点排查。3.3 tcpdump抓包视角下的慢启动看实时数据不过瘾可以抓包。慢启动阶段最直观的观测方式就是看一个窗口内连续发送的包数量和ACK到达节奏。tcpdump -i eth0 -s 0 -w slowstart.pcap port 443抓完后用Wireshark打开在 Statistics - TCP Stream Graph - Time-Sequence Graph(Stevens) 里能看到典型的指数曲线序列号随时间呈锯齿状快速抬升。如果曲线出现一段平台期说明触发了快速恢复如果出现了竖直下落说明发生了重传。我之前排查一个“每次传输到30秒就卡住”的问题就是用这个图发现cwnd涨到65535后频繁触发快速重传后来把接收缓冲区调大才解决。值得提醒的是Wireshark 里的 TCP 分析基于抓包点观测会受抓包位置影响。比如你在发送端抓包看到的是发送侧窗口变化在接收端抓包看到的是接收侧窗口和乱序情况。要分析拥塞控制最好在发送端抓因为 cwnd 是发送侧的状态接收端是看不到的。3.4 用tc模拟丢包和延迟验证调优效果这步是我每次调拥塞控制必经的实验环节。在没有条件做真实长链路测试时可以用 Linux 自带的tc在本地制造一个可控的劣化网络环境。# 在 eth0 出方向模拟 50ms 延迟 0.1% 随机丢包 tc qdisc add dev eth0 root netem delay 50ms loss 0.1% # 查看规则 tc qdisc show dev eth0 # 清理规则 tc qdisc del dev eth0 root注意tc的 netem 丢包默认是均匀随机丢包和真实路由器队列溢出的“突发丢包”不完全一样所以它适合验证算法的鲁棒性不适合精确模拟拥塞场景。想要模拟更真实的拥塞可以再加一个rate限速例如tc qdisc add dev eth0 root handle 1: tbf rate 10mbit burst 32kbit latency 400ms然后用 iperf3 灌流量测吞吐。我在测试 CUBIC 和 BBR 时常用的命令是# 服务端 iperf3 -s -p 5201 # 客户端开4条流测30秒打满带宽 iperf3 -c 127.0.0.1 -p 5201 -t 30 -P 4对比策略很简单先在干净链路分别测两个算法的基准吞吐再丢包率加到1%、5%分别再测。最终选哪个算法以你业务的实际流量模型为准不要只看单流带宽值。如果是RPC类短连接BBR 的收益很有限反而可能因为 Pacing 参数导致连接建立后的前几百毫秒吞吐爬升偏慢。4. 常见问题与排查技巧实录4.1 带宽明明富余但吞吐上不去怎么办这是群里被问烂的问题。一台10Gbps网卡的服务器跑单TCP流死活只有2GbpsCPU和网卡都不饱和。我排查的套路是固定的四步。第一步看窗口限制用ss -tin看 cwnd 和 rwnd算一下理论吞吐min(cwnd, rwnd) / RTT是多少。如果 cwnd 离 BDP 差很远说明增长受限如果 cwnd 涨到很大但实际吞吐还是低查接收窗口是不是被协商死了。第二步看丢包和重传netstat -s找重传率如果重传高大概率是链路质量问题或者中间设备在丢包。第三步看延迟ping和 MTR 看有没有连续高延迟路由器队列是不是已满。第四步换算法把 CUBIC 切到 BBR 试跑一轮 iperf3如果吞吐立刻上来说明瓶颈就是丢包反馈型算法的局限。我见过一个典型case同机房两台机器互传走内网延迟0.5ms但业务总是不到百兆。一看ss -tincwnd 只有百来KB而 BDP 算下来得有几十MB问题出在默认初始窗口太小加上慢启动被频繁丢包打断。把初始拥塞窗口调大并启用tcp_slow_start_after_idle0避免空闲连接重新慢启动问题直接解决。4.2 初始窗口设多大才算合理传统TCP初始窗口是10个MSS约14KB左右老内核也有按4个MSS实现的。对网页和小文件传输这个值偏小往往一次RTT不够传完。Linux 提供了ip route级别的初始窗口设置可以按目标网段差异化配置# 设置到目标10.0.0.0/8网段的初始窗口为64个MSS对应factor 64 ip route change 10.0.0.0/8 dev eth0 initcwnd 64 # 查看路由附带的初始窗口 ip route show这个initcwnd的单位是“MSS的倍数”64 意味着初始窗口是 64 × 1460 ≈ 93KB。我经验上对外部互联网用户初始窗口设 64 是安全的对数据中心内部的高带宽低延迟链路可以试 100 甚至 200。设太大有个风险如果瓶颈链路上的队列很浅瞬间会把队列打爆产生突发丢包。所以不要盲目追求大初始窗口要配合瓶颈带宽测试来选。顺带提一个容易被忽略的参数net.ipv4.tcp_slow_start_after_idle默认值是1表示连接空闲后再次发送数据时会重新走慢启动。对长连接但数据稀疏的业务比如MQTT心跳、数据库连接池探活应该把它改成0sysctl -w net.ipv4.tcp_slow_start_after_idle0否则每次空闲后发送都会被慢启动的指数增长拖累前几个RTT的吞吐。4.3 缓冲区溢出与接收窗口太小接收窗口太小的典型症状是接收端吞吐上不去抓包能看到接收端不断发送Window0或者窗口非常小的通告。此时需要检查接收端的 socket 缓冲区设置。# 查看当前socket缓冲区上下限 sysctl net.ipv4.tcp_rmem sysctl net.ipv4.tcp_wmem # 典型的“视频传输型”设置 sysctl -w net.ipv4.tcp_rmem4096 87380 16777216 sysctl -w net.ipv4.tcp_wmem4096 65536 16777216tcp_rmem的三列含义是最小、默认、最大。默认值决定每个 socket 初始分配大小最大值决定可自动增长上限。很多发行版默认最大只有4MB左右走不了高BDP链路。我一般会把最大缓冲设为16MB同时启用窗口缩放Linux默认开启net.ipv4.tcp_window_scaling1这样TCP可以在三次握手时协商出大窗口。核心思路你想要的吞吐如果超过默认缓冲允许的范围就先算一笔账所需缓冲 ≈ BDP比如RTT50ms、目标吞吐1GbpsBDP约为6.25MB缓冲至少按这个配。4.4 BBR的公平性与浅队列适配问题BBR 在实验环境里表现惊艳但上线之前有几个坑必须知道。第一个坑是公平性。BBR 流和 CUBIC 流混跑时BBR 会抢占大部分带宽。这是因为 CUBIC 撞到丢包就减半而 BBR 丢包后维持速率。如果你的业务跑到一个有多类流量共存的链路上直接启用 BBR 可能引发其他业务的投诉。第二个坑是浅队列。BBR 通过主动排空Drain来保持队列不堆积但在 CAKE/fq_codel 这类自身就在管理队列的路由器后面BBR 的带宽探测周期可能导致利用率波动。第三个坑是版本差异。内核版本不同BBR 的实现细节也在变比如 BBRv2 对丢包的响应更平和。线上启用前务必在目标内核上用 iperf3 和真实流量顺手压测一轮。我自己用 BBR 最多的是跨远端链路、有一定丢包率的场景。那种纯有线零丢包的低延迟内网CUBIC 的收益其实已经挺好BBR 未必是更优解还白白增加排查复杂度。4.5 常见问题速查表现象可能原因快速排查命令常用解法单流吞吐低于预期cwnd太小、rwnd太小ss -tin调大缓冲、调大initcwnd重传率持续偏高链路随机丢包、队列溢出netstat -s换BBR、降低Pacing速率、排查中间设备空闲后首包变慢慢启动重启sysctl tcp_slow_start_after_idle置为0短连接多但整体慢初始窗口太小ip route ... initcwnd调大initcwndcwnd周期性骤降快速恢复频繁触发Wireshark时序图增大缓冲、优化上游丢包延迟抖动剧烈路由器队列堆积tc qdisc查看排队时延用 fq_codel、启用BBR、限速多流之间互相抢带宽RTT不公平/公平性差iperf3 -P 并发测调整算法、按业务分队列5. 写在最后的几点体会调拥塞控制这几年我最大的感觉是不能把算法当黑盒也不能只看一两项测试指标就下结论。每种算法背后都是一个关于“网络是怎样的”的模型假设。Reno 假设丢包等于拥塞CUBIC 假设拥塞点在路由器缓冲队列BBR 假设瓶颈带宽相对稳定、丢包来源复杂。你的业务环境符合哪个假设哪个算法就好用。另一条经验是改任何参数前先摸清两个值——链路的真实瓶颈带宽和往返时延。这两个值决定了 BDP也就决定了理论上的传输上限。很多“调拥塞控制无效”的案例追到最后其实是物理链路的带宽根本不够算法再优化也突破不了上限。还有一个小技巧分享出来调优时不要只盯着吞吐一定要同时观测延迟和重传率。有时候吞吐上去了代价是排队时延涨了几十倍对实时业务反而更糟。我习惯在压测时同时记录“传输完成时间、平均RTT、最大RTT、重传率”这几个指标综合起来判断算法到底适不适合。如果哪天你也遇到“带宽明明很空TCP就是跑不满”的怪问题不妨按这篇文章的思路走一遍抓包、算BDP、调窗口、试算法。工具和思路都有了剩下就是耐心和数据。