RED主动队列管理详解:从尾丢弃到随机早期检测
做网络运维这些年我最怕看到的画面就是核心路由器出接口的队列深度一路顶满。TCP流量一多丢包开始出现所有连接像听到同一道号令一样集体降速过一会儿又集体恢复周而复始。懂行的人都知道这里缺的不是带宽而是能在队列完全塞满之前就主动干预的机制。这个机制的经典实现就是REDRandom Early Detection随机早期检测很多老资料里也把这个算法简写为REA业界更常见的叫法是RED。虽然现在CoDel、PIE这类新算法越来越流行但RED依然是理解主动队列管理AQM绕不开的起点也是大量设备、SDN交换机和Linux内核里仍在使用的默认能力。这篇文章围绕RED展开为什么要主动丢包、算法本身怎么工作、拿到设备上该怎么配、怎么调、怎么排障。1. 为什么网络需要RED传统尾丢弃的三大痛点1.1 收费站视角看尾丢弃问题都在最后一刻先说说传统队列最常用的处理方式——尾丢弃Tail Drop。所谓尾丢弃就是数据包进入设备后先在缓存队列里排队队列满了以后新到的包直接扔掉不管这个包是语音、视频还是网页请求。你可以把它想象成高速公路收费站的入口车道的排队空间就那么大队尾排满了之后后面来的车不会得到任何提示直接被引导离开。真正的问题在于这种机制在最后一刻之前没有任何反馈。队列从0涨到满的整个过程中系统完全不干预直到溢出的那一刻后续所有流量一起遭殃。对于TCP来说丢包是唯一的拥塞信号。尾丢弃等于在所有流已经挤到极限的时候才给出信号而且一给就是全员通知。这个模式在流量小的时候没什么问题一旦流量大起来队列长期处于满水位各种问题就会接二连三地出现。1.2 全局同步TCP流量的集体抽搐这里要说一个尾丢弃最典型的并发症——全局同步Global Synchronization。假设有几条TCP流共享同一个瓶颈队列各自都在稳定地增长发送窗口。队列慢慢变满直到某几个包同时被尾丢弃。问题来了这些流几乎在同一时刻感知到丢包于是所有流都执行拥塞避免算法把窗口减半甚至触发超时重传。结果就是链路利用率像锯齿一样上下抖动前面几秒链路满负荷丢包后一瞬间链路空转带宽白白浪费流恢复增长之后又一起涨到满再次集体丢包。你在现网里看到的表象就是带宽明明还有富余用户却一阵一阵卡。RED的巧妙之处就是不等到队列满而是让每个包被丢弃的概率和平均队列长度挂钩。队列越长丢弃概率越大至于具体丢到谁头上则是随机点名。这样一来一次拥塞事件里只有一部分流被点名降速系统整体不用集体急刹车链路利用率就能稳在一个比较平滑的水平。1.3 Bufferbloat队列越长时延越高还有一类问题比丢包更隐蔽就是排队时延膨胀。很多设备的缓存其实不小动辄几百KB甚至数MB本意是吸收突发流量。但如果没有主动队列管理TCP流会让队列长期维持在一个很高的水位。每个包进来都要先排队RTT从正常的几毫秒被拉到几十甚至几百毫秒。这个现象有个专门名字叫Bufferbloat中文常译作缓冲膨胀。表现在用户体验上就是视频打开慢、网页卡顿、游戏操作迟滞但链路利用率看起来又很高。RED提前丢包本质上是让TCP提前感知拥塞并降速队列自然就维持在一个低而稳定的水位排队时延随之降下来。这就是主动两个字的意义不等溢出提前通知。2. RED核心机制拆解平均队列长度与三段落丢弃2.1 平均队列长度给瞬时队列装一个阻尼器RED最核心的一个设计是它不使用瞬时队列长度来做判断。网络流量天生是突发的如果拿瞬时队列长度当触发器一次毫秒级的突发就可能触发一串丢弃而这些包本来队列稍微缓冲一下就能容纳丢了完全是浪费。RED维护的是一个指数加权移动平均EWMA公式写成这样avg (1 - w_q) × avg w_q × curq。其中curq是当前瞬时队列长度w_q是权重系数。你可以把它理解成一个带阻尼的体温计测的不是每秒都在跳动的体温读数而是一段时间内平滑变化后的趋势。w_q越小平均值对瞬时变化越迟钝过滤突发的能力越强w_q越大平均值越灵敏但误丢的可能性也越高。为什么这个设计重要因为RED的丢包判断必须慢半拍。它的目标不是惩罚一次突发而是识别持续累积的拥塞趋势。没有这层平均化RED和尾丢弃在效果上就拉不开差距。2.2 三段落策略min_th、max_th与max_p各自管什么RED把平均队列长度分成三个工作区域对应三种处理策略区域平均队列长度条件丢包行为设计意图绿色区avg min_th全部转发容忍正常突发不干预黄色区min_th ≤ avg max_th按概率随机丢包概率从0线性涨到max_p让部分流先降速平滑释放压力红色区avg ≥ max_th全部丢弃兜底保护防止队列溢出失控黄色区是RED的灵魂所在。当前平均队列超过min_th后丢包概率按线性关系增长p_b max_p × (avg - min_th) / (max_th - min_th)。等avg到了max_th概率就涨到max_p超过之后直接全部丢。之所以用线性渐变而不是阶跃是为了给TCP一个渐进的拥塞信号让各条流可以分批次降速而不是再次走向全局同步。关于参数搭配最初那篇论文里的建议至今仍然适用max_th至少取min_th的2到3倍min_th设置到能容纳典型突发的大小。两个阈值靠得太近黄色区太窄随机丢弃和全丢弃会频繁切换反而制造抖动。2.3 w_q、max_p怎么定给一套能落地的调参思路参数敏感是RED最让人头疼的地方但也不是无迹可寻。我的做法是先定目标时延再倒推阈值。假设链路带宽是10Mbps平均包长1000字节我希望把排队时延控制在100毫秒以内。目标时延对应的队列水位大约是带宽乘时延10Mbps × 0.1秒 100万bit约等于125KB折算下来大概83个1500字节的包。那么min_th取这个量级就合理max_th按2到3倍关系取也就是250KB到400KB附近。如果你把目标时延定成10毫秒算出来只有12.5KB约等于8个1500字节包这个水位太浅稍微有点突发就会触发丢弃实际部署很难稳定。max_p一般从0.01到0.1之间选。设得太高随机丢弃逼近全丢弃对TCP吞吐伤害明显设得太低拥塞信号太弱队列照样淤积。我自己的经验是从0.02起步观察平均队列曲线稳定后再微调。w_q则建议维持经典值0.002附近千兆以上高带宽链路可以再取小一点让平均值更稳定。Linux的tc工具里不直接暴露w_q而是通过burst参数按公式换算后面实操部分会再说到。3. 从RED到WRED、ARED与ECN把算法用得更顺手3.1 WRED按优先级实施差异化丢弃基础RED对所有流量一视同仁但现实网络里不行。语音、视频会议、关键业务系统和后台下载、日志同步不该享受同样的丢包待遇。于是就有了加权随机早期检测WREDWeighted RED。WRED的做法是为不同的DSCP值或IP优先级准备独立的丢弃参数每种流量拥有各自的min_th、max_th和max_p。高优先级流量的阈值设得高、丢弃概率设得低意思是队列紧张时先丢你们的机会不大低优先级流量阈值低、丢得早意思是拥塞时先拿你开刀。在主流厂商路由器的命令行里WRED的典型配置长这样以DSCP-based模式为例policy-map WAN-EDGE class EF bandwidth percent 10 random-detect dscp 46 100 200 10 class AF41 bandwidth percent 30 random-detect dscp 34 80 160 10 class class-default random-detect dscp 0 40 80 10 ! interface GigabitEthernet0/0 service-policy output WAN-EDGE语法里random-detect后面四个值分别是DSCP值、最小阈值、最大阈值和标记概率分母。分母为10时最大丢包概率就是1/10。这里我踩过一个坑给语音EF队列也配上很小的阈值结果一拥塞语音反而先丢完全背离了初衷。我的建议是EF这类高优先级队列要么不配RED要么把阈值设到极大只在极端拥塞时起保护作用真正需要精细化丢弃的是AF和BE这类有损容忍的流量。3.2 ARED参数自动调节省去手动摸索RED参数敏感人工调优又费时于是有了自适应RED简称ARED。它的思路是周期性观察平均队列长度和目标区间的关系平均队列老是偏高就自动加大max_p让丢弃更激进一点平均队列长期偏低就自动减小max_p减少误伤。这套机制对现网环境特别友好尤其是流量模式变化大的场景。白天高峰期流的数量可能是夜里的几十倍人工维护一组固定参数根本跟不上。在Linux的tc里给red队列加上adaptive关键字就能启用ARED实测下来比固定参数省心不少。我现在的原则是能用ARED的地方尽量用只有需要非常细粒度控制时才退回手工参数。3.3 ECN从丢包通知升级为标记通知RED的另一个重要增强是ECNExplicit Congestion Notification显式拥塞通知。传统的拥塞通知就是丢包而ECN允许路由器在拥塞时不动数据只在IP头里打一个拥塞标记。接收端看到标记后通过ACK里的ECE标志把拥塞信息回声给发送端发送端照样降窗但数据包本身完好无损不需要重传。要启用ECN链路两端都要配合。在Linux上发送端和接收端需要把net.ipv4.tcp_ecn设为1队列侧的tc命令里要带上ecn关键字。我见过最常见的翻车现场就是只改队列侧没改主机侧结果查了半天一个标记包都没有。另外ECN标记只对支持ECN的流量有效不支持的老流量仍然走正常丢弃路径所以两者是并行关系不会互相干扰。对延迟敏感的应用比如网页首屏和实时交互ECN的价值特别大。丢包意味着至少一个RTT的重传等待而ECN标记是同一条数据路径上即时传递的能少一次往返。4. 实操示例在Linux中搭建一个RED验证环境4.1 实验拓扑与初始化准备纸上谈兵没意思下面给一套可以直接复现的实验方案。拓扑三台Linux主机A作为发送端B作为接收端中间一台双网卡主机R做转发把瓶颈设在R的eth1出方向。三个节点各自配置好IPA用192.0.2.10/24R的eth0用192.0.2.1/24eth1用198.51.100.1/24B用198.51.100.10/24。确保A到B的流量经过R转发并且R的IP转发已开启。注意关掉R上不必要的加速特性避免硬件卸载干扰队列观察纯软件环境测试结果更干净。4.2 tc命令配置RED的完整过程关键一步是先在eth1上制造一个明确的瓶颈。物理链路是100Mbps我想模拟10Mbps的窄带出口先用htb把速率限住再在限速class下面挂red队列# 创建根句柄1:默认class 10 tc qdisc add dev eth1 root handle 1: htb default 10 tc class add dev eth1 parent 1: classid 1:1 htb rate 10mbit ceil 10mbit tc class add dev eth1 parent 1:1 classid 1:10 htb rate 10mbit ceil 10mbit # 在class 10下挂red队列开启ECN tc qdisc add dev eth1 parent 1:10 handle 10: red limit 600kb min 60kb max 240kb avpkt 1000 burst 50 probability 0.02 ecn bandwidth 10mbit逐个解释参数limit是队列物理缓存上限600KBmin和max对应min_th和max_th60KB和240KBavpkt是平均包长估计值按1000字节算burst是突发容忍量tc用它反推w_q典型算法里burst越大w_q越小probability就是max_p0.02bandwidth告诉内核所在链路的速率用于换算参数ecn开启标记模式。这里有个新手常犯的错误直接把red挂成root qdisc。如果接口速率本身远大于流量速率队列永远不满RED压根不会介入看起来配了等于没配。必须先限速或确实存在物理瓶颈让队列有积压的空间RED才有用武之地。配置完成后可以用下面的命令观察统计tc -s qdisc show dev eth1输出里会显示backlog积压量、dropped数量以及开启ecn后的marked标记数量。这几个数值是后面判断配置是否生效的重要依据。4.3 iperf3压测与结果解读打流工具我用iperf3。接收端启动服务iperf3 -s发送端开10条并发TCP流持续120秒iperf3 -c 198.51.100.10 -t 120 -P 10建议分三组对比。第一组只做htb限速不加red相当于纯尾丢弃基线第二组在htb下挂red但不带ecn第三组red加ecn。重点看三个指标总吞吐是否平稳、tc统计里dropped的时间分布是否均匀、以及ping测出来的RTT是否稳定。我实际跑下来的典型结果是这样纯尾丢弃那组iperf3每秒输出的吞吐数据锯齿明显偶尔出现整条流跌到接近0的尖峰ping的RTT在几毫秒到上百毫秒之间剧烈跳动加了red之后吞吐整体接近限速值但波动幅度明显收窄RTT的抖动也小了一个量级再加上ecn后统计里marked逐渐增加而dropped明显减少吞吐曲线更平滑。这个现象背后就是RED的随机早期丢弃让各条流错峰降速避免了全局同步。如果要用UDP流量做对照可以这样打iperf3 -c 198.51.100.10 -u -b 20M -t 120UDP没有拥塞窗口RED丢再多它也不降速。跑完之后你会看到dropped数量很多但UDP发送速率纹丝不动。这个实验能直观说明一个道理RED对TCP流有效对UDP流只能做到丢该丢的没法让UDP源自动收敛。5. 常见问题与排查技巧实录5.1 配置了RED吞吐反而上不去的三个原因这是我收到过最多的反馈配置完RED之后链路吞吐不升反降。排查优先级从高到低排序第一min_th和max_th设得太小。平均队列长度几乎永远停在黄色区随机丢包连续不断每条流都以为网络很堵都不敢开大窗口。处理办法是按第2节的时延倒推法重算阈值把黄色区放宽。第二max_p设得太大。我见过有人直接抄别人配置把probability设成0.5这等于大部分包都在丢。从0.01到0.02起步观察队列曲线后再收紧。第三burst和w_q不匹配。burst设得太小导致平均队列对瞬时队列过于敏感一有突发就误丢一批。检查tc配置里的burst值按man page里的公式重新计算。我的排障顺序永远是先看tc -s qdisc show里的dropped速率如果丢包持续均匀存在多半是max_p问题如果丢包集中在压力瞬间多半是阈值或burst问题。5.2 UDP流量不受控RED被打穿RED对付不了UDP的持续高压。原因很简单UDP没有拥塞窗口RED丢它的包它转眼又原速灌进来。如果在一条混有大量UDP的链路上只做普通RED最终效果是UDP流量抢占了队列TCP流量被挤到黄色区里持续丢包体验更差。网上的解决办法还是得靠流量分级。语音、视频这类UDP流量放进高优先级队列配大阈值尽量少丢后台下载等低优先级流量放默认队列配小阈值拥塞时先丢它们。这就是WRED存在的意义——不是把RED做得更复杂而是让不同流承担不同的拥塞代价。5.3 ECN开启后却没有标记包出现这个现象时我会按下面三个环节逐一查队列侧的命令有没有带ecn关键字发送端和接收端的net.ipv4.tcp_ecn是不是1中间设备有没有把ECN字段当成普通保留位处理。其中主机侧最容易漏因为很多发行版默认值是0或者22的意思是被动响应但不主动请求效果也和1不一样。还要注意一个细节Linux的tc red里ecn标记只对平均队列位于min与max之间的包生效一旦avg超过max就转为直接丢弃。所以如果队列已经长期处于红色区你看到的只会是dropped猛涨而marked很少这其实是阈值设置问题不是ECN没生效。5.4 一张表看清常见误配置症状可能原因处理动作平均队列持续顶到limitmin/max设太高TCP来不及在溢出前降速按目标时延重算阈值适当下调整体吞吐偏低max_p过大丢包过激从0.01到0.02起步调小突发流量一来就误丢w_q过大或burst过小增大burst降低w_q高速链路完全看不出效果参数没有按链路带宽重算以带宽时延积为基准重新估算ECN标记数为0主机侧tcp_ecn未开启或队列未加ecn关键字两端检查sysctl与tc配置UDP与TCP混跑时TCP被饿死未做流量分级默认队列共用配置WRED或优先级队列隔离6. 从RED到现代AQM我的选型建议与调参心得6.1 在现网调RED的三条体会第一条参数不能抄作业。同一组min_th和max_p换一条链路就完全是另一个效果。链路带宽、平均包长、目标时延、并发流数量每一样都会影响最优参数。老老实实按自己的数据算一遍比直接抄任何现成配置都靠谱。第二条优先打开ARED和ECN。这两个特性一个解决参数自适应一个缓解丢包对上层应用的影响都是在不大幅增加配置复杂度的前提下提升体验的方案。我在几个流量波动很大的出口链路上做过对比固定参数RED要隔三差五调一次ARED基本托管。第三条观察平均队列曲线比看接口利用率有价值得多。接口利用率只告诉你忙不忙平均队列曲线告诉你堵没堵、堵了多久。把tc统计接入监控每天看avg曲线的形态往往能提前几天发现拥塞趋势。6.2 RED之后的演进方向RED之后还有CoDel、PIE、FQ-CoDel这些更现代的主动队列管理算法它们在Linux里开箱即用理念上都是从提前干预这个思路长出来的。区别主要在于不再依赖手动调参CoDel用最小排队时延作为拥塞指标PIE用排队时延变化率作反馈FQ-CoDel更是把公平队列和AQM结合到了一起。但如果要理解它们为什么这么设计回到RED把算法原理彻底吃透是值得的。我后来排查过一次诡异的瞬时丢包问题最后定位到就是对窗口内的数据包做了资源预分配本质上和RED面向拥塞提前介入的思路一脉相承。学到后面的算法再回头看这些老设计你会更明白设计者当时做出的每一个取舍。