6. 丢包检测 (Loss Detection)
QUIC 发送方使用确认 (acknowledgments) 来检测丢失的包,并使用 PTO 来确保接收到确认;参见第 6.2 节。本节提供了这些算法的描述。
如果包丢失,QUIC 传输需要从该丢失中恢复,例如通过重传数据、发送更新的帧或丢弃帧。有关更多信息,请参见 [QUIC-TRANSPORT] 的第 13.3 节。
丢包检测 (loss detection) 是按包编号空间 (packet number space) 分离的,这与 RTT 测量和拥塞控制不同,因为 RTT 和拥塞控制是路径的属性,而丢包检测还依赖于密钥可用性。
6.1. 基于确认的检测 (Acknowledgment-Based Detection)
基于确认的丢包检测实现了 TCP 的快速重传 (Fast Retransmit) [RFC5681]、早期重传 (Early Retransmit) [RFC5827]、前向确认 (Forward Acknowledgment) [FACK]、SACK 丢包恢复 [RFC6675] 和 RACK-TLP [RFC8985] 的精神。本节概述了这些算法在 QUIC 中如何实现。
如果包满足以下所有条件,则声明该包丢失:
-
该包未被确认,在途 (in flight),并且在被确认的包之前发送。
-
该包在被确认的包之前
kPacketThreshold个包发送(第 6.1.1 节),或者它在过去足够长的时间内被发送(第 6.1.2 节)。
确认表明稍后发送的包已交付,包和时间阈值为包重排序提供了一些容忍度。
错误地将包声明为丢失会导致不必要的重传,并可能由于拥塞控制器在检测到丢包时的操作而导致性能下降。实现可以检测虚假重传并增加包或时间重排序阈值,以减少未来的虚假重传和丢包事件。具有自适应时间阈值的实现可以 (MAY) 选择从较小的初始重排序阈值开始,以最小化恢复延迟。
6.1.1. 包阈值 (Packet Threshold)
包重排序阈值 (kPacketThreshold) 的推荐 (RECOMMENDED) 初始值是 3,基于 TCP 丢包检测的最佳实践 [RFC5681] [RFC6675]。为了与 TCP 保持相似,实现不应 (SHOULD NOT) 使用小于 3 的包阈值;参见 [RFC5681]。
某些网络可能表现出更高程度的包重排序,导致发送方检测到虚假丢失。此外,包重排序在 QUIC 中可能比 TCP 更常见,因为可以观察和重排序 TCP 包的网络元素无法对 QUIC 这样做,而且 QUIC 包编号是加密的。在虚假检测到丢包后增加重排序阈值的算法,例如 RACK [RFC8985],已被证明在 TCP 中很有用,并且预计在 QUIC 中至少同样有用。
6.1.2. 时间阈值 (Time Threshold)
一旦同一包编号空间内的稍后包已被确认,端点应 (SHOULD) 在先前的包在过去阈值时间量内发送时声明它丢失。为了避免过早地将包声明为丢失,此时间阈值必须 (MUST) 至少设置为本地计时器粒度,如 kGranularity 常量所指示。时间阈值为:
max(kTimeThreshold * max(smoothed_rtt, latest_rtt), kGranularity)
如果在最大确认包之前发送的包尚不能被声明为丢失,则应 (SHOULD) 为剩余时间设置计时器。
使用 max(smoothed_rtt, latest_rtt) 可以防止以下两种情况:
-
最新的 RTT 样本低于平滑的 RTT,可能是由于重排序导致确认遇到了较短的路径;
-
最新的 RTT 样本高于平滑的 RTT,可能是由于实际 RTT 持续增加,但平滑的 RTT 尚未赶上。
推荐 (RECOMMENDED) 的时间阈值 (kTimeThreshold),表示为 RTT 乘数,为 9/8。推荐 (RECOMMENDED) 的计时器粒度值 (kGranularity) 为 1 毫秒。
注意: TCP 的 RACK [RFC8985] 指定了一个稍大的阈值,相当于 5/4,用于类似的目的。QUIC 的经验表明 9/8 效果很好。
实现可以 (MAY) 尝试绝对阈值、来自先前连接的阈值、自适应阈值或包含 RTT 变异的阈值。较小的阈值会降低重排序弹性并增加虚假重传,而较大的阈值会增加丢包检测延迟。
6.2. 探测超时 (Probe Timeout)
探测超时 (PTO) 会在确认触发包 (ack-eliciting packets) 在预期的时间段内未被确认或服务器可能尚未验证客户端地址时触发发送一个或两个探测数据报。PTO 使连接能够从尾部包或确认的丢失中恢复。
与丢包检测一样,PTO 是按包编号空间 (per packet number space) 的。也就是说,每个包编号空间计算一个 PTO 值。
PTO 计时器到期事件不表示包丢失,并且不得 (MUST NOT) 导致先前未确认的包被标记为丢失。当接收到新确认包的确认时,丢包检测按照包和时间阈值机制进行;参见第 6.1 节。
QUIC 中使用的 PTO 算法实现了 TCP 的尾部丢失探测 (Tail Loss Probe) [RFC8985]、RTO [RFC5681] 和 F-RTO 算法 [RFC5682] 的可靠性功能。超时计算基于 TCP 的 RTO 周期 [RFC6298]。
6.2.1. 计算 PTO (Computing PTO)
当传输确认触发包时,发送方按如下方式为 PTO 周期安排计时器:
PTO = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay
PTO 周期是发送方应该等待已发送包的确认的时间量。此时间段包括估计的网络 RTT (smoothed_rtt)、估计中的变异 (4*rttvar) 和 max_ack_delay,以考虑接收方可能延迟发送确认的最长时间。
当为 Initial 或 Handshake 包编号空间设置 PTO 时,PTO 周期计算中的 max_ack_delay 设置为 0,因为预期对等方不会故意延迟这些包;参见 [QUIC-TRANSPORT] 的第 13.2.1 节。
PTO 周期必须 (MUST) 至少为 kGranularity,以避免计时器立即到期。
当多个包编号空间中的确认触发包在途时,计时器必须 (MUST) 设置为 Initial 和 Handshake 包编号空间的较早值。
端点在握手确认之前不得 (MUST NOT) 为应用数据包编号空间设置其 PTO 计时器。这样做可以防止端点在对等方还没有密钥来处理它们或端点还没有密钥来处理其确认时在包中重传信息。例如,当客户端向服务器发送 0-RTT 包时会发生这种情况;它这样做时不知道服务器是否能够解密它们。类似地,当服务器在确认客户端已验证服务器证书并因此可以读取这些 1-RTT 包之前发送 1-RTT 包时,也会发生这种情况。
每次发送或确认确认触发包时,或者当 Initial 或 Handshake 密钥被丢弃时([QUIC-TLS] 的第 4.9 节),发送方应 (SHOULD) 重启其 PTO 计时器。这确保 PTO 始终基于 RTT 的最新估计设置,并针对跨包编号空间的正确包。
当 PTO 计时器到期时,PTO 退避必须 (MUST) 增加,导致 PTO 周期设置为其当前值的两倍。当接收到确认时,PTO 退避因子会被重置,除非在以下情况下。服务器在握手期间响应包可能需要比平时更长的时间。为了保护这样的服务器免受重复的客户端探测,在尚不确定服务器已完成验证客户端地址的客户端上不会重置 PTO 退避。也就是说,客户端在接收 Initial 包中的确认时不会重置 PTO 退避因子。
这种发送方速率的指数降低很重要,因为连续的 PTO 可能是由于严重拥塞导致的包或确认丢失引起的。即使在多个包编号空间中有确认触发包在途,PTO 的指数增加也会跨所有空间发生,以防止网络上的过度负载。例如,Initial 包编号空间中的超时会使 Handshake 包编号空间中的超时长度加倍。
连续 PTO 到期的总时间长度受空闲超时 (idle timeout) 限制。
如果为时间阈值丢包检测设置了计时器,则不得 (MUST NOT) 设置 PTO 计时器;参见第 6.1.2 节。在大多数情况下,为时间阈值丢包检测设置的计时器会比 PTO 计时器更早到期,并且不太可能虚假重传数据。
6.2.2. 握手和新路径 (Handshakes and New Paths)
在同一网络上恢复的连接可以 (MAY) 使用先前连接的最终平滑 RTT 值作为恢复连接的初始 RTT。当没有先前的 RTT 可用时,初始 RTT 应 (SHOULD) 设置为 333 毫秒。这导致握手以 1 秒的 PTO 开始,如 TCP 的初始 RTO 推荐的那样;参见 [RFC6298] 的第 2 节。
连接可以 (MAY) 使用发送 PATH_CHALLENGE 和接收 PATH_RESPONSE 之间的延迟来为新路径设置初始 RTT(参见附录 A.2 中的 kInitialRtt),但该延迟不应 (SHOULD NOT) 被视为 RTT 样本。
当 Initial 密钥和 Handshake 密钥被丢弃时(参见第 6.4 节),任何 Initial 包和 Handshake 包都不能再被确认,因此它们从在途字节 (bytes in flight) 中移除。当 Initial 或 Handshake 密钥被丢弃时,PTO 和丢包检测计时器必须 (MUST) 被重置,因为丢弃密钥表示前向进展,并且丢包检测计时器可能已经为现在丢弃的包编号空间设置。
6.2.2.1. 地址验证之前 (Before Address Validation)
在服务器验证路径上的客户端地址之前,它可以发送的数据量限制为接收到的数据量的三倍,如 [QUIC-TRANSPORT] 的第 8.1 节所述。如果无法发送其他数据,则在从客户端接收到数据报之前,服务器的 PTO 计时器不得 (MUST NOT) 被设置,因为在 PTO 上发送的包计入反放大限制。
当服务器从客户端接收到数据报时,放大限制增加并且服务器重置 PTO 计时器。如果 PTO 计时器随后设置为过去的时间,则立即执行。这样做可以避免在对完成握手至关重要的包之前发送新的 1-RTT 包。特别是,当 0-RTT 被接受但服务器未能验证客户端的地址时,可能会发生这种情况。
由于服务器可能被阻塞直到从客户端接收到更多数据报,因此客户端有责任发送包来解除对服务器的阻塞,直到它确定服务器已完成其地址验证(参见 [QUIC-TRANSPORT] 的第 8 节)。也就是说,如果客户端尚未收到其任何 Handshake 包的确认并且握手未确认(参见 [QUIC-TLS] 的第 4.1.2 节),则客户端必须 (MUST) 设置 PTO 计时器,即使没有包在途。当 PTO 触发时,如果客户端有 Handshake 密钥,则客户端必须 (MUST) 发送 Handshake 包,否则它必须 (MUST) 在有效载荷至少为 1200 字节的 UDP 数据报中发送 Initial 包。
6.2.3. 加速握手完成 (Speeding up Handshake Completion)
当服务器接收到包含重复 CRYPTO 数据的 Initial 包时,它可以假设客户端没有接收到服务器在 Initial 包中发送的所有 CRYPTO 数据,或者客户端的估计 RTT 太小。当客户端在获得 Handshake 密钥之前接收到 Handshake 或 1-RTT 包时,它可能假设服务器的某些或所有 Initial 包丢失了。
为了在这些条件下加速握手完成,端点可以 (MAY) 在每个连接的有限次数内,在 PTO 到期之前发送包含未确认 CRYPTO 数据的包,但须遵守 [QUIC-TRANSPORT] 第 8.1 节中的地址验证限制。对每个连接最多执行一次此操作就足以快速从单个包丢失中恢复。始终重传包以响应接收它无法处理的包的端点有创建无限包交换的风险。
端点还可以使用合并包(参见 [QUIC-TRANSPORT] 的第 12.2 节)来确保每个数据报至少引发一次确认。例如,客户端可以将包含 PING 和 PADDING 帧的 Initial 包与 0-RTT 数据包合并,服务器可以将包含 PING 帧的 Initial 包与其第一次飞行中的一个或多个包合并。
6.2.4. 发送探测包 (Sending Probe Packets)
当 PTO 计时器到期时,发送方必须 (MUST) 在包编号空间中作为探测发送至少一个确认触发包。端点可以 (MAY) 发送最多两个包含确认触发包的全尺寸数据报,以避免由于单个丢失的数据报而导致昂贵的连续 PTO 到期,或传输来自多个包编号空间的数据。在 PTO 上发送的所有探测包必须 (MUST) 是确认触发的。
除了在计时器到期的包编号空间中发送数据外,发送方应 (SHOULD) 从具有在途数据的其他包编号空间发送确认触发包,如果可能的话合并包。当服务器同时有 Initial 和 Handshake 数据在途,或者当客户端同时有 Handshake 和应用数据在途时,这特别有价值,因为对等方可能只有两个包编号空间之一的接收密钥。
如果发送方想在 PTO 上引发更快的确认,它可以跳过一个包编号以消除确认延迟。
端点应 (SHOULD) 在 PTO 到期时发送的包中包含新数据。如果无法发送新数据,则可以 (MAY) 发送先前发送的数据。实现可以 (MAY) 使用替代策略来确定探测包的内容,包括基于应用程序的优先级发送新的或重传的数据。
发送方可能没有新的或先前发送的数据要发送。例如,考虑以下事件序列:在 STREAM 帧中发送新的应用数据,被视为丢失,然后在新包中重传,然后确认原始传输。当没有数据要发送时,发送方应 (SHOULD) 在单个包中发送 PING 或其他确认触发帧,重新设置 PTO 计时器。
或者,发送方可以 (MAY) 将仍在途的任何包标记为丢失,而不是发送确认触发包。这样做可以避免发送额外的包,但会增加过于激进地声明丢包的风险,从而导致拥塞控制器进行不必要的速率降低。
连续的 PTO 周期呈指数增长,因此,随着包继续在网络中被丢弃,连接恢复延迟呈指数增长。在 PTO 到期时发送两个包可以提高对包丢失的弹性,从而降低连续 PTO 事件的概率。
当 PTO 计时器多次到期并且无法发送新数据时,实现必须在每次发送相同有效载荷还是发送不同有效载荷之间做出选择。每次发送相同的有效载荷可能更简单,并确保最高优先级的帧首先到达。每次发送不同的有效载荷可以减少虚假重传的机会。
6.3. 处理重试包 (Handling Retry Packets)
Retry 包会导致客户端发送另一个 Initial 包,有效地重新启动连接过程。Retry 包表明 Initial 包已被接收但未处理。Retry 包不能被视为确认,因为它不表示包已被处理或指定包编号。
接收 Retry 包的客户端会重置拥塞控制和丢包恢复状态,包括重置任何挂起的计时器。保留其他连接状态,特别是加密握手消息;参见 [QUIC-TRANSPORT] 的第 17.2.5 节。
客户端可以 (MAY) 将从发送第一个 Initial 包到接收 Retry 或版本协商包的时间段计算为到服务器的 RTT 估计。客户端可以 (MAY) 使用此值代替其默认的初始 RTT 估计。
6.4. 丢弃密钥和包状态 (Discarding Keys and Packet State)
当 Initial 和 Handshake 包保护密钥被丢弃时(参见 [QUIC-TLS] 的第 4.9 节),使用这些密钥发送的所有包都不能再被确认,因为无法处理它们的确认。发送方必须 (MUST) 丢弃与这些包相关的所有恢复状态,并且必须 (MUST) 将它们从在途字节计数中移除。
端点一旦开始交换 Handshake 包就停止发送和接收 Initial 包;参见 [QUIC-TRANSPORT] 的第 17.2.2.1 节。此时,所有在途 Initial 包的恢复状态都被丢弃。
当 0-RTT 被拒绝时,所有在途 0-RTT 包的恢复状态都被丢弃。
如果服务器接受 0-RTT,但不缓冲在 Initial 包之前到达的 0-RTT 包,则早期的 0-RTT 包将被声明丢失,但这预计是不常见的。
预计密钥会在用它们加密的包被确认或声明丢失后的某个时间被丢弃。但是,一旦 Handshake 和 1-RTT 密钥被证明对客户端和服务器都可用,Initial 和 Handshake 机密就会被丢弃;参见 [QUIC-TLS] 的第 4.9.1 节。