6. 丢包检测 (Loss Detection)
QUIC 发送方使用确认 (acknowledgment) 检测丢包, 并使用 Probe Timeout (PTO) 确保能够收到确认; 见 第 6.2 节. 本节描述这些算法.
如果一个 packet 丢失, QUIC transport 需要从该丢失中恢复, 例如重传数据、发送更新后的 frame, 或放弃该 frame. 更多信息见 [QUIC-TRANSPORT] 第 13.3 节.
与 RTT 测量和拥塞控制不同, 丢包检测按 packet number space 分开执行. RTT 和拥塞控制是路径属性, 而丢包检测还依赖密钥可用性.
6.1. 基于确认的检测
基于确认的丢包检测体现了 TCP Fast Retransmit [RFC5681]、Early Retransmit [RFC5827]、Forward Acknowledgment [FACK]、SACK loss recovery [RFC6675] 和 RACK-TLP [RFC8985] 的思想. 本节概述这些算法在 QUIC 中如何实现.
如果一个 packet 满足以下所有条件, 就声明为丢失:
- 该 packet 未被确认, 仍在 flight 中, 且发送时间早于某个已确认 packet.
- 该 packet 比某个已确认 packet 早发送了
kPacketThreshold个 packet (第 6.1.1 节), 或者它已经在足够久以前发送 (第 6.1.2 节).
确认信息表明较晚发送的 packet 已经交付, 而 packet 阈值和时间阈值为 packet 重排序提供一定容忍度.
错误地把 packet 声明为丢失会导致不必要的重传, 并可能因为拥塞控制器检测到丢包后采取动作而降低性能. 实现可以检测虚假重传, 并提高 packet 或时间重排序阈值, 以减少后续虚假重传和丢包事件. 采用自适应时间阈值的实现可以选择较小的初始重排序阈值, 以降低恢复延迟.
6.1.1. Packet 阈值
基于 TCP 丢包检测最佳实践 [RFC5681] [RFC6675], packet 重排序阈值 kPacketThreshold 的推荐初始值为 3. 为保持与 TCP 相似, 实现不应使用小于 3 的 packet 阈值; 见 [RFC5681].
某些网络可能表现出更高程度的 packet 重排序, 导致发送方检测到虚假丢包. 此外, QUIC 中 packet 重排序可能比 TCP 更常见, 因为能观察并重排 TCP packet 的网络元素无法以同样方式处理 QUIC, 且 QUIC packet number 是加密的. 在 TCP 中, 类似 RACK [RFC8985] 的算法会在检测到虚假丢包后提高重排序阈值, 已被证明有用; 预期这些算法在 QUIC 中至少同样有用.
6.1.2. 时间阈值
一旦同一 packet number space 中较晚发送的 packet 被确认, endpoint 应当在较早 packet 的发送时间已经早于某个阈值时声明其丢失. 为避免过早声明丢包, 该时间阈值必须至少为本地计时器粒度, 即 kGranularity 常量表示的值. 时间阈值为:
max(kTimeThreshold * max(smoothed_rtt, latest_rtt), kGranularity)
如果最大已确认 packet 之前发送的 packet 还不能声明为丢失, 则应为剩余时间设置 timer.
使用 max(smoothed_rtt, latest_rtt) 可防护以下两种情况:
- 最新 RTT 样本低于 smoothed RTT, 可能是由于重排序导致 acknowledgment 经过更短路径.
- 最新 RTT 样本高于 smoothed RTT, 可能是实际 RTT 持续升高, 但 smoothed RTT 还未跟上.
推荐时间阈值 kTimeThreshold 以 RTT 倍数表示, 值为 9/8. 推荐计时器粒度 kGranularity 为 1 millisecond.
注意: TCP 的 RACK [RFC8985] 为类似目的指定了稍大的阈值, 等价于 5/4. QUIC 经验表明 9/8 效果良好.
实现可以试验绝对阈值、来自先前连接的阈值、自适应阈值, 或包含 RTT variation 的阈值. 较小阈值会降低对重排序的抵抗力并增加虚假重传; 较大阈值会增加丢包检测延迟.
6.2. 探测超时 (Probe Timeout)
当 ack-eliciting packet 未在预期时间内得到确认, 或服务器可能尚未验证客户端地址时, Probe Timeout (PTO) 会触发发送一个或两个 probe datagram. PTO 使连接能够从尾部 packet 或 acknowledgment 丢失中恢复.
与丢包检测一样, PTO 按 packet number space 分开. 也就是说, 每个 packet number space 都会计算一个 PTO 值.
PTO timer 过期事件并不表示 packet 丢失, 且禁止导致之前未确认 packet 被标记为丢失. 当新发送的 ack-eliciting packet 收到 acknowledgment 时, 丢包检测会按 packet 阈值和时间阈值机制继续执行; 见 第 6.1 节.
QUIC 使用的 PTO 算法实现了 TCP Tail Loss Probe [RFC8985]、RTO [RFC5681] 和 F-RTO [RFC5682] 的可靠性功能. 超时计算基于 TCP RTO period [RFC6298].
6.2.1. 计算 PTO
发送 ack-eliciting packet 时, 发送方按以下 PTO period 设置 timer:
PTO = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay
PTO period 是发送方等待已发送 packet 确认的时间. 该时间包括估计的网络 RTT (smoothed_rtt)、估计变化量 (4*rttvar) 以及 max_ack_delay, 用于计入接收方可能延迟发送 acknowledgment 的最长时间.
当 PTO 为 Initial 或 Handshake packet number space 设置时, PTO period 计算中的 max_ack_delay 设置为 0, 因为预期对端不会有意延迟这些 packet; 见 [QUIC-TRANSPORT] 第 13.2.1 节.
PTO period 必须至少为 kGranularity, 以避免 timer 立即过期.
当多个 packet number space 中都有 ack-eliciting packet 在 flight 中时, timer 必须设置为 Initial 和 Handshake packet number space 中较早的值.
在 handshake confirmed 之前, endpoint 禁止为 Application Data packet number space 设置 PTO timer. 这样可以防止 endpoint 重传某些 packet 中的信息, 因为对端可能还没有处理它们的密钥, 或 endpoint 还没有处理其 acknowledgment 的密钥. 例如, 客户端向服务器发送 0-RTT packet 时, 并不知道服务器是否能解密它们. 类似地, 服务器在确认客户端已经验证服务器证书前发送 1-RTT packet 时, 客户端可能还无法读取这些 1-RTT packet.
每当发送或确认 ack-eliciting packet, 或 Initial/Handshake key 被丢弃时, 发送方应当重启 PTO timer, 见 [QUIC-TLS] 第 4.9 节. 这确保 PTO 始终基于最新 RTT 估计, 并且针对正确的 packet number space 中的正确 packet 设置.
当 PTO timer 过期时, PTO backoff 必须增加, 从而使 PTO period 变为当前值的两倍. 收到 acknowledgment 时会重置 PTO backoff 因子, 但有一个例外. 服务器在 handshake 期间响应 packet 可能比其他时候更慢. 为保护这样的服务器免受客户端重复探测, 尚未确定服务器已经完成客户端地址验证的客户端, 在收到 Initial packet 中的 acknowledgment 时不会重置 PTO backoff 因子.
这种指数降低发送速率的方式很重要, 因为连续 PTO 可能由严重拥塞导致 packet 或 acknowledgment 丢失引起. 即使多个 packet number space 中都有 ack-eliciting packet 在 flight 中, PTO 的指数增长也会跨所有空间发生, 以防止对网络施加过多负载. 例如, Initial packet number space 中一次超时会使 Handshake packet number space 的超时时长加倍.
连续 PTO 过期发生的总时长受 idle timeout 限制.
如果已经为时间阈值丢包检测设置 timer, 则禁止设置 PTO timer; 见 第 6.1.2 节. 在多数情况下, 时间阈值丢包检测 timer 会早于 PTO timer 过期, 且更不容易虚假重传数据.
6.2.2. Handshake 和新路径
在同一网络上恢复的连接可以使用先前连接最终的 smoothed RTT 值作为恢复连接的初始 RTT. 如果没有可用的先前 RTT, initial RTT 应当设置为 333 milliseconds. 这样 handshake 会以 1 second 的 PTO 开始, 与 TCP 初始 RTO 推荐值一致; 见 [RFC6298] 第 2 节.
连接可以使用发送 PATH_CHALLENGE 与接收 PATH_RESPONSE 之间的延迟为新路径设置 initial RTT, 见附录 A.2 中的 kInitialRtt, 但该延迟不应被视为 RTT sample.
当 Initial 和 Handshake key 被丢弃时, 见 第 6.4 节, 所有 Initial packet 和 Handshake packet 都不再能被确认, 因此会从 bytes in flight 中移除. PTO 和丢包检测 timer 必须重置, 因为丢弃 key 表示连接取得前向进展, 而丢包检测 timer 可能是为现已丢弃的 packet number space 设置的.
6.2.2.1. 地址验证之前
在服务器验证路径上的客户端地址之前, 服务器可发送的数据量限制为已接收数据量的三倍, 如 [QUIC-TRANSPORT] 第 8.1 节所规定. 如果不能发送更多数据, 服务器的 PTO timer 禁止 armed, 直到从客户端收到 datagram, 因为 PTO 发送的 packet 也计入 anti-amplification limit.
当服务器从客户端收到 datagram 时, amplification limit 增加, 服务器重置 PTO timer. 如果 PTO timer 随后设置到过去的时间点, 就立即执行. 这样可以避免在完成 handshake 关键 packet 之前发送新的 1-RTT packet. 特别是在接受 0-RTT 但服务器未能验证客户端地址时, 可能发生这种情况.
由于服务器可能一直被阻塞直到从客户端收到更多 datagram, 因此客户端负责发送 packet 来解除服务器阻塞, 直到它确定服务器已经完成地址验证, 见 [QUIC-TRANSPORT] 第 8 节. 也就是说, 如果客户端尚未收到任何 Handshake packet 的 acknowledgment 且 handshake 尚未确认, 则客户端必须设置 PTO timer, 即使没有 packet 在 flight 中, 见 [QUIC-TLS] 第 4.1.2 节. 当 PTO 触发时, 如果客户端拥有 Handshake key, 必须发送 Handshake packet; 否则必须在 UDP datagram 中发送 payload 至少 1200 bytes 的 Initial packet.
6.2.3. 加速 Handshake 完成
当服务器收到包含重复 CRYPTO 数据的 Initial packet 时, 可以假定客户端没有收到服务器在 Initial packet 中发送的全部 CRYPTO 数据, 或客户端估计的 RTT 太小. 当客户端在获得 Handshake key 之前收到 Handshake 或 1-RTT packet 时, 可以假定服务器的部分或全部 Initial packet 已经丢失.
为在这些情况下加速 handshake 完成, endpoint 可以在每个连接内有限次数地, 早于 PTO 过期发送包含未确认 CRYPTO 数据的 packet, 但必须受 [QUIC-TRANSPORT] 第 8.1 节的地址验证限制约束. 每个连接最多这样做一次, 就足以从单个 packet 丢失中快速恢复. 如果 endpoint 总是在收到无法处理的 packet 后重传 packet, 就有产生无限 packet 交换的风险.
Endpoint 还可以使用 coalesced packet, 见 [QUIC-TRANSPORT] 第 12.2 节, 确保每个 datagram 至少引发一个 acknowledgment. 例如, 客户端可以把包含 PING 和 PADDING frame 的 Initial packet 与 0-RTT data packet 合并; 服务器可以把包含 PING frame 的 Initial packet 与首个 flight 中的一个或多个 packet 合并.
6.2.4. 发送 Probe Packet
当 PTO timer 过期时, 发送方必须在对应 packet number space 中发送至少一个 ack-eliciting packet 作为 probe. Endpoint 可以发送最多两个包含 ack-eliciting packet 的 full-sized datagram, 以避免因单个 datagram 丢失而导致代价高昂的连续 PTO 过期, 或用于从多个 packet number space 传输数据. PTO 发送的所有 probe packet 都必须是 ack-eliciting.
除了在 timer 过期的 packet number space 中发送数据外, 发送方还应当从其他有 in-flight data 的 packet number space 发送 ack-eliciting packet, 如可能则合并 packet. 当服务器同时有 Initial 和 Handshake data 在 flight 中, 或客户端同时有 Handshake 和 Application Data 在 flight 中时, 这特别有价值, 因为对端可能只拥有两个 packet number space 中一个的接收 key.
如果发送方希望 PTO 后更快获得 acknowledgment, 可以跳过一个 packet number, 以消除 acknowledgment delay.
Endpoint 应当在 PTO 过期时发送的 packet 中包含新数据. 如果无法发送新数据, 可以发送之前已发送的数据. 实现可以使用替代策略决定 probe packet 内容, 包括根据应用优先级发送新数据或重传数据.
发送方可能没有新数据或之前发送的数据可发送. 例如, 新应用数据在 STREAM frame 中发送, 被判定丢失, 随后在新 packet 中重传, 接着原始传输又被确认. 当没有数据可发送时, 发送方应当在单个 packet 中发送 PING 或其他 ack-eliciting frame, 并重新 armed PTO timer.
作为替代方案, 发送方可以不发送 ack-eliciting packet, 而是把仍在 flight 中的 packet 标记为丢失. 这样可以避免额外发送 packet, 但会增加虚假声明 packet 丢失的风险, 导致拥塞控制器不必要地降低速率.
连续 PTO period 会指数增长, 因此当网络持续丢弃 packet 时, 连接恢复延迟也会指数增长. PTO 过期时发送两个 packet 可以提高对 packet 丢失的抵抗力, 从而降低连续 PTO 事件概率.
当 PTO timer 多次过期且无法发送新数据时, 实现必须在每次发送相同 payload 或发送不同 payload 之间选择. 发送相同 payload 可能更简单, 并确保最高优先级 frame 先到达. 每次发送不同 payload 则能降低虚假重传概率.
6.3. 处理 Retry Packet
Retry packet 会使客户端发送另一个 Initial packet, 实际上重启连接过程. Retry packet 表示 Initial packet 已收到但未处理. Retry packet 不能视为 acknowledgment, 因为它既不表示 packet 已处理, 也不指定 packet number.
收到 Retry packet 的客户端会重置拥塞控制和丢包恢复状态, 包括重置任何待处理 timer. 其他连接状态会保留, 尤其是 cryptographic handshake messages; 见 [QUIC-TRANSPORT] 第 17.2.5 节.
客户端可以把从发送第一个 Initial packet 到收到 Retry 或 Version Negotiation packet 之间的时间作为到服务器的 RTT 估计. 客户端可以用该值替代默认 initial RTT 估计.
6.4. 丢弃 Key 和 Packet 状态
当 Initial 和 Handshake packet protection key 被丢弃时, 见 [QUIC-TLS] 第 4.9 节, 所有使用这些 key 发送的 packet 都不再能被确认, 因为它们的 acknowledgment 无法处理. 发送方必须丢弃与这些 packet 关联的所有恢复状态, 并必须从 bytes in flight 计数中移除它们.
Endpoint 在开始交换 Handshake packet 后停止发送和接收 Initial packet; 见 [QUIC-TRANSPORT] 第 17.2.2.1 节. 此时, 所有 in-flight Initial packet 的恢复状态都会被丢弃.
当 0-RTT 被拒绝时, 所有 in-flight 0-RTT packet 的恢复状态都会被丢弃.
如果服务器接受 0-RTT, 但不缓冲在 Initial packet 之前到达的 0-RTT packet, 早期 0-RTT packet 会被声明为丢失, 但预期这种情况并不常见.
预期 key 会在用它们加密的 packet 被确认或声明丢失后某个时间点丢弃. 不过, 一旦证明客户端和服务器都已拥有 Handshake 和 1-RTT key, Initial 和 Handshake secret 就会被立即丢弃; 见 [QUIC-TLS] 第 4.9.1 节.