TCP的可靠性建立在重传之上,而重传的准确性取决于丢包检测。如果检测过慢,发送端会长时间停等,吞吐量骤降;如果检测过快,就可能把乱序到达的包误判为丢失,重复发送本已送达的数据。RACK(Relying on Acknowledgment Packet)正是为了解决这两类问题而生,它引入了基于时间的丢包推断逻辑,取代了RFC 3517 SACK-based loss recovery和FACK等传统启发式算法。Linux内核从4.15开始引入RACK,到5.3版本默认启用,如今已成为BBR、CUBIC等拥塞控制算法的标配丢包检测层。

一、传统丢包检测的痛点与RACK的设计动机
在RACK出现之前,Linux TCP的丢包检测主要依赖三个机制:快速重传(收到3个重复ACK)、SACK记分牌分析(RFC 3517的dupthresh判断)以及RTO超时重传。这些机制都以重复ACK计数为核心,即“收到多少个针对某个包的重复确认”来推断丢包。这种思路在高时延、高乱序的网络中问题很大。
举例来说,如果网络中存在明显的乱序交付(比如多路径路由、链路聚合),一个序号为1000的包可能比序号为2000的包晚到。传统算法看到2000已被SACK确认,而1000未确认,就很可能在凑够重复ACK后误判1000已丢失并重传,造成带宽浪费。反过来,在一个只有少量数据包在途的流中(比如交互式请求响应流量),3个重复ACK可能迟迟凑不齐,丢包只能等RTO超时,而一次RTO往往意味着数百毫秒的停顿和拥塞窗口坍塌到1。
RACK的核心洞察是:当一个数据包比另一个更晚发送的包更早被确认时,说明更早发送的那个包大概率已经丢失。它不再数重复ACK,而是记录每个包的发送时间戳,并用最新收到的ACK中携带的时间信息计算一个“推断时间窗口”。如果一个包在比它晚发送的所有包都被确认之后,经过了一个足够保守的时间余量仍未被确认,RACK就判定它丢失并触发重传。这个时间余量默认取RTT的四分之一,兼顾了乱序容忍和检测速度。
二、RACK的工作原理与状态维护
RACK的实现围绕几个关键状态展开。发送端为每个已发送未确认的数据包维护发送时间戳xmit_time,同时维护全局变量rack.xmit_time(最近被确认的包中最晚的发送时间)和rack.rtt(该包对应的RTT采样)。每当收到一个新的ACK或SACK,RACK执行以下推断:
/* RACK丢包推断的伪代码 */
void rack_detect_loss(struct sock *sk)
{
/* 超时时间 = 最近确认包的发送时间 + RTT + 余量 */
u32 timeout = rack.xmit_time + rack.rtt + (rack.rtt >> 2);
skb_for_each_sent(skb, sk) {
if (skb->sacked) /* 已被SACK确认,跳过 */
continue;
if (skb->xmit_time < rack.xmit_time) {
/* 该包比已确认的包更早发送,判定丢失 */
mark_lost(skb);
} else if (tcp_time_stamp + skb->xmit_time >= timeout) {
/* 发送时间接近但可能乱序,挂一个定时器再等 */
arm_reordering_timer(skb, timeout);
}
}
}这段伪代码体现了两个分支:对于明确比最新确认包更早发送却未被确认的包,直接标记为丢失;对于发送时间略晚、可能是乱序的情况,RACK设置一个重排序定时器(reordering timer),到期后重新检查。这个定时器替代了传统方案中的部分RTO职责,可以在不满足重复ACK条件时也及时发现丢包。
值得一提的是时间余量的自适应调整。如果RACK重传后发现误判(重传的包收到ACK时发现原来的包其实到了),说明重排序程度比预期大,RACK会将时间余量加倍,学习网络的真实乱序程度;反之如果没有误判,余量会缓慢收缩,恢复检测灵敏度。这种机制让RACK能自动适应有线到无线各种网络环境。
三、RACK-TLP配合:解决尾部丢包难题
RACK单独工作已经比SACK-based recovery精确得多,但它还有一个天生的盲区:流的尾部丢包。如果连接发送的最后两个包丢了,之后再也没有新数据发出,接收端不会有任何重复ACK返回,RACK无从推断。这正是TLP(Tail Loss Probe,尾部丢包探测)要解决的问题。
TLP的思想是:当发送端在一段时间内没有收到ACK且没有新数据可发时,主动重传(或发送一个纯ACK包凑数)序号最大的那个未确认包。如果接收端确实丢了它,会立即回ACK暴露丢包;如果没丢,接收端也会对重复数据回应ACK,发送端据此确认包其实已到。RACK和TLP在Linux内核中是深度耦合的:TLP的探测定时器由RACK的重排序定时器框架统一驱动,探测触发的条件之一就是RACK认为在途包中最高序号的数据可能丢失。
两者配合后的收益非常直观。尾部丢包场景下,传统TCP必须等待一个完整RTO(最小通常200毫秒),而RACK+TLP通常在一个PTO(Probe Timeout,约为max(2*RTT, 1.5*SRTT+TCP_RTO_MIN))内就能发现问题并恢复,交互式请求的尾延迟可以下降一个数量级。对HTTP短连接、RPC调用这类小包流场景,这种改进尤为显著。
四、实践部署:如何启用、调参与验证RACK效果
在较新的Linux内核上RACK默认开启,也可以通过sysctl显式控制。相关的开关主要有两个:
# 查看当前RACK与TLP状态(1为开启) sysctl net.ipv4.tcp_recovery # 显式启用RACK与TLP sysctl -w net.ipv4.tcp_recovery=1 # 如果需要关闭RACK回到旧的SACK推断逻辑 sysctl -w net.ipv4.tcp_recovery=0
验证RACK效果最直接的方法是构造丢包与乱序环境进行对比测试。可以用netem在发送端网卡注入丢包和乱序:
# 在eth0上注入2%丢包 + 10ms乱序抖动 tc qdisc add dev eth0 root netem loss 2% delay 10ms reorder 5% 50% # 使用ss观察重传统计 ss -tin dst 192.168.0.1
观察ss -ti输出时重点关注几个计数:retrans总重传次数、reordering检测到的重排序事件数、tlp触发次数,以及rto超时次数。启用RACK后合理的现象是:RTO次数大幅下降,TLP计数上升(大量尾部丢包由探测兜住),整体重传率下降(误判减少)。如果配合BBR使用,由于BBR自身的丢包恢复也依赖RACK-TLP,这些计数同样是评估BBR部署质量的重要指标。
需要注意的一点是,RACK依赖发送端时间戳的精度。如果服务器时钟抖动严重或者使用了某些做了时间戳劣化的虚拟化环境,RACK的推断余量会自动放大,检测变迟钝。生产环境部署前建议确认TSOPT(Timestamps option)协商正常,并关注tcp_rack_reo_timeout相关信息,必要时通过内核日志排查定时器行为。
五、RACK与FACK、RFC 3517的对比总结
从演进脉络看,RACK并不是第一个尝试改进丢包检测的方案。FACK曾经是Linux默认的重排序处理算法,它利用SACK信息把重排序窗口显式建模出来,但本质仍是基于序号空间的判断,对无线网络中常见的突发丢包模式适应性不佳。RFC 3517定义的SACK-based loss recovery则规定了一系列基于重复ACK计数的推断规则,在深度乱序场景下容易产生不必要的重传。
RACK把这些基于计数的启发式全部替换为基于时间的单一推断规则,好处是模型简单、行为可预测,并且天然兼容SACK信息的输入。标准化的成果体现在RFC 8985中,该文档完整定义了RACK-TLP的算法细节,包括定时器管理、RTT采样和与拥塞控制的交互。对新实现的协议栈而言,实现RACK-TLP后无需再单独实现FACK、ER等一堆零散算法,降低了复杂度。
总结来看,RACK的贡献可以概括为三点:用时间维度替代计数维度做丢包推断,统一了乱序处理与丢包恢复;配合TLP消灭了尾部丢包必须等RTO的痛点;通过与BBR等新拥塞控制算法的协同,让TCP在无线和高时延网络中的表现更稳定。对于仍在运行老内核或自研协议栈的团队,将丢包检测升级到RACK-TLP通常是一项投入小、收益明确的改进。