1. 从一个让人抓狂的传输现象说起做网络协议调优的人几乎都遇到过这样一种场景明明链路带宽没跑满发送端却像被踩了刹车一样速度死活上不去抓包一看满屏都是重复ACK偶尔还夹杂着几个RACK相关的标记。第一次碰到这种情况的时候我盯着Wireshark的序列号看了整整一个下午才慢慢摸清楚这背后其实是两套丢包判定机制在互相配合又在某些情况下互相打架。RACK全称Recent Acknowledgment中文一般叫“最近确认”是近几年在传输层拥塞控制里越来越重要的一套丢包检测思路。它和传统的重复ACK机制不是替代关系而是互补关系。重复ACK负责快速发现“中间丢了一个包”RACK负责兜底那些重复ACK覆盖不到的边角场景比如尾部丢包、乱序严重、ACK被延迟合并等等。理解这两者的分工和配合是排查传输性能问题的基本功。这篇文章适合谁看如果你在做网络性能优化、写过自定义的可靠传输协议、调过拥塞控制算法或者只是单纯被“为什么带宽跑不满”这个问题折磨过那接下来的内容应该能帮你省下不少抓包和试错的时间。我会从设计思路讲到具体参数再讲到实际排查时怎么区分到底是重复ACK在起作用还是RACK在起作用尽量把踩过的坑都摊开说清楚。2. 重复ACK与RACK的整体设计思路拆解2.1 重复ACK到底在解决什么问题重复ACK的核心逻辑其实很朴素接收端每收到一个乱序的报文就立刻回一个ACK告诉发送端“我还在等你前面那个没到的包”。发送端连续收到三个针对同一个序列号的ACK就判定这个包丢了触发快速重传不用等超时定时器。这套机制的精妙之处在于它把丢包检测的延迟从“超时时间”压缩到了“三个报文往返”的量级。在没有重复ACK之前发送端只能靠重传超时来发现丢包那个时间通常是几百毫秒甚至秒级。三个重复ACK把发现时间降到了毫秒级这是传输性能的一次巨大飞跃。但重复ACK有个天生的软肋它依赖“后续报文还能继续到达接收端”。如果丢的是发送窗口里的最后一个包后面没有新报文了接收端就凑不出三个重复ACK发送端只能干等超时。这就是所谓的“尾部丢包”问题。另一个软肋是乱序如果链路本身乱序严重接收端会因为乱序产生大量重复ACK发送端可能误判丢包触发不必要的重传。2.2 RACK为什么被引入RACK换了一个完全不同的判定角度。它不看“收到了几个重复ACK”而是看“时间”。核心思想是如果一个报文发出后经过足够长的时间比它更晚发出的报文都已经被确认了那这个报文大概率是丢了。这个“足够长的时间”不是固定的而是根据当前连接的往返时延动态计算的。RACK维护一个“最近被确认的报文”的时间戳当收到新的ACK时如果某个未被确认的报文发送时间比这个最近确认时间早了超过一个RTT加上一点余量就判定它丢了。RACK最大的优势是不依赖后续报文的到达。哪怕丢的是最后一个包只要之前有报文被确认过RACK就能根据时间差把它揪出来。这就补上了重复ACK在尾部丢包场景下的短板。同时RACK对乱序的容忍度也更好因为它用的是时间窗口而不是计数窗口偶发的乱序不会轻易触发误判。2.3 两者为什么必须配合而不是二选一有人可能会想既然RACK这么强为什么不干脆只用RACK原因在于RACK的判定需要时间积累它的反应速度天然比重复ACK慢。重复ACK在理想情况下三个报文往返就能触发重传而RACK至少要等一个RTT加余量。对于中间丢包这种重复ACK能快速处理的场景用RACK反而会拖慢恢复速度。所以实际系统里的做法是重复ACK负责快速路径第一时间发现并重传中间丢包RACK负责慢速路径和兜底处理重复ACK覆盖不到的尾部丢包、乱序误判、ACK合并等场景。两者共享同一个重传队列和拥塞窗口谁先判定丢包谁就触发重传另一个看到包已经被重传了就自动跳过。这种配合关系决定了排查问题时不能只看一个指标。你看到重复ACK多不代表RACK没在工作你看到RACK触发了重传也不代表重复ACK机制失效了。得把两者的触发条件和时间线放在一起看才能还原出真实的传输行为。3. 核心细节解析与实操要点3.1 重复ACK的触发条件与边界情况重复ACK的判定逻辑看起来简单但边界情况特别多。标准定义是“收到三个重复ACK触发快速重传”但实际实现里有很多变体。比如有些实现会在收到第二个重复ACK时就进入“快速恢复”的预备状态收到第三个才真正重传。还有些实现会根据拥塞窗口的大小动态调整这个阈值。一个容易被忽略的细节是重复ACK的计数是针对同一个序列号的。如果接收端因为乱序回了ACK 100、ACK 100、ACK 120、ACK 100那针对100的重复ACK只有三个但中间夹了一个120有些实现会把计数器重置。这个行为在不同协议栈里不一致排查时如果发现重传没触发先确认一下重复ACK的计数逻辑。另一个关键点是SACK选择性确认的配合。开了SACK之后接收端可以在ACK里附带“我已经收到了哪些块”的信息发送端就能更精确地判断丢的是哪个包而不是靠重复ACK的数量猜。SACK和重复ACK是互补的SACK让重传更精准重复ACK让触发更及时。注意如果你的抓包里看到大量重复ACK但重传很少先检查SACK是否开启。SACK开启后很多重复ACK会被SACK块“解释”掉发送端知道那些包只是乱序不是丢了就不会触发重传。3.2 RACK的时间窗口计算与参数选择RACK的核心参数是那个“时间余量”通常记作reo_wnd或者类似的名称。它的计算方式是先取当前连接的平滑RTT然后乘以一个系数再加上一个固定的小偏移。这个系数和偏移的选择直接决定了RACK的敏感度和误判率。系数太小RACK会把正常乱序误判成丢包触发不必要的重传浪费带宽还加重拥塞。系数太大RACK反应迟钝尾部丢包要等很久才能被发现。实际调优时我一般会先看链路的乱序程度如果乱序主要来自多路径或者无线链路系数要适当放大如果链路很干净系数可以收紧。还有一个容易被忽略的参数是RACK的“时间基准”怎么选。有的实现用最近被ACK的报文的发送时间有的用最近被ACK的报文的确认时间。这两个选择在ACK延迟合并的场景下差异很大。用发送时间做基准RACK会更激进用确认时间做基准RACK会更保守。排查时如果发现RACK触发过于频繁可以先确认一下基准的选择。3.3 两者共享状态时的冲突处理重复ACK和RACK共享重传队列这就带来一个状态同步问题。假设重复ACK先判定包A丢了触发了重传重传的包还在路上RACK又根据时间差判定包A丢了这时候如果不做去重就会重传两次。实际实现里通常用一个“已重传”标记来避免重复。但这里有个微妙的时序问题如果重复ACK触发的重传包本身也丢了RACK再次判定时那个“已重传”标记还在RACK就会跳过导致这个包永远不被重传只能等超时。这个场景在丢包率较高的链路上偶尔会出现表现就是某个包的重传延迟异常大。解决办法通常是给“已重传”标记加一个有效期超过一定时间后允许再次重传。这个有效期的设置需要权衡太短会导致重复重传太长会导致上面说的死锁。我一般会把它设成一个RTT左右实测下来在大多数链路上都能兼顾。3.4 实操中怎么区分两者的贡献排查传输问题时最实际的需求是搞清楚“这次重传到底是重复ACK触发的还是RACK触发的”。抓包工具通常不会直接告诉你但可以通过时间线推断。如果重传发生在收到第三个重复ACK之后紧接着的几个毫秒内大概率是重复ACK触发的。如果重传发生在某个ACK到达之后但之前并没有凑够三个重复ACK那大概率是RACK触发的。更精确的做法是看重传包的序列号重复ACK触发的重传通常针对的是重复ACK指向的那个序列号RACK触发的重传可能针对的是更早的、已经被“跳过”的序列号。还有一个技巧是临时关闭其中一个机制做对比测试。很多协议栈支持通过参数关闭RACK或者调整重复ACK的阈值。关掉RACK后如果尾部丢包的恢复时间明显变长说明RACK在起作用关掉重复ACK的快速重传后如果中间丢包的恢复变慢说明重复ACK在起作用。这种对比测试虽然粗暴但能快速定位问题。4. 实操过程与核心环节实现4.1 搭建可复现的测试环境要复现重复ACK和RACK的交互最方便的是用网络模拟工具构造一个可控的丢包和乱序环境。我一般会用一个发送端、一个接收端中间加一个可以注入丢包和延迟的模拟链路。发送端跑一个支持RACK的传输协议栈接收端正常收包。关键配置是模拟链路的参数基础RTT设成50毫秒左右丢包率从0.1%开始逐步加到2%乱序率单独控制。这样可以在不同条件下观察两者的行为差异。测试数据用大文件传输保证有足够多的报文来触发各种边界情况。发送端需要打开详细的日志记录每个报文的发送时间、每个ACK的到达时间、每次重传的触发原因。接收端也要记录每个报文的到达时间和回ACK的时间。两边的时间戳要对齐最好用同一个时钟源或者做偏移校正。4.2 构造尾部丢包场景观察RACK尾部丢包是RACK的主场。构造方法是让发送端发送一批报文然后在最后一个报文上注入丢包同时确保之前的所有报文都正常到达并被确认。这时候接收端凑不出三个重复ACK因为后面没有新报文了。在没有RACK的情况下发送端只能等重传超时恢复时间通常是几百毫秒。打开RACK后发送端会在最后一个被确认的报文之后大约一个RTT加余量的时间点判定尾部丢包触发重传。恢复时间能压缩到几十毫秒。实测时要注意尾部丢包的判定依赖“之前有报文被确认”。如果整个发送窗口的报文全丢了RACK也没有时间基准还是得靠超时。所以测试时要确保至少有一个报文成功到达并被确认。4.3 构造中间丢包场景观察重复ACK中间丢包是重复ACK的主场。构造方法是发送端发送一批报文在中间某个报文上注入丢包确保后续报文继续到达。接收端收到后续报文后会回重复ACK发送端凑够三个就触发快速重传。这个场景下RACK也会介入但通常重复ACK会先触发。因为重复ACK只需要三个报文往返而RACK要等一个RTT加余量。实测时如果发现RACK先触发了说明重复ACK的计数逻辑可能有问题或者SACK把重复ACK“吃掉”了。一个值得观察的细节是重传后的恢复速度。重复ACK触发重传后发送端会进入快速恢复拥塞窗口减半但不回到慢启动。RACK触发重传后拥塞控制的行为取决于具体实现有的会进入快速恢复有的会直接进慢启动。这个差异会显著影响后续的吞吐量。4.4 参数调优的实操记录我在一次实际调优中遇到过这样的场景链路RTT 80毫秒丢包率0.5%乱序率1%。默认参数下吞吐量只有带宽的60%左右。抓包发现重复ACK很多但重传也不少说明有误判。第一步是收紧RACK的时间余量。默认系数是2倍RTT我把它降到1.5倍误判减少了一些但吞吐量提升不明显。第二步是检查重复ACK的阈值发现实现里用的是固定3个但在乱序率1%的链路上这个阈值偏低。把阈值动态调整为“3加上乱序率乘以一个系数”后误判明显减少。第三步是调整SACK的行为。默认SACK块的数量有限乱序严重时SACK块不够用发送端还是得靠重复ACK猜。增加SACK块的数量后发送端能更精确地判断哪些包只是乱序重传更精准。三步下来吞吐量恢复到了带宽的85%以上。提示调优时不要一次改多个参数否则无法判断哪个参数起了作用。每次只改一个观察至少几分钟的稳定状态再改下一个。5. 常见问题与排查技巧实录5.1 重复ACK很多但吞吐量正常这种情况通常不是问题而是链路乱序的正常表现。乱序导致接收端回重复ACK但SACK让发送端知道那些包只是乱序不是丢了所以不触发重传。吞吐量正常说明拥塞控制没有误判不需要干预。如果确认乱序是链路特性而不是故障可以考虑适当放宽重复ACK的阈值减少不必要的ACK流量。但要注意放宽阈值会降低对真实丢包的敏感度需要权衡。5.2 RACK频繁触发但重传后问题依旧RACK频繁触发说明它认为有很多包超时未确认。但如果重传后问题依旧可能是重传的包也丢了或者RACK的时间基准有问题。先检查链路是不是有持续的高丢包如果是RACK频繁触发是正常反应问题在链路本身。如果链路丢包不高但RACK还是频繁触发检查时间基准的选择。用发送时间做基准在ACK延迟合并的场景下会偏激进可以改成用确认时间做基准试试。另外检查RTT的测量是否准确RTT测量偏差会直接导致RACK的时间窗口计算错误。5.3 尾部丢包恢复时间异常长尾部丢包恢复时间长说明RACK没有及时触发。可能的原因有几个一是RACK被关闭了或者参数设得太保守二是尾部丢包之前没有报文被确认RACK没有时间基准三是“已重传”标记的死锁问题导致RACK跳过了本该重传的包。排查时先确认RACK是否开启然后看尾部丢包之前是否有报文被确认。如果都没有问题检查“已重传”标记的有效期设置适当缩短有效期看是否能改善。5.4 常见问题速查表现象可能原因排查方向解决思路重复ACK多但重传少链路乱序SACK在起作用检查SACK是否开启正常现象无需干预RACK频繁触发链路丢包高或时间基准偏激进检查链路丢包率和RTT测量调整时间基准或放宽余量尾部丢包恢复慢RACK未开启或参数保守确认RACK状态和时间基准开启RACK或收紧余量重传后问题依旧重传包也丢了或死锁检查重传包是否到达缩短已重传标记有效期吞吐量上不去两者误判导致拥塞窗口频繁减半对比关闭其中一个机制分别调优阈值和余量5.5 几个容易踩的坑第一个坑是只看重复ACK数量不看SACK。SACK开启后很多重复ACK是“良性”的不代表丢包。只看重复ACK数量会误判链路质量。第二个坑是RACK的时间余量设得太小。在无线或者多路径链路上乱序是常态余量太小会导致大量误判重传反而降低吞吐量。我一般会在干净链路上用1.5倍RTT在乱序链路上用2到2.5倍RTT。第三个坑是忽略ACK延迟合并的影响。有些接收端会把多个ACK合并成一个发送这会导致RACK的时间基准偏移判定失准。排查时要确认接收端的ACK行为必要时调整RACK的基准选择。第四个坑是调优时只看吞吐量不看延迟。有时候吞吐量上去了但延迟抖动变大了这对交互式应用是灾难。调优时要同时看吞吐量和延迟两个指标找到平衡点。6. 从实际项目里攒下来的几点体会我在几个不同的传输优化项目里反复调过重复ACK和RACK的配合最大的体会是这两个机制没有“最优参数”只有“最适合当前链路的参数”。同一条链路白天和晚上的乱序率可能就不一样固定参数很难一直保持最优。一个比较实用的做法是让参数自适应。RACK的时间余量可以根据实时测量的乱序率动态调整重复ACK的阈值也可以根据SACK反馈的乱序程度动态变化。这样不用人工反复调系统自己就能适应链路变化。另一个体会是日志的重要性。重复ACK和RACK的交互发生在毫秒级靠抓包很难看清全貌。在协议栈里埋好日志记录每次重传的触发原因、时间戳、当时的RTT和乱序率排查问题时能省下大量时间。我现在的习惯是任何传输相关的改动第一件事就是把日志埋点加上不然出了问题只能靠猜。最后分享一个小技巧如果你不确定某个重传是重复ACK还是RACK触发的可以在重传触发点加一个临时的计数器分别统计两者的触发次数。跑一段时间后看比例就能大致判断当前链路的主要丢包类型。中间丢包多就优化重复ACK尾部丢包多就优化RACK对症下药比盲目调参有效得多。