3. RTO 计算与自适应算法:Jacobson、Karn 与 RTT 样本采集
RTO(Retransmission Timeout)重传超时时间,是 TCP 可靠传输的命门。设短了,网络一抖就疯狂重传,带宽全浪费;设长了,丢包半天才发现,用户卡到怀疑人生。所以 RTO 必须动态调整,不能写死。
这一节我们深入 RTO 的计算内核。我会把 Jacobson 算法、Karn 算法、RTT 样本采集与平滑这几个核心点串起来讲。你理解透了,以后抓包看到重传,一眼就能判断是 RTO 不合理还是真丢包。
3.1 RTT 样本采集——RTO 的原材料
RTO 不是凭空算的,它依赖 RTT(Round-Trip Time)样本。RTT 就是一个数据包从发出去到收到 ACK 的时间差。
但采集 RTT 样本有个坑:你该拿哪个包和哪个 ACK 配对?
早期实现很简单——每发一个数据段,记下时间 T1;收到 ACK 时,用当前时间 T2 减去 T1。但问题来了:如果发生了重传,这个 ACK 是对应第一次发的包,还是第二次发的?
我在项目中遇到过这样一个案例:某嵌入式设备在弱网环境下 RTO 越算越小,最后疯狂重传,把本来就差的链路彻底打满。查了半天,就是 RTT 样本采集时没处理重传歧义,把重传后的 ACK 当成了原始包的 RTT,导致样本偏小。
3.2 Karn 算法——解决重传歧义
Karn 算法就是专门解决上面那个问题的。它的核心思想很简单:
- 发生重传时,忽略本次 RTT 样本,不参与 RTO 计算
- 直到收到一个没有重传的段的 ACK,才恢复采样
说白了,就是「吃一堑,长一智」——既然分不清,那就不用了。等链路稳定了再说。
但 Karn 算法还有个配套机制:退避(Backoff)。当重传发生时,RTO 会指数增长(通常乘以 2),避免在链路已经拥塞时继续激进重传。
- 重传期间不采集 RTT 样本
- RTO 按指数退避增长
- 收到非重传段的 ACK 后恢复采样,RTO 重新计算
嗯,这里要注意:Karn 算法只是解决了「样本该不该用」的问题,但 RTO 具体怎么算,还得靠 Jacobson 算法。
3.3 Jacobson 算法——RTO 的自适应计算
Jacobson 算法是 TCP RTO 计算的基石。它用指数加权移动平均(EWMA)来平滑 RTT,同时引入偏差项来应对抖动。
公式其实不复杂,我拆开讲:
SRTT = (1 - α) × SRTT + α × RTT_sample
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT_sample|
RTO = SRTT + 4 × RTTVAR
参数说明:
- SRTT:平滑后的 RTT,初始值为第一个 RTT 样本
- RTTVAR:RTT 偏差(抖动),初始值为 RTT_sample / 2
- α:平滑因子,通常取 1/8(0.125)
- β:偏差因子,通常取 1/4(0.25)
- RTO:最终的超时时间,下限通常设为 1 秒,上限 60 秒
为什么是 4 倍的偏差?我个人习惯的理解是:在正态分布假设下,均值 ± 4 倍标准差能覆盖 99.99% 的情况。说白了,就是给网络抖动留足余量。
3.4 算法流程与决策逻辑
把 Jacobson 和 Karn 串起来,完整的 RTO 计算流程是这样的:
3.5 参数对比与调优建议
不同场景下,RTO 参数的最佳取值差异很大。我整理了一张表,方便你对照:
| 参数 | 默认值 | 内网场景 | 跨洲链路 | 移动网络 |
|---|---|---|---|---|
| α(平滑因子) | 1/8 | 1/8 | 1/16(更平滑) | 1/4(更快响应) |
| β(偏差因子) | 1/4 | 1/4 | 1/8 | 1/2 |
| RTOmin | 1s | 200ms | 2s | 1s |
| RTOmax | 60s | 10s | 120s | 60s |
3.6 避坑指南
最后分享几个我踩过的坑:
- 不要忽略初始 RTO:TCP 连接建立时的初始 RTO 通常为 3 秒(RFC 6298)。如果链路 RTT 很大(比如卫星链路),3 秒可能不够,建议根据实测调整。
- RTO 计算不要用整数除法:SRTT 和 RTTVAR 是浮点或定点数,用整数除法会导致精度丢失,RTO 越算越偏。我见过一个嵌入式实现用整数除 8,结果 RTO 永远收敛不到正确值。
- Karn 算法不是银弹:它只是避免了重传歧义,但代价是重传期间没有新样本,RTO 只能靠退避。如果链路持续抖动,RTO 会退避到很大,影响恢复速度。
- 注意 RTT 样本的异常值:偶尔的延迟尖峰(比如 10 秒)会严重拉高 SRTT。我习惯在采集时做简单的异常过滤——超过 3 倍当前 SRTT 的样本直接丢弃。