4. RTO 计算:从 RTT 采样到 Linux 内核实现

RTO(Retransmission Timeout)是 TCP 重传的核心参数。设得太小,网络一抖动就疯狂重传;设得太大,丢包了半天才发现。说白了,RTO 就是 TCP 的「耐心值」—— 怎么算才合适?

我刚开始调优时,总觉得 RTO 就是个固定值。后来在数据中心项目里,一台机器因为 RTO 设置不当,导致大量虚假重传,把整个集群的吞吐都拖垮了。嗯,从那以后,我老老实实把 RTO 计算的每个细节都啃了一遍。

4.1 RTT 采样:原始数据的获取

RTO 计算的第一步,是拿到 RTT(Round-Trip Time)的原始样本。TCP 怎么测量 RTT?

经典做法是:发送一个数据段时,记录下当前时间 t1;收到这个段的 ACK 时,记录时间 t2。RTT = t2 - t1。

但这里有个坑 —— 重传后的 ACK 怎么算?

重传歧义问题:如果数据包重传了,收到的 ACK 对应的是第一次发送还是重传?TCP 无法区分。早期实现直接忽略重传段的 RTT 采样,后来 Karn 算法解决了这个问题。

Karn 算法很简单:重传的数据包不参与 RTT 采样。直到收到一个非重传段的 ACK,才恢复采样。我曾在某款网卡驱动里见过没处理好这个逻辑的 bug,导致 RTO 忽大忽小,排查了两天才定位到。

Linux 内核中,RTT 采样在 tcp_ack() 函数里触发。核心代码如下:

/* net/ipv4/tcp_input.c */
static void tcp_rtt_estimator(struct sock *sk, long mrtt_us)
{
    struct tcp_sock *tp = tcp_sk(sk);
    long m = mrtt_us; /* 本次 RTT 采样值,单位微秒 */

    /* 第一次采样,直接初始化 */
    if (tp->srtt_us == 0) {
        tp->srtt_us = m << 3;    /* srtt = m * 8 */
        tp->mdev_us = m << 1;    /* mdev = m * 2 */
        tp->mdev_max_us = tp->mdev_us;
        tp->rttvar_us = max(tp->mdev_us, rto_min_us(sk));
        return;
    }

    /* 后续采样,平滑更新 */
    tp->srtt_us >>= 3;  /* 先右移 3 位,准备做加权平均 */
    tp->srtt_us += m;    /* srtt = 7/8 * srtt + 1/8 * m */
}

注意这里的单位是微秒(us),不是毫秒。内核里所有 RTT 相关计算都用微秒,精度更高。

4.2 SRTT 计算:平滑的加权平均

单次 RTT 采样抖动太大,不能直接用。我们需要一个平滑后的值 —— SRTT(Smoothed RTT)。

公式很简单:

SRTT = (1 - α) × SRTT + α × RTT_sample

α 是平滑因子,标准推荐值是 1/8(0.125)。为什么是 1/8?

我个人理解:1/8 在二进制里就是右移 3 位,CPU 算起来极快。内核里确实也是这么实现的 —— 用移位代替浮点运算。

参数 标准值 内核实现 说明
α(平滑因子) 1/8 右移 3 位 SRTT 更新权重
β(偏差因子) 1/4 右移 2 位 RTTVAR 更新权重
K(RTO 系数) 4 4 RTO = SRTT + K × RTTVAR

你想想看,如果 α 太大,SRTT 对瞬时抖动太敏感;α 太小,又跟不上网络变化。1/8 是个折中,经过几十年验证,确实好用。

4.3 RTTVAR 计算:偏差的度量

光有平均值不够。网络延迟可能忽高忽低,我们需要知道它的「抖动程度」。这就是 RTTVAR(RTT Variation)。

计算公式:

RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT_sample|

β 是 1/4。注意这里用的是绝对偏差,不是方差。为什么不用方差?

因为方差需要平方运算,CPU 开销大。绝对偏差用移位就能算,效率高得多。内核里是这么实现的:

/* 计算偏差 */
long delta = tp->srtt_us - m;
if (delta < 0)
    delta = -delta;

/* 更新 RTTVAR */
tp->mdev_us += delta;   /* mdev = 3/4 * mdev + 1/4 * |delta| */
tp->mdev_us >>= 1;     /* 这里实际是右移 2 位,等效于 1/4 权重 */
tp->mdev_max_us = max(tp->mdev_us, tp->mdev_max_us);

这里有个细节:mdev_max_us 记录的是 RTTVAR 的最大值。为什么需要最大值?

因为 RTO 要「保守」一点。用最大值而不是当前值,可以避免 RTO 在抖动剧烈时缩得太小。我曾经在无线网络环境下遇到过这个问题 —— 信号波动大,RTTVAR 忽大忽小,RTO 也跟着乱跳。后来发现内核默认就用了 mdev_max_us,这个设计确实老道。

4.4 RTO 公式演进:从 RFC 793 到 RFC 6298

RTO 公式不是一天定型的。它经历了几个阶段:

  1. RFC 793(1981):RTO = SRTT × 2。简单粗暴,但网络抖动时完全不够用。
  2. RFC 1122(1989):引入 RTTVAR,RTO = SRTT + 2 × RTTVAR。开始考虑抖动。
  3. RFC 2988(2000):RTO = SRTT + 4 × RTTVAR。系数从 2 改成 4,更保守。
  4. RFC 6298(2011):沿用 RFC 2988 公式,但明确了 RTO 下限为 1 秒(后来 Linux 改成了 200ms)。

当前 Linux 内核使用的公式:

RTO = SRTT + max(4 × RTTVAR, 2 × RTTVAR + 1 个时钟滴答)

为什么是 4 倍?我个人的理解是:4 倍偏差可以覆盖 99% 以上的正常波动。如果网络延迟符合正态分布,4 倍标准差能覆盖 99.99% 的情况。当然,实际网络不是正态分布,但 4 倍确实是个经验好值。

RTO 下限:Linux 内核中,RTO 最小值是 200ms(HZ=100 时是 20 个 jiffies),最大值是 120 秒。这个下限在数据中心场景下可能偏大,很多调优方案会把它降到 10ms 甚至 1ms。

4.5 Linux 内核实现:代码级解析

我们来看完整的 RTO 计算流程。内核里,每次收到 ACK 都会调用 tcp_rtt_estimator(),然后调用 tcp_set_rto() 更新 RTO。

/* net/ipv4/tcp_input.c */
static void tcp_set_rto(struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    
    /* 计算 RTO = SRTT + 4 × RTTVAR */
    inet_csk(sk)->icsk_rto = tp->srtt_us >> 3;  /* srtt_us 是 8 倍放大,右移 3 还原 */
    inet_csk(sk)->icsk_rto += tp->rttvar_us;    /* 加上 4 × RTTVAR */
    
    /* 确保 RTO 在合法范围内 */
    inet_csk(sk)->icsk_rto = min(inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
    inet_csk(sk)->icsk_rto = max(inet_csk(sk)->icsk_rto, TCP_RTO_MIN);
}

注意 srtt_us 是 8 倍放大的值,所以右移 3 位才是真实 SRTT。rttvar_us 已经是 4 倍 RTTVAR 了,直接加就行。

整个计算流程可以用下图概括:

RTO 计算流程 RTO 计算流程(Linux 内核实现) RTT 采样 Karn 算法:过滤重传采样 SRTT = 7/8×SRTT + 1/8×RTT RTTVAR = 3/4×RTTVAR + 1/4×|Δ| RTO = SRTT + 4 × RTTVAR 裁剪到 [TCP_RTO_MIN, TCP_RTO_MAX] 关键参数 TCP_RTO_MIN = 200ms TCP_RTO_MAX = 120s α = 1/8, β = 1/4 K = 4

最后,RTO 还有个「退避」机制。如果重传后还没收到 ACK,RTO 会指数增长:

/* net/ipv4/tcp_timer.c */
void tcp_retransmit_timer(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    
    /* RTO 退避:每次重传,RTO 翻倍 */
    inet_csk(sk)->icsk_rto = min(inet_csk(sk)->icsk_rto << 1, TCP_RTO_MAX);
    
    /* 启动下一次重传定时器 */
    sk_reset_timer(sk, &sk->sk_timer, jiffies + inet_csk(sk)->icsk_rto);
}

退避到最大值后,TCP 会一直重传直到超时(RTO 达到 120 秒)。这个机制叫「指数退避」,目的是避免网络拥塞时雪上加霜。

调优建议:在数据中心内网(延迟 < 1ms),建议将 TCP_RTO_MIN 降到 10ms。修改方式:sysctl -w net.ipv4.tcp_rto_min=10ms。但注意,广域网场景不要动这个值,否则会引发大量虚假重传。

嗯,RTO 计算这块内容就这些。从采样到平滑,从偏差到退避,每一步都有它的道理。理解这些细节,你就能在性能调优时做出正确的判断 —— 而不是盲目改参数。