计算机网络 20:为什么缓冲更多反而更慢,bufferbloat、队列管理与 ECN
下载一直有进度,交互请求却迟迟没有响应,这两种现象可以同时发生。出口持续发送并不说明新到的短报文很快就能通过:如果前方积累了大量数据,吞吐接近瓶颈容量时仍可能有很长的等待。
本篇承接第 19 篇的发送窗口与共享瓶颈,在相同限速出口下比较 FIFO 和 FQ-CoDel。实验需要区分吸收短暂突发的缓冲与长期积压,并观察拥塞信号如何返回发送端。
缓冲容量不是转发容量
设理想出口每秒服务 5,000,000 bit,前方已有 625,000 B 等待,且不考虑其他调度、协议开销或突发许可。仅把这些字节送出就需要一秒。把存储空间扩大一倍不会把出口变成每秒一千万 bit,只会允许保留更多尚未服务的数据。
1 | |
这道题计算的是给定积压对应的理想服务时间,不能直接拿内核的某个缓冲参数代入,便当作真实 RTT。实际路径还有反向传输、端点调度、报文大小、分段卸载和多级队列。容量是允许的上限,积压是某时刻已经排入的内容,两者也不能互换。
缓冲吸收有限突发是有用的。若一个突发到来后很快排空,偶尔较高的队列长度不必表示持续拥塞。bufferbloat 关注的是过多的持续排队带来的延迟;把所有队列都删掉,也可能增加突发丢失或使链路利用率下降。需要控制的是积压随时间的行为,而不只是一个静态容量数字。
队列管理与服务顺序是两件事
FIFO 按队列中的先后次序出队。若采用尾丢弃,直到达到限制才丢弃新到报文,拥塞反馈可能在已经积累较长等待后出现。这个反应时机和队列最终允许装多少内容有关,但不能从一次丢包断定所有延迟都由同一队列产生。
CoDel 观察报文在队列中的停留时间,区分持续积压与能够排空的突发。RFC 8289 §3.1 至 3.2解释了用一个观察区间内的局部最小停留时间判断持续性,而不是仅凭瞬时队列长度决定。文档状态为 Experimental。
target=5ms 因而不是“所有报文最多等五毫秒”的服务保证。持续时间判断、调度和发送时机都可能使单个样本超过该值;interval=100ms 也不是给每个请求设置的超时。把这两个参数理解成用户请求延迟上限,会误读实测结果。
FQ-CoDel 还改变了服务顺序。RFC 8290 §3描述了流分类、基于字节信用的调度以及每个队列中的 CoDel 管理。该文档同为 Experimental。流经哈希映射到有限数量的队列,可能发生碰撞,并非无条件一条连接独占一个队列。
因此,将一个持续 TCP 负载和低频 UDP 探针放在一起时,探针延迟的变化既可能涉及主动队列管理,也可能涉及流之间的调度。FIFO 对 FQ-CoDel 的实验同时改变了这两部分,不能把全部收益归给 CoDel,更不能把探针 RTT 当成 TCP 负载流自身的 RTT。
ECN 要把信号传回发送端
经典 ECN 的报文标记与 TCP 标志位分属不同首部。IP 的 ECT 表示该报文具备 ECN 能力,CE 表示遇到拥塞;TCP 使用 ECE 返回通知,使用 CWR 表达发送侧已经作出相应的窗口调整。RFC 3168 §6.1给出了这条反馈路径。
经典握手中,发起端 SYN 同时设置 ECE、CWR,响应端 SYN-ACK 设置 ECE 而不设置 CWR,表示协商能力。SYN 上的这两个位不表示连接已经遇到拥塞。连接建立后还要分别核查报文是否带 ECT、是否出现 CE,以及反馈和发送端反应。
队列统计中的 ecn_mark 是标记计数,不能独自证明接收端反馈到达、发送端按预期减窗。反过来,捕获到一个 CWR 也不能单独证明发生过 CE:RFC 3168 §6.1.2 允许 ECN 连接因其他拥塞原因减窗后设置 CWR。必须结合方向、时序及其他证据解释。
本篇采用经典 ECN。当前 Linux IP sysctl 文档还包含 AccECN 相关取值,不能把 tcp_ecn 概括为只有 0、1、2 三种永恒不变的选择。本次只在自建两端命名空间设置并记录 tcp_ecn=1,再检查实际握手,不用文档默认值替代运行事实。
两种明确配置的队列
实验使用专用 Linux 虚拟机中的 A、R、B 三个节点,R 只转发自建 veth 上的流量。A 发出一条显式选择 Reno 的 TCP 持续负载,同时用独立进程向 B 发送 UDP 回显探针。R 到 B 的出口挂同一 TBF 父队列,下面分别挂两种子队列,每种运行三轮,每轮重建命名空间。
1 | |
两条分支是不同轮次的替代配置,不是同时挂载的并行出口。tc-fq_codel(8)分别定义包数量限制与内存限制;同为 256 个队列对象不等于同为固定字节容量。分段卸载也使虚拟设备中的队列对象不必等同于物理线上的单个帧。
父 TBF 创建时使用 limit 64kb,但紧接着替换其内部队列。tc-tbf(8)明确指出,替换内部队列后父级 limit/latency 不再有效。输出仍打印某个 lat 数值,不能据此宣称全部报文受到该排队上限保护。此时必须读子队列配置和实际 backlog。
接收端定义共同的十八秒时钟:前三秒为空载探针期,随后启动 TCP;负载前三秒用于预热,末十二秒按 recv() 返回后的时间戳累计实际字节,并逐块检查内容等于约定的 x 字节。goodput 是该计数乘八再除以十二秒,不是发送调用接受的字节数,也不是含重传的线上吞吐。
探针每次只有一个在途请求,目标间隔 200ms,每次最多等待一秒。响应慢或超时会降低实际发送频率,最后一秒不再发新请求,避免接收端定时关闭影响结果。迟到的旧序号响应不能冒充当前响应。分位数只统计成功样本,全超时记为未取得分位数,不能把一秒超时门限填成 RTT。
负载探针统计按发送时间落在最后十二秒内选样;TCP 自身 RTT 则来自独立的 ss 采样。实际探针发送覆盖第六至第十七秒,TCP goodput 覆盖第六至第十八秒;B 的 UDP 与 TCP 还共享同一接收事件循环。二者定义和端点开销不同。队列丢弃计数还覆盖预热与结束阶段,不能除以十二秒接收字节就称为该区间的丢包率。
实际记录与可复现入口
实跑环境为 Linux 6.18.10-200.fc43.aarch64、Python 3.14.3、iproute2 6.14.0。每轮空载探针均发出十五次且无超时,按下表顺序,空载成功样本 p95 分别为 0.331、0.880、0.404、0.551、0.437、1.121ms。负载期结果如下,吞吐单位为十进制 Mbit/s,RTT 单位为 ms:
| 队列 / 轮次 | goodput | 探针发送 / 超时 | 成功 p50 | 成功 p95 | 子队列 dropped |
|---|---|---|---|---|---|
| FIFO / 1 | 5.044832 | 11 / 11 | 未取得 | 未取得 | 282 |
| FQ-CoDel / 1 | 4.774539 | 31 / 6 | 4.430 | 10.590 | 7 |
| FIFO / 2 | 5.098576 | 11 / 11 | 未取得 | 未取得 | 272 |
| FQ-CoDel / 2 | 4.773573 | 30 / 6 | 5.144 | 18.357 | 8 |
| FIFO / 3 | 5.083131 | 11 / 11 | 未取得 | 未取得 | 289 |
| FQ-CoDel / 3 | 4.772608 | 33 / 5 | 4.794 | 19.032 | 7 |
p50 使用排序后较低的中位次序值,p95 使用向上取整的 95% 次序值。FIFO 全部超时,没有负载 RTT 分布可供计算;FQ-CoDel 的分位数只描述每轮二十四至二十八个成功响应,另有五至六个超时。当前数据支持“此配置下较多探针能在门限内响应”,不支持“所有请求都已得到低延迟保证”。
ss(8)将 RTT 字段定义为毫秒;TCP 自身的 ss RTT 采样提供了另一组观察。最后十二秒中,FIFO 三轮范围分别为 1066.330 至 1485.620、1042.610 至 1383.050、1060.640 至 1373.600ms;FQ-CoDel 分别为 17.651 至 56.121、16.692 至 55.952、18.924 至 57.196ms。它们是发送端 TCP 的 RTT 估计样本范围,不是每个报文的逐包延迟极值,也不能和 UDP 的成功样本 p95 混用。
FIFO 的十二秒应用 goodput 略高于配置中的 5mbit,这一现象原样保留。短区间接收计数、边界处成块交付、虚拟设备行为和长期整形速率并非同一观测量;本次没有追踪出唯一原因,不能改写数值使其“符合”预期。结束时 FIFO 仍有 698028、661758、667814 B backlog,删除命名空间会终止这些尚未排空的队列,不表示最后所有发送字节均已交付。
dropped 列是子队列累计丢弃计数。父 TBF 和子队列可能对同一次丢弃分别记账,不能把父子计数相加;overlimits 也不是丢包计数。三个 FIFO 轮次在统计期 ss 样本中记录到的最大 bytes_retrans 分别为 408336、402544、427160 B,FQ-CoDel 样本没有该非零字段。这些观察不允许逐个认定丢弃的是哪个 UDP 请求,也不证明未采样时刻完全没有 TCP 重传。
六轮均保存了 A 的 SYN 标志值 194(SYN、ECE、CWR)和 B 的 SYN-ACK 值 82(SYN、ACK、ECE),经典 ECN 协商有实际握手证据。FQ-CoDel 三轮的 ecn_mark 为 71、70、69,捕获扫描也分别计到相同数量的 CE;非 SYN 的 ECE 计数为 322、326、345,CWR 为 66、65、64。反馈会跨多个 ACK 持续出现,这些计数本来就不应机械一一相等。
附件只保留握手与首个 CE、ECE、CWR 的原始帧,其余报文仅作有界扫描计数。三轮首例均按 A→B 的 CE、B→A 的 ECE、随后 A→B 的 CWR 排列,CWR 样本带 ECT(0)。这支持存在相应标记与反馈现象,但不是完整 pcap,没有 AF_PACKET 捕获丢失统计,也没有内核减窗分支的逐次因果追踪。
FIFO 三轮没有扫描到 CE、ECE,却各有一个非 SYN 的 CWR,正好说明不能只用 CWR 判定 ECN 标记生效。FQ-CoDel 仍有队列丢弃与探针超时,ECN 也没有把实验变成零丢包系统。接收端到时关闭引发的发送端终止结果保留在记录中,不当作额外注入的网络故障。
附件保留完整实验脚本、六轮原始记录和运行环境与边界说明。脚本只使用 Python 标准库及已安装的 ip、tc、ss;不自动安装工具,也不调整宿主默认路由。
下载脚本后,先在普通终端查看参数;实际实验只能在具有相应权限、已经安装工具的专用 Linux 虚拟机内执行:
1 | |
sudo 行不适用于 macOS 宿主机。脚本为每轮生成随机命名空间前缀,在结束或异常清理时终止自建进程、删除本轮命名空间,并检查残留。复现应保留完整标准输出与错误输出;本次记录不是预期输出模板,不要求每次产生相同吞吐和分位数。
练习与验证边界
已知理想出口为 5 Mbit/s,队列前方有 62,500 B,忽略其他因素时服务时间为多少?答案是 100ms。若只知道队列容量为 62,500 B,则还缺少实际占用,不能直接推出当前等待时间。
若全部 UDP 探针超时,但 TCP 持续有 goodput,能否判断 UDP 都被丢弃?不能。超时只说明在门限内没有得到匹配响应;排队、丢失和端点调度都可能参与,需要额外捕获或时间关联来区分。只报告成功样本的 p95 还会遗漏超时,因此必须同时报告样本数和超时数。
若 FQ-CoDel 下探针 p95 明显降低,能否认定 TCP 流的每个报文也只等待同样的时间?不能。探针可能属于另一个调度队列;TCP RTT 采样和直接停留时间测量回答不同问题。若要单独评估 AQM,应另外设计固定调度、只改变队列管理的对照,不能从当前两组自动推出。
本篇验证限于指定 Linux 虚拟环境、短时间 Reno 负载和固定配置。它没有覆盖物理网卡、无线竞争、多租户公平性、不同基本 RTT、公网中间盒或生产流量;没有逐次关联每个 CE、ECE、CWR 与内核减窗分支。构建成功、附件可下载只验证交付可用性,不补足这些协议与性能证据。
一手资料
资料在本篇写作时核查:RFC 8289 的停留时间机制、RFC 8290 的流调度与 CoDel 组合、RFC 3168 的经典 ECN 握手与反馈;Linux sysctl 文档和 iproute2 手册用于核对配置含义。具体链接已放在对应结论旁。实现版本与当次测量以附件环境记录为准,不能把上游文档当前状态当成本机所有功能均已验证。




