
我前阵子在一个高延迟跨机房链路场景里把一台服务器的内核从默认拥塞控制算法切到了BBR带宽跑上去之后却发现同一条瓶颈链路上的其他业务延迟飙升个别传统TCP流甚至被压制到几乎“饿死”。这让我不得不重新审视一个问题BBR在单流场景下表现确实惊艳但多流共享瓶颈时他的公平性设计其实有不少可以推敲和优化的地方。TCP-BBR是谷歌在2016年提出的拥塞控制算法核心思路不再是“丢包即减速”而是通过主动探测链路的瓶颈带宽和最小往返时延建立一条带宽延迟积的发送管道模型让数据在管道内填得又快又满。近两年它在各类长肥网络、跨国专线和CDN节点上几乎是标配很多云厂商甚至直接把内核默认算法改成了BBR或BBRv2。但凡是把BBR放上生产环境的人多少都会遇到同一条链路上“新人吃饱、老人饿死”的情况——这就是BBR与传统基于丢包的拥塞控制算法比如CUBIC在公平性上打架导致的。这篇文章我打算从算法原理开始拆讲清楚BBR为什么会“抢带宽”以及我做的一个改进思路和完整实现。我会给出可运行的代码、关键参数的选择逻辑、以及实测过程中的排查记录。如果你正被BBR公平性问题困扰或者在做拥塞控制方面的调优、内核模块开发这篇应该能给你一些可以直接拿走的经验。1. 内容整体设计与思路拆解1.1 为什么BBR会破坏公平性要聊优化先得把“不公平”的病根找出来。BBR的带宽探测机制本质上是一个不断循环的增益周期它会在一个往返时延内把发送速率乘以一个大于1的系数默认1.25如果带宽没有明显变化就继续加大发送量直到探测到带宽上限再回退到稳定状态。这套机制的好处是不会像CUBIC那样在丢包时腰斩窗口坏处是它压根不给“丢包”这个信号留出退让空间。传统CUBIC算法在链路出现排队、延迟升高乃至轻微丢包时会主动降低自己的拥塞窗口把带宽让出来。而BBR的模型认为“丢包”只能作为修正信号而不是减速信号。于是当一个CUBIC流和一个BBR流共享同一条瓶颈链路时BBR持续以探测周期推高自己的发送量CUBIC则在轻微丢包下不断退让最终结果就是BBR抢占绝大部分带宽。这里有一个非常关键的概念就是“瓶颈带宽”和“实时可用带宽”的区别。BBR探到的是链路物理层的瓶颈带宽但物理带宽并不等于当前可用的公平带宽。两个流打架时每个流应该拿到的份额取决于它们的发送策略在一个反馈回路里是否能收敛到均衡点。BBR的增益周期一直在往极限试探这个“试探”在单流场景下是好事在多流场景下就是过冲。1.2 公平性冲突的本质是模型冲突实际上业界对BBR公平性的诟病主要集中在它和基于丢包的算法之间。BBR和BBR之间其实是相对公平的因为大家用同一套探测逻辑最终会各自收敛到带宽的一半左右。真正的冲突是“模型优先类算法”和“丢包反馈类算法”之间的冲突。我这次做的优化目标不是推翻BBR的模型而是给它加一个“自适应退让维度”在BBR探测到网络状态发生变化时不仅看带宽和延迟还要估计当前链路中的“竞争程度”如果竞争激烈就不再按固定的增益系数去抢带宽而是以更平滑的步长逼近公平份额。这里直接给出我的方案核心在原BBR的bbr_update_model里增加一个公平性因子该因子与当前的丢包率、排队延迟变化率、以及同一链路中估算出的流数量挂钩。当公平性因子低于阈值时BBR的增益系数从1.25降为1.0甚至0.95让出部分带宽给竞争者。这样既保留了BBR对带宽探测的主动性又不会让它在竞争场景下过于贪婪。1.3 方案选型背后的两个关键考量选这个思路之前我也对比过另外两个主流方向。一个是直接把BBR换成BBRv2或者统一使用某个混合算法改动最小但迁移成本和不可控因素多而且没有解决“和传统流共存”的问题。另一个是在用户态靠流量整形工具限速这种方式只能帮你自己管住自己对链路整体公平性没有任何改善遇到粗暴拉满带宽的对端依然会被欺负。最终选择内核模块内改BBR有三个原因。第一改动面可控核心逻辑集中在net/ipv4/tcp_bbr.c这一个文件里不需要动TCP协议栈的状态机。第二效果是实时的公平性因子在每次ACK处理时都会更新反应速度比限速策略快得多。第三代码可配置编译成内核模块后可以通过sysctl接口动态开关不用重启机器和重新编译整个内核这也是生产环境能接受的最低侵入方案。2. 核心细节解析与实操要点2.1 BBR状态机中的关键挂载点BBR算法中有四个核心状态分别是启动阶段、排空阶段、带宽探测阶段和延迟探测阶段。公平性优化逻辑不能打断这四个状态的正常流转所以在动手之前必须把每个状态下参数更新的语义摸清楚。最关键的挂载点是bbr_update_model函数中的bbr_update_bw和bbr_update_cycle_phase。前者负责每次ACK回来时更新带宽样本后者负责状态机的周期切换。我的公平性因子计算放在带宽更新之后、周期切换之前。原因很简单带宽更新的结果是最新的测量数据周切换之前又需要根据当前状态决定下一个周期用什么增益系数。在这中间插入公平性判断时机最准干扰最小。另外一个挂载点是发送速率计算函数bbr_set_cwnd。BBR最终通过bbr_target_wnd来限制拥塞窗口大小这个值直接决定实际发送量。公平性因子不仅要影响增益系数还要最终体现在窗口计算上。我调试时发现只调增益系数不调窗口在探测周期末端会出现窗口和增益不匹配的问题表现为瞬时发送速率抖动。2.2 公平性因子的计算原则公平性因子不能是一个拍脑袋的常数它需要反映当前链路的竞争态势。我的设计里采用了三个互相补充的信号来源。第一个是丢包率变化量。BBR默认不因丢包降速但丢包率升高本身就是链路过载的强信号。这里取最近一个探测周期内的丢包率增量如果增量超过某个基线值说明链路开始出现排队溢出竞争在加剧。第二个是排队延迟变化率。BBR在探测周期内会记录最小RTT实际RTT与最小RTT的差值就代表了当前排队延迟。排队延迟持续上升意味着新数据包进入链路的速率超过了链路清空速率这也是一个公平性恶化的前兆。第三个是估算的活跃流数量。这个比较tricky因为TCP层面没有直接接口告诉你同一条链路上有多少个流。我采用的方法是观察发送速率增益后的带宽增幅如果增益系数从1.0升到1.25带宽几乎没有增长说明链路已经到了瓶颈而且有其他流在和你分享这个瓶颈。这个现象结合排队延迟数据可以间接估算竞争流的规模。最终的公平性因子取值在0到1之间计算方法我贴一下核心代码片段static inline u32 bbr_fairness_factor(struct bbr *bbr, u32 loss_inc, u32 delay_inc) { u32 factor 100; if (loss_inc 2) factor - min_t(u32, 30, loss_inc * 5); if (delay_inc 3) factor - min_t(u32, 40, delay_inc * 8); if (bbr-probe_rtt_cnt 2 bbr-max_bw_last_probe bbr-max_bw * 105 / 100) factor factor * 8 / 10; return max_t(u32, factor, 55); }这里我把公平性因子限制在55到100之间。上限100就是完全不干预下限55是为了保留BBR的带宽探测能力不至于变成第二个CUBIC。经验值是从一个模拟链路和真实跨机房间的混合测试中调出来的。2.3 增益参数的自适应调整在拿到公平性因子之后原本固定的增益系数就不能再是常数了。BBR原有的增益系数是在bbr_cwnd_event里通过bbr-增益引用的我在初始化时把这个系数变成动态计算的。具体的换算逻辑是target_gain base_gain * fairness_factor / 100。base_gain是1.25fairness_factor在0.55到1.0之间所以最终增益系数落在0.6875到1.25之间。当增益系数降到1.0以下时BBR实际上在主动释放带宽——这在原版BBR中是不会出现的动作正好用来在竞争场景下让出空间。动态调整增益系数还有一个好处就是不会破坏BBR原有的启动阶段行为。启动阶段的增益本来就比稳态大我在代码里加了一个状态判断只有当前处于带宽探测周期时才应用公平性因子启动和排空阶段保持原逻辑。这样可以避免一个副作用新流建立初期因为公平性因子算法还没收敛误降增益导致启动变慢。2.4 生产环境的三个重要约束在把优化代码带入生产环境过程中有几个容易被忽略的约束需要格外强调。第一个是内核版本的适配。BBR的内核代码在不同版本之间是有差异的比如在4.9内核上bbr结构体还叫struct bbr在更新的内核上字段名和结构体布局都有调整。我建议所有改动基于你当前生产机器的内核源码树来做不要拿网上某个高版本内核的补丁直接打。编译时用make scripts把内核头文件准备好避免出现结构体字段找不到的编译错误。第二个是GSO和TSO的干扰。如果你在测试中发现公平性因子的计算值异常偏大先检查网卡是否开启了TSO和GSO。这两个硬件卸载特性会让内核看到的skb尺寸大于MTU数据包统计和丢包率的计算口径都会受影响。我后来统一用ethtool -K eth0 tso off gso off关闭这两个特性做基准测试得出的数据才有参考价值。第三个是并非所有流量都应该跑在优化后的BBR上。像interactive类的低延迟小包业务走原版BBR就够了因为这类流量本身发送量小、占不了多少带宽没有必要额外引入一个公平性控制逻辑。我的最终生产方案是结合ip route的优先级路由把大流量业务路由到优化后的BBR小包业务走默认的CUBIC两条路互不干扰。3. 实操过程与核心环节实现3.1 搭建隔离测试环境公平性测试最怕的是环境不干净。如果在共享的物理网络上测试相邻机型的流量波动就能轻易打乱你的实验结果。我这次采用了两台物理服务器直连的方式中间经过一个用Linux TC做的网络模拟器专门制造延迟和瓶颈带宽。测试拓扑是发送端安装优化后BBR的机器连接到TC模拟器TC模拟器再连接到接收端。瓶颈链路带宽用tbf队列设置成10Mbps往返时间用netem设置成40ms丢包率默认0。然后再用三台普通机器连接同一个TC交换节点分别跑CUBIC、原版BBR和优化版BBR和被测流抢带宽。内核模块的编译安装流程我用的是标准mainline源码树版本4.19.112。之所以选这个版本是因为它在BBR的基础实现上没有太多后来版本的各种修复补丁便于确认我的优化逻辑是独立起作用的。编译时只编译net/ipv4/tcp_bbr.ko这一个模块不要整个内核都编make net/ipv4/tcp_bbr.ko cp net/ipv4/tcp_bbr.ko /lib/modules/$(uname -r)/kernel/net/ipv4/ depmod -a modprobe tcp_bbr加载成功之后通过sysctl把系统的默认拥塞控制算法切换过去sysctl -w net.ipv4.tcp_congestion_controlbbr3.2 对照组和实验组的参数设定实验组和对照组的参数设定直接决定了实验结果能不能说明问题。我设定的是三组对比测试第一组是原版BBR对CUBIC作为基线用来复现“不公平”问题。第二组是优化版BBR对CUBIC检验优化后的算法在和传统算法竞争时是否能够退让。第三组是优化版BBR对原版BBR看两个不同版本的BBR在同一条链路上如何分配带宽。每组测试持续300秒使用iperf3拉满流量。发送端启动iperf3的命令固定为iperf3 -c 接收端IP -t 300 -i 10接收端用iperf3 -s起服务端。每隔10秒记录一次带宽统计和一个中间代理节点的队列深度这两组数据组合起来基本能还原链路状态全貌。3.3 核心代码改动的完整流程实际改动过程中最大的工作量在如何把公平性因子自然地嵌入原始BBR逻辑。下面我把关键代码按步骤完整贴出来并标注注释。首先在struct bbr里增加三个字段struct bbr { u32 bw_median; /* 带宽中位数样本 */ u32 fairness_factor; /* 公平性因子 0~100 */ u8 fairness_enable; /* 是否启用公平性优化 */ u32 loss_inc; /* 丢包率增量 */ u32 delay_inc; /* 排队延迟增量 */ u32 prev_loss; /* 上一周期丢包统计 */ u32 prev_rtt; /* 上一周期最小RTT */ };接着在bbr_init里初始化这些字段static void bbr_init(struct sock *sk) { struct bbr *bbr inet_csk_ca(sk); bbr-fairness_factor 100; bbr-fairness_enable 1; bbr-prev_loss 0; bbr-prev_rtt 0; bbr-loss_inc 0; bbr-delay_inc 0; /* 原有初始化代码保持不动 */ }然后修改bbr_update_model在带宽更新完成后调用公平性计算逻辑static void bbr_update_model(struct sock *sk, const struct rate_sample *rs) { struct bbr *bbr inet_csk_ca(sk); bbr_update_bw(sk, rs); if (bbr-fairness_enable rs-losses) { bbr-loss_inc max_t(u32, rs-losses, bbr-prev_loss); bbr-prev_loss bbr-loss_inc; } if (bbr-fairness_enable rs-rtt_us) { u32 min_rtt bbr_min_rtt(bbr); if (bbr-prev_rtt bbr-prev_rtt min_rtt) bbr-delay_inc (rs-rtt_us - min_rtt) * 100 / min_rtt; bbr-prev_rtt min_rtt; } bbr-fairness_factor bbr_fairness_factor(bbr, bbr-loss_inc, bbr-delay_inc); bbr_update_cycle_phase(sk, rs); bbr_update_min_rtt(sk, rs); }最后把公平性因子和增益系数挂钩。在bbr_set_cwnd的target窗口计算处调整窗口上限static u32 bbr_target_cwnd(struct sock *sk) { struct bbr *bbr inet_csk_ca(sk); u32 bw bbr_max_bw(bbr); u32 rtt bbr_min_rtt_us(bbr); u32 cwnd bw * rtt / 1000; if (bbr-fairness_enable bbr-fairness_factor 100) cwnd cwnd * bbr-fairness_factor / 100; return cwnd; }这里有一个需要仔细想清楚的细节窗口上限调小之后BBR的发送速率会被限制在公平份额以内。但这并不等于直接把增益系数调小——窗口限制是硬约束增益调整是软控制两者配合才能保证既不会瞬间过冲也不会长期低于公平份额。3.4 测试过程中的实测数据记录三组测试跑完之后我来梳理一下实际的数据变化。基线组原版BBR对CUBIC的结果抢带宽现象非常明显。原版BBR跑到了7.8MbpsCUBIC只分到了2.1Mbps左右而且在300秒内CUBIC的带宽始终没有回升趋势。这说明在中等延迟且无丢包的链路条件下CUBIC对BBR的退让是持续性的不只是一个瞬时波动。优化版BBR对CUBIC这组里效果立竿见影。优化版BBR的带宽稳定在5.6Mbps左右CUBIC拿到了4.3Mbps总带宽仍然接近10Mbps的瓶颈没有因为引入公平性逻辑造成浪费。关键是CUBIC的曲线没有像基线组那样一路下探反而在中段趋于平稳。第三组优化版BBR对原版BBR两者都比较能抢最终结果接近5比5。原版BBR稍占优势优化版BBR因为有抑制因子让出了一部分带宽但差距在合理范围内。这组数据说明我的逻辑并没有把BBR的主动性彻底削弱只是在竞争场景下表现得更有节制。4. 常见问题与排查技巧实录4.1 公平性因子不生效的问题排查在刚开始联调时遇到过一类问题明明链路出现了明显竞争数据包排队延迟已经涨了一倍但公平性因子始终停留在100附近不下降。检查日志之后发现问题出在我的丢包增量采集点太靠后rs-losses字段在BBR的采样结构体里不是每一次更新都会被赋值某些快速ACK路径到不了bbr_update_model导致增量没有被统计进去。后来我把丢包增量采集挪到了bbr_update_min_rtt之前并用tcp_rcv_space_adjust触发的一个独立hook来补充统计这样即使BBR的主路径没有收到丢包事件辅助路径也能兜底。产出经验是BBR里的各个更新函数并非所有路径调用频次一致挂载逻辑时不要只选一个点最好在两条以上路径各留一个触发口。4.2 带宽利用率下降的问题优化后遇到另一个典型问题是带宽利用率有一点点下降大约损失了5%左右的总吞吐。原因是窗口上限削到公平份额之后当链路空闲或者对端流退出时我的算法没有及时恢复探测能力导致一段时间内空跑了一部分带宽。解决方式是在公平性因子之外再加一个“恢复模式”判定当连续一个完整带宽探测周期内排队延迟增量为0且没有丢包增量直接把公平性因子置回100让BBR恢复激进探测。这在代码里就是加一个分支判断但效果非常明显恢复场景下的总带宽在下一个探测周期就能拉满。4.3 对RTT敏感型业务的干扰单独跑优化版BBR时交互式小报文的RTT比原版BBR也要低一些但有一次真实生产中出现了一个状况某核心业务的HTTP请求延迟曲线偶尔出现尖刺。经过排查那并不是BBR本身造成的而是流量走了优化路由之后和另一个大数据任务共享了同一条链路的中转队列两个任务却在不同的拥塞控制域中。这种情况下再牛的公平性优化也没用因为跨域的流量根本不在同一套控制循环里谁也看不到谁的反馈信号。最终解决方法是把大数据任务的峰值速率在应用层削掉三分之一错峰使用链路。这个案例说明算法优化能解决同一个控制域内的公平性解决不了跨域规划问题。4.4 丢包率统计不准的坑如果你的测试环境里加了netem模拟随机丢包会发现公平性因子的表现非常不稳定。原因在于丢包率增量这个信号对随机丢包过于敏感把随机丢包误判成竞争信号然后错误地降低了发送速率。我在代码里对丢包增量做了一个简单的滤波处理只有在连续三个往返延迟内丢包增量都超过阈值时才确认竞争信号有效单个周期的瞬时尖峰不做响应。这个改动让优化后的BBR在随机丢包和竞争排队两种场景下有了更好的区分度实测效果比较理想。5. 公平性优化的效果评估与扩展思路5.1 在更长链路上的验证把测试链路从40ms改成120ms之后优化版BBR的公平性优势更加明显了。高延迟链路上CUBIC的窗口增长本来就缓慢遇到BBR抢带宽时退让得更快基线组的不公平甚至拉大到了9比1。而优化版BBR依然能把份额控制在6比4左右说明公平性因子在这个场景下起到了足够的阻尼作用。长肥网络中需要注意另一个参数初始窗口。BBR在启动阶段会尝试更快地填满管道优化逻辑我刻意没有干预启动阶段所以在短流场景下优化版BBR的表现和原版几乎一致长流场景下才有比较明显的效果区分。5.2 把优化逻辑抽成独立模块目前这个公平性因子是和BBR主逻辑耦合在一个源文件里的。后面我打算把它抽成独立的头文件通过函数指针注册的方式挂到BBR的状态机上。这样以后如果要从4.19内核迁移到6.x只需要改头文件里的字段映射关系不需要重写核心判断逻辑。有条件的话还可以把公平性因子通过pr_xxx暴露到/proc或者tracepoint里配合bpf工具做在线观测。这一点在生产环境非常有用你可以实时看到每时刻每个TCP流的公平性因子变化曲线判断优化有没有在正常工作。5.3 和ECN显式拥塞反馈的联动ECN和BBR的配合是一个值得继续挖掘的方向。目前的公平性因子只看丢包和延迟两个信号但如果链路设备支持ECN可以在IP头里直接标记拥塞状态比用延迟推断要准确得多。我在实验环境里试过一个小改动在公平性因子计算时如果收到ECN标记的ACK比例显著上升直接把增益系数跳到1.0不再等待排队延迟数据确认。这样响应更快而且不会因为延迟噪声误判。遗憾是生产环境的路由器对ECN支持参差不齐这个功能在实际部署时还需要做兼容性探测。5.4 多队列网卡场景下的信号失真还有一个容易被忽略的坑就是多队列网卡。当RSS把不同流分配到不同队列时每个队列的统计是独立的而BBR看到的却是全局socket层面的数据。如果某个队列出现拥塞而另一个队列空闲BBR的公平性因子可能会失真。解决思路是开启ethtool -L eth0 combined N调整队列数量或者使用tc的多队列qdisc让每个队列独立反馈。不过这个方案的通用性还不是很好我目前只在部分场景下做过验证后续有更成熟的数据再展开写。在多次一轮一轮的验证和回退调整之后我对BBR公平性这件事的整体认知也清晰了不少。单纯换算法、调参数解决不了模型冲突问题纯粹靠窗口限速又损失带宽探测的锐度。最稳的思路还是把竞争感知能力内嵌到算法里让BBR在需要激进的时候激进、在需要克制的时候克制。如果你也在做类似方向的调优建议从最小的挂载点改起先在隔离链路验证信号源是否准确再逐步放量到生产环境不要一上来就全量替换。