Karn 算法 — ACK 歧义问题

情况 A原报文段丢失 → 重传 → 收到确认

发送方发出 seq=200 的数据报文段,该段在传输中丢失。超时后重传,收到 ACK。此 ACK 实际是对重传报文段的确认。

发送方 A
接收方 B
数据 seq=200
× 在途中丢失
RTO
重传 seq=200
ACK ack=201

⚠ 发送方无法判断

这个 ACK 是对原报文段的确认,还是对重传报文段的确认?

实际上是对重传报文段的确认。若误当作原报文段的 ACK 来测 RTT → RTT 偏大

/* 发送方视角 */
send(seq=200);           // 发送数据
/* 超时 ... */
retransmit(seq=200);     // 重传
recv(ack=201);           // 收到确认
/* ❓ 这个 ACK 对应哪次发送? */
/* 无法用它的时间戳计算 RTT */

情况 BACK 迟到 → 超时重传 → 迟到 ACK 到达

发送方发出 seq=200,接收方正常收到并回 ACK。但 ACK 因网络延迟未能及时到达,触发超时重传。重传后,迟到的原 ACK 才到达。

发送方 A
接收方 B
数据 seq=200
ACK 在路上…
RTO
超时重传!
迟到 ACK ack=201

⚠ 发送方无法判断

这个迟到的 ACK 是对原报文段的确认,还是对重传报文段的确认?

实际上是对原报文段的确认。若误当作重传报文段的 ACK 来测 RTT → RTT 偏小

/* 发送方视角 */
send(seq=200);           // 发送数据
/* 超时,但原 ACK 其实在路上 */
retransmit(seq=200);     // 不必要重传
recv(ack=201);           // 迟到 ACK 到达
/* ❓ 对应原报文还是重传? */
/* 无法用它的时间戳计算 RTT */

Karn 算法 — 基本规则

凡是重传过的报文段,不采用其 RTT 样本。

一旦发生重传,本轮不更新 RTTₛ,也不重新计算 RTO。宁可暂时不更新,也不要用错误样本污染 RTT 估计。


修正规则 — 防止 RTO 永远不更新

如果网络时延突然持续增大:RTO 太小 → 每次都超时重传 → Karn 基本规则拒绝所有 RTT 样本 → RTO 永远不变 → 反复重传。

修正:报文段每重传一次,RTO 翻倍。

RTOⁿᵉʷ = 2 × RTOᵒˡᵈ

这让 RTO 在持续拥塞时快速增长,避免反复重传加重拥塞。等网络恢复正常后,新 RTT 样本会重新纠正 RTO。

/* Karn 算法伪代码 */
function updateRTO(rtt_sample) {
  if (was_retransmitted) {
    /* 基本规则:不用重传样本 */
    /* 修正规则:每次重传 RTO 翻倍 */
    RTO = RTO * 2;
    return;  // 不更新 RTTs 和 RTTd
  }
  /* 正常样本:按 RFC6298 更新 */
  RTTs = (1-α)*RTTs + α*rtt_sample;
  RTTd = (1-β)*RTTd + β*|RTTs-rtt_sample|;
  RTO = RTTs + 4*RTTd;
}

总结:Karn 算法解决的核心矛盾

问题重传后收到 ACK,无法判断是对原报文还是重传报文的确认
错误后果情况 A → RTT 偏大,情况 B → RTT 偏小,都会导致 RTO 计算错误
基本规则重传报文段的 RTT 样本一律不用 → 避免错误样本污染估计
副作用网络持续拥塞时 RTO 永远不更新 → 反复超时重传
修正规则每重传一次 RTO 翻倍 → RTO 随拥塞增长,避免死循环

核心思想:不确定的样本宁可不用;但也不能因为不用就让 RTO 在真实拥塞中僵死。

/* 记忆口诀 */
/* 样本不靠谱 → 不用 */
/* 反复超时 → RTO 翻倍 */
/* 恢复正常 → 重新测量 */
/* 自适应于网络状态 */