RFC 9002 QUIC 丢包检测 2021 年 5 月
Iyengar & Swett 标准跟踪 [页]
流:
互联网工程任务组 (IETF)
RFC:
9002
类别:
标准跟踪
发布:
ISSN:
2070-1721
作者:
J. Iyengar, 编.
Fastly
I. Swett, 编.
Google

RFC 9002

QUIC 丢包检测和拥塞控制

摘要

本文档描述了 QUIC 的丢包检测和拥塞控制机制。

本备忘录的状态

这是一份互联网标准跟踪文档。

本文档是互联网工程任务组 (IETF) 的产物。它代表了 IETF 社区的共识。本文档已经过公开审查,并已由互联网工程指导组 (IESG) 批准发布。有关互联网标准的更多信息可在 RFC 7841 的 第 2 节中找到。

有关本文档当前状态、任何勘误以及如何提供反馈的信息, 可在 https://www.rfc-editor.org/info/rfc9002 获取。

目录

1. 引言

QUIC 是一种安全的通用传输协议,见 [QUIC-TRANSPORT]。本文档描述了 QUIC 的丢包 检测和拥塞 控制机制。

2. 约定和定义

本文档中的关键词“MUST”、“MUST NOT”、 “REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、 “SHOULD NOT”、“RECOMMENDED”、“NOT RECOMMENDED”、“MAY”和“OPTIONAL”,应按 BCP 14 [RFC2119] [RFC8174] 所述进行解释,但仅当它们像此处所示以全部大写形式出现时才如此。

本文档中使用的术语定义如下:

诱发 ACK 的帧:

除 ACK、PADDING 和 CONNECTION_CLOSE 之外的所有帧都被认为是 诱发 ACK 的。

诱发 ACK 的包:

包含诱发 ACK 帧的包会使接收方在最大确认延迟内 发送 ACK,并称为诱发 ACK 的包。

传输中的包:

当包是诱发 ACK 的,或包含 PADDING 帧,并且已经发送但尚未被确认、声明为 丢失或随旧密钥一起丢弃时,这些包被认为处于传输中。

3. QUIC 传输机制的设计

QUIC 中的所有传输都带有包级头部,该头部指示 加密级别并包含包序列号(下文称为 包编号)。如 [QUIC-TRANSPORT] 的 第 12.3 节所述,加密级别 指示包编号空间。在一个连接的生命周期内,包编号在同一包编号空间中 不会重复。包编号在一个空间内按单调递增顺序发送,从而避免歧义。 允许某些包编号从未被使用,从而留下有意的 空隙。

这种设计消除了区分传输和 重传的需要;这从 QUIC 对 TCP 丢包检测机制的 解释中消除了显著的复杂性。

QUIC 包可以包含多个不同类型的帧。恢复 机制确保需要可靠交付的数据和帧得到 确认,或被声明为丢失并按需在新包中发送。 包中所含帧的类型会影响恢复和拥塞控制逻辑:

4. QUIC 和 TCP 之间的相关差异

熟悉 TCP 丢包检测和拥塞控制的读者会发现, 这里的算法与一些知名的 TCP 算法相似。然而,QUIC 和 TCP 之间的 协议差异会导致算法差异。这些协议 差异将在下文简要说明。

4.1. 独立的包编号 空间

QUIC 为每个加密级别使用独立的包编号空间, 但 0-RTT 以及所有代的 1-RTT 密钥使用相同的包 编号空间。独立的包编号空间确保 使用某个加密级别发送的包的确认不会 导致使用另一个加密级别发送的包发生伪 重传。拥塞控制和往返时间 (RTT) 测量在各个包编号空间之间是统一的。

4.2. 单调递增的 包编号

TCP 将发送方的传输顺序与接收方的交付顺序混为一谈, 从而产生重传歧义问题 [RETRANSMISSION]。QUIC 将 传输顺序与交付顺序分离: 包编号指示传输顺序,而交付顺序由 STREAM 帧中的 流偏移确定。

QUIC 的包编号在包编号空间内严格递增, 并直接编码传输顺序。更高的包编号表示 该包发送得更晚,更低的包编号表示 该包发送得更早。当检测到包含诱发 ACK 帧的包丢失时,QUIC 会将必要的帧包含在一个具有 新包编号的新包中,从而消除在收到 ACK 时哪个包被 确认的歧义。因此,可以进行更准确的 RTT 测量,可以轻易检测到伪重传,并且 快速重传等机制可以仅基于 包编号而普遍应用。

这一设计点显著简化了 QUIC 的丢包检测机制。 大多数 TCP 机制会隐式地尝试基于 TCP 序列号推断传输顺序——这是一项并不简单的任务, 尤其是在 TCP 时间戳不可用时。

4.3. 更清晰的丢包时期

QUIC 在包丢失时开始一个丢包时期。当该时期开始后发送的任何 包得到确认时,该丢包时期结束。TCP 会等待序列号空间中的空缺 被填补,因此如果一个段连续多次丢失, 丢包时期可能在几个往返时间内都不会结束。由于二者都应当 每个时期只降低一次拥塞窗口,因此 QUIC 会对每个经历丢包的 往返时间降低一次,而 TCP 可能只会在多个 往返时间内降低一次。

4.4. 不撤销确认

QUIC ACK 帧包含的信息类似于 TCP 选择性 确认 (SACK) [RFC2018] 中的信息。不过, QUIC 不允许撤销包的 确认,这大大简化了双方的实现, 并降低了发送方的内存压力。

4.5. 更多 ACK 范围

QUIC 支持许多 ACK 范围,而 TCP 只有三个 SACK 范围。在 高丢包环境中,这会加快恢复、减少伪重传,并 确保无需依赖超时即可取得前进进展。

4.6. 对延迟确认的显式校正

QUIC 端点会测量从接收包到 发送相应确认之间产生的延迟,从而允许对端维护 更准确的 RTT 估计;见 [QUIC-TRANSPORT] 的 第 13.2 节

4.7. 探测超时代替 RTO 和 TLP

QUIC 使用探测超时(PTO;见第 6.2 节),其定时器基于 TCP 的 重传超时 (RTO) 计算;见 [RFC6298]。QUIC 的 PTO 包含 对端的最大预期确认延迟,而不是使用固定的 最小超时。

与 TCP 的 RACK-TLP 丢包检测算法 [RFC8985] 类似,QUIC 不会 在 PTO 过期时收缩拥塞窗口,因为尾部的单个包 丢失并不表示持续拥塞。相反,QUIC 会在声明持续拥塞时收缩拥塞窗口;见 第 7.6 节。这样做时,QUIC 避免了 不必要的拥塞 窗口降低,也消除了对 Forward RTO-Recovery (F-RTO) [RFC5682] 等校正机制的需要。由于 QUIC 不会 在 PTO 过期时收缩拥塞 窗口,如果在 PTO 过期后仍有可用的拥塞 窗口,QUIC 发送方就不受限制,仍可发送更多 传输中的包。当发送方受应用限制且 PTO 定时器 过期时,就会发生这种情况。与 TCP 的 RTO 机制相比,这在受应用 限制时更激进,但在不受应用限制时相同。

每当定时器过期时,QUIC 都允许探测包临时超过拥塞窗口。

4.8. 最小拥塞 窗口为两个包

TCP 使用一个包作为最小拥塞窗口。然而,该 单个包的丢失意味着发送方需要等待 PTO 才能恢复(第 6.2 节),这 可能比一个 RTT 长得多。发送单个诱发 ACK 的包也 会增加接收方延迟其 确认时产生额外时延的概率。

因此,QUIC 建议最小拥塞窗口为两个 包。虽然这会增加网络负载,但它被认为是安全的,因为 发送方在持续拥塞下仍会以指数方式降低其发送 速率(第 6.2 节)。

4.9. 握手包并非 特殊

TCP 将 SYN 或 SYN-ACK 包的丢失视为持续拥塞, 并将拥塞窗口降低到一个包;见 [RFC5681]。QUIC 将 包含握手数据的包的丢失与其他丢包同等处理。

5. 估算往返时间

在较高层次上,端点将从包发送到 它被确认为止的时间测量为一个 RTT 样本。端点使用 RTT 样本和 对端报告的主机延迟(见 [QUIC-TRANSPORT] 的 第 13.2 节)来生成 网络路径 RTT 的统计描述。端点为每条路径计算 以下三个值:一段时间内的最小值 (min_rtt)、指数加权移动平均值 (smoothed_rtt),以及观测到的 RTT 样本中的平均偏差(在本文档其余部分称为“变动”) (rttvar)。

5.1. 生成 RTT 样本

端点在收到满足以下两个条件的 ACK 帧时生成 RTT 样本:

  • 最大的已确认包编号是新近 确认的,并且
  • 新近确认的包中至少有一个是 诱发 ACK 的。

RTT 样本 latest_rtt 生成为自最大 已确认包发送以来经过的时间:

latest_rtt = ack_time - send_time_of_largest_acked

RTT 样本只使用接收到的 ACK 帧中的 最大已确认包生成。这是因为对端只会为 ACK 帧中的最大已确认包报告确认延迟。虽然报告的 确认延迟不会用于 RTT 样本测量,但它会用于 在后续 smoothed_rtt 和 rttvar 计算中调整 RTT 样本(第 5.3 节)。

为避免为单个包生成多个 RTT 样本,如果 ACK 帧没有新近 确认最大的已确认包,则SHOULD NOT 使用该 ACK 帧更新 RTT 估计。

在收到未新近确认至少一个诱发 ACK 包的 ACK 帧时,MUST NOT 生成 RTT 样本。对端通常不会 在只收到非诱发 ACK 的包时发送 ACK 帧。因此, 只包含非诱发 ACK 包确认的 ACK 帧 可能包含任意大的 ACK Delay 值。忽略 这样的 ACK 帧可以避免后续 smoothed_rtt 和 rttvar 计算中的复杂性。

当一个 RTT 内收到多个 ACK 帧时,发送方可能会在每个 RTT 中 生成多个 RTT 样本。如 [RFC6298] 所建议,这样做可能会导致 smoothed_rtt 和 rttvar 中的历史不足。确保 RTT 估计 保留足够历史仍是一个开放研究问题。

5.2. 估算 min_rtt

min_rtt 是发送方对给定网络 路径在一段时间内观测到的最小 RTT 的估计。在本文档中,min_rtt 由丢包检测 用来拒绝不合理地小的 RTT 样本。

在第一个 RTT 样本上,min_rtt MUST 设置为 latest_rtt。在所有其他 样本上,min_rtt MUST 设置为 min_rtt 和 latest_rtt(第 5.1 节)中的较小者。

端点在计算 min_rtt 时仅使用本地观测到的时间, 并且不会根据对端报告的确认延迟进行调整。这样做允许 端点完全基于自身观测为 smoothed_rtt 设置下界 (见 第 5.3 节),并限制 对端错误报告延迟造成的潜在低估。

网络路径的 RTT 可能会随时间变化。如果路径的实际 RTT 降低,min_rtt 会在第一个较低样本上立即适应。如果 路径的实际 RTT 增加,min_rtt 则不会适应它,从而允许 小于新 RTT 的未来 RTT 样本被纳入 smoothed_rtt。

端点在确定持续 拥塞后,SHOULD 将 min_rtt 设置为最新的 RTT 样本。 这避免在 RTT 增加时反复声明持续 拥塞。这也允许连接在发生破坏性网络事件后 重置其 min_rtt 和 smoothed_rtt 的估计; 见 第 5.3 节

端点 MAY 在连接中的其他 时间重新建立 min_rtt,例如 当流量较低且收到确认延迟较低的确认时。 实现 SHOULD NOT 过于频繁地刷新 min_rtt 值,因为路径的实际最小 RTT 并不 经常可观测到。

5.3. 估算 smoothed_rtt 和 rttvar

smoothed_rtt 是端点 RTT 样本的指数加权移动平均值,而 rttvar 使用平均 变动来估算 RTT 样本中的变动。

smoothed_rtt 的计算使用根据 确认延迟调整后的 RTT 样本。这些延迟按 [QUIC-TRANSPORT] 的 第 19.3 节所述,从 ACK 帧的 ACK Delay 字段中解码。

对端在握手期间可能会报告大于其 max_ack_delay 的确认延迟([QUIC-TRANSPORT] 的 第 13.2.1 节)。为 处理这种情况,端点在握手 确认之前,SHOULD 忽略 max_ack_delay,握手确认的定义见 [QUIC-TLS] 的 第 4.1.2 节。 当这些较大的确认延迟出现时,它们很可能不会重复,并且局限于 握手。因此,端点可以在不将它们限制到 max_ack_delay 的情况下使用它们,从而避免不必要地抬高 RTT 估计。

请注意,如果对端报告确认延迟出错,或者 端点的 min_rtt 估计出错,则较大的确认延迟可能导致 smoothed_rtt 被显著抬高。因此,在 握手确认之前,如果按确认延迟调整 RTT 样本会导致该样本小于 min_rtt,端点 MAY 忽略该 RTT 样本。

握手确认后,对端报告的任何 大于其 max_ack_delay 的确认延迟,都会归因于 无意但可能重复发生的延迟,例如对端的调度器时延 或先前确认的丢失。过量延迟也可能源于 不合规的接收方。因此,这些额外延迟实际上被视为 路径延迟的一部分,并纳入 RTT 估计。

因此,当使用对端报告的确认 延迟调整 RTT 样本时,端点:

  • 对 Initial 包 MAY 忽略确认延迟,因为 这些 确认不会被对端延迟([QUIC-TRANSPORT] 的 第 13.2.1 节);
  • 在握手确认之前,SHOULD 忽略对端的 max_ack_delay;
  • 握手确认后,MUST 使用确认延迟和对端 max_ack_delay 中的较小者;并且
  • 如果结果值小于 min_rtt,则 MUST NOT 从 RTT 样本中减去确认延迟。这限制了 由于对端误报而导致的 smoothed_rtt 低估。

此外,当相应的解密密钥不能立即使用时, 端点可能会推迟处理确认。例如,客户端可能会 收到对 0-RTT 包的确认,但由于 1-RTT 包保护密钥 尚不可用而无法解密。在 这种情况下,端点在握手确认之前,SHOULD 从其 RTT 样本中减去这类本地延迟。

[RFC6298] 类似, smoothed_rtt 和 rttvar 按如下方式计算。

端点在连接建立期间以及连接迁移时重置估计器时 初始化 RTT 估计器;见 [QUIC-TRANSPORT] 的 第 9.4 节。 在新路径上尚无任何 RTT 样本可用之前,或当 估计器被重置时,估计器使用初始 RTT 进行初始化;见 第 6.2.2 节

smoothed_rtt 和 rttvar 按如下方式初始化,其中 kInitialRtt 包含 初始 RTT 值:

smoothed_rtt = kInitialRtt
rttvar = kInitialRtt / 2

网络路径的 RTT 样本记录在 latest_rtt 中;见 第 5.1 节。在初始化后的第一个 RTT 样本上, 估计器使用该样本重置。这确保估计器不保留 过去样本的历史。如 [QUIC-TRANSPORT] 的 第 9.4 节所述,在其他路径上发送的包不会向 当前路径贡献 RTT 样本。

在初始化后的第一个 RTT 样本上,smoothed_rtt 和 rttvar 设置 如下:

smoothed_rtt = latest_rtt
rttvar = latest_rtt / 2

在后续 RTT 样本上,smoothed_rtt 和 rttvar 按如下方式演化:

ack_delay = decoded acknowledgment delay from ACK frame
if (handshake confirmed):
  ack_delay = min(ack_delay, max_ack_delay)
adjusted_rtt = latest_rtt
if (latest_rtt >= min_rtt + ack_delay):
  adjusted_rtt = latest_rtt - ack_delay
smoothed_rtt = 7/8 * smoothed_rtt + 1/8 * adjusted_rtt
rttvar_sample = abs(smoothed_rtt - adjusted_rtt)
rttvar = 3/4 * rttvar + 1/4 * rttvar_sample

6. 丢包检测

QUIC 发送方使用确认来检测丢失的包,并使用 PTO 来确保 收到确认;见 第 6.2 节。本节给出这些算法的 描述。

如果包丢失,QUIC 传输需要从该丢失中恢复, 例如通过重传数据、发送更新后的帧,或丢弃该 帧。更多信息见 [QUIC-TRANSPORT] 的 第 13.3 节

丢包检测按包编号空间分别进行,这不同于 RTT 测量和 拥塞控制,因为 RTT 和拥塞控制是 路径的属性,而丢包检测还依赖于密钥可用性。

6.1. 基于确认的 检测

基于确认的丢包检测体现了 TCP 的快速 重传 [RFC5681]、早期重传 [RFC5827]、转发确认 [FACK]、SACK 丢包恢复 [RFC6675] 以及 RACK-TLP [RFC8985] 等算法的精神。本 节概述这些算法如何在 QUIC 中实现。

如果一个包满足以下所有条件,则会被声明为丢失:

  • 该包未被确认,处于传输中,并且发送时间早于一个 已确认的包。
  • 该包比一个已确认包早发送 kPacketThreshold 个包 (第 6.1.1 节),或者它已经发送了足够长的 时间 (第 6.1.2 节)。

确认表明一个较晚发送的包已被交付,而 包阈值和时间阈值为包重排序提供了一定容忍度。

错误地将包声明为丢失会导致不必要的重传, 并且可能因拥塞控制器在检测到丢包时采取的动作而导致性能 下降。实现可以检测伪重传, 并增加包重排序阈值或时间重排序阈值,以 减少未来的伪重传和丢包事件。具有 自适应时间阈值的实现 MAY 选择从较小的初始重排序 阈值开始,以最小化恢复时延。

6.1.1. 包阈值

包重排序阈值 (kPacketThreshold) 的RECOMMENDED 初始值是 3,该值基于 TCP 丢包检测的最佳实践 [RFC5681] [RFC6675]。为了与 TCP 保持相似, 实现 SHOULD NOT 使用小于 3 的包阈值;见 [RFC5681]

某些网络可能表现出更高程度的包重排序, 导致发送方检测到伪丢包。此外, 与 TCP 相比,QUIC 中的包重排序可能更常见,因为能够观察并重排 TCP 包的网络元素无法对 QUIC 做到这一点,并且也因为 QUIC 包编号是 加密的。在错误检测到丢包后提高重排序阈值的算法, 例如 RACK [RFC8985],已被证明在 TCP 中有用,并且 预期在 QUIC 中至少同样有用。

6.1.2. 时间阈值

一旦同一包编号空间中较晚的包已被 确认,如果一个较早的包在过去已经发送了达到阈值的 一段时间,端点 SHOULD 将其声明为丢失。为避免过早地将包声明为丢失,该 时间阈值 MUST 至少设置为本地定时器 粒度,如 kGranularity 常量所指示。时间阈值为:

max(kTimeThreshold * max(smoothed_rtt, latest_rtt), kGranularity)

如果在最大已确认包之前发送的包尚不能被声明为 丢失,则 SHOULD 为剩余时间设置一个定时器。

使用 max(smoothed_rtt, latest_rtt) 可防止以下两种 情况:

  • 最新 RTT 样本低于 smoothed RTT,可能是因为 重排序使确认经过了较短路径;
  • 最新 RTT 样本高于 smoothed RTT,可能是由于实际 RTT 持续增加,但 smoothed RTT 尚未赶上。

RECOMMENDED 时间阈值 (kTimeThreshold) 以 RTT 乘数表示, 为 9/8。定时器粒度 (kGranularity) 的RECOMMENDED 值为 1 毫秒。

实现 MAY 试验绝对阈值、来自 先前连接的阈值、自适应阈值,或纳入 RTT 变动。 更小的阈值会降低重排序韧性并增加伪 重传,更大的阈值会增加丢包检测延迟。

6.2. 探测超时

当诱发 ACK 的包没有在预期时间段内被 确认,或者服务器可能尚未验证客户端地址时, 探测超时 (PTO) 会触发发送一个或两个探测数据报。PTO 使 连接能够从尾部包或确认的丢失中恢复。

与丢包检测一样,PTO 按包编号空间分别进行。也就是说, 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

当传输一个诱发 ACK 的包时,发送方按如下方式为 PTO 周期调度一个定时器:

PTO = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay

PTO 周期是发送方应当等待一个 已发送包确认的时间量。该时间周期包括估计的 网络 RTT (smoothed_rtt)、该估计的变动 (4*rttvar), 以及 max_ack_delay,用于计入接收方可能 延迟发送确认的最长时间。

当 PTO 为 Initial 或 Handshake 包编号空间启动时, PTO 周期计算中的 max_ack_delay 被设置为 0,因为预期对端不会有意 延迟这些包;见 [QUIC-TRANSPORT] 的 第 13.2.1 节

PTO 周期 MUST 至少为 kGranularity,以避免定时器立即 过期。

当多个包编号空间中都有诱发 ACK 的包处于 传输中时,定时器 MUST 设置为 Initial 和 Handshake 包 编号空间中的较早值。

在握手确认之前,端点 MUST NOT 为 Application Data 包编号 空间设置其 PTO 定时器。这样做可以防止端点在 对端尚无处理这些包的密钥,或端点尚无处理其 确认的密钥时,重传包中的信息。例如,当客户端向服务器发送 0-RTT 包时可能发生这种情况;它这样做时并不知道服务器是否能够 解密这些包。同样,当服务器在确认客户端已验证服务器证书并因此 能够读取这些 1-RTT 包之前发送 1-RTT 包时,也可能发生这种情况。

每当发送或确认一个诱发 ACK 的包,或者 Initial 或 Handshake 密钥被丢弃 ([QUIC-TLS] 的 第 4.9 节)时,发送方 SHOULD 重新启动其 PTO 定时器。这确保 PTO 始终基于 最新的 RTT 估计,并针对跨包 编号空间中的正确包设置。

当 PTO 定时器过期时,PTO 回退因子 MUST 增加,从而使 PTO 周期设置为其当前值的两倍。PTO 回退因子在 收到确认时重置,但以下情况除外。服务器在握手期间 响应包可能比其他时候花费更长时间。为了 保护这样的服务器免受客户端重复探测,未能确定服务器是否已完成验证 客户端地址的客户端,不会重置 PTO 回退因子。 也就是说,客户端在收到 Initial 包中的确认时, 不会重置 PTO 回退因子。

发送方速率的这种指数降低很重要, 因为连续 PTO 可能由严重 拥塞导致的包或确认丢失引起。即使在多个 包编号空间中都有诱发 ACK 的包处于传输中,PTO 的指数增长也会 跨所有空间发生,以防止给网络带来过量负载。 例如,Initial 包编号空间中的超时会使 Handshake 包 编号空间中的超时时长加倍。

连续 PTO 过期的总持续时间 受空闲超时限制。

如果已为时间阈值丢包检测设置了定时器,则 MUST NOT 设置 PTO 定时器;见 第 6.1.2 节。为时间 阈值丢包检测设置的定时器在大多数情况下会早于 PTO 定时器 过期,并且不太可能伪重传数据。

6.2.2. 握手和 新路径

在相同网络上恢复的连接 MAY 使用先前连接的 最终 smoothed 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 包都无法再被确认,因此它们会从传输中 字节数中移除。当 Initial 或 Handshake 密钥被丢弃时,PTO 和丢包 检测定时器 MUST 被重置,因为丢弃密钥表明 有前进进展,并且丢包检测定时器可能已经针对 一个现已丢弃的包编号空间设置。

6.2.2.1. 地址验证 之前

在服务器验证路径上的客户端地址之前, 它可以发送的数据量被限制为收到数据量的三倍, 如 [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. 加速 握手完成

当服务器收到包含重复 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. 发送探测 包

当 PTO 定时器过期时,发送方 MUST 在相应包编号空间中发送至少一个诱发 ACK 的包 作为探测。端点 MAY 发送最多两个 满尺寸的数据报,其中包含诱发 ACK 的包,以避免由于单个数据报丢失而导致代价高昂的 连续 PTO 过期,或从多个包编号空间传输数据。 PTO 上发送的所有探测包 MUST 都是 诱发 ACK 的。

除了在定时器 过期的包编号空间中发送数据外,发送方 SHOULD 从其他有 传输中数据的包编号空间发送诱发 ACK 的包, 如果可能则进行包合并。当服务器同时有 Initial 和 Handshake 数据处于 传输中,或当客户端同时有 Handshake 和 Application Data 处于传输中时, 这尤其有价值,因为对端可能只拥有这两个包编号 空间之一的接收密钥。

如果发送方希望在 PTO 上诱发更快的确认,它可以 跳过一个 包编号以消除确认延迟。

端点 SHOULD 在 PTO 过期时发送的包中包含新数据。 如果没有新数据可发送,MAY 发送先前发送过的数据。 实现 MAY 使用替代策略来确定探测包的内容, 包括基于应用的优先级发送新的或重传的数据。

发送方可能没有新的或先前发送过的数据可发送。 举例来说,考虑以下事件序列:新的应用数据 在 STREAM 帧中发送,被判定为丢失,然后在新包中重传, 随后原始传输被确认。当没有数据可 发送时,发送方 SHOULD 在单个 包中发送 PING 或其他诱发 ACK 的帧,并重新启动 PTO 定时器。

或者,发送方可以不发送诱发 ACK 的包,而 MAY 将 任何仍处于传输中的包标记为丢失。这样做避免了发送额外的 包,但会增加过于激进地声明丢失的风险,从而导致 拥塞控制器不必要地降低速率。

连续的 PTO 周期会以指数方式增加,因此, 当网络中包持续被丢弃时,连接 恢复时延也会以指数方式增加。在 PTO 过期时发送两个包可以提高对 包丢失的韧性,从而降低连续 PTO 事件的概率。

当 PTO 定时器多次过期且无法发送新数据时, 实现必须在每次发送相同载荷 或发送不同载荷之间作出选择。发送相同载荷可能更简单, 并确保最高优先级的帧先到达。每次发送不同 载荷会减少伪重传的可能性。

6.3. 处理 Retry 包

Retry 包会导致客户端发送另一个 Initial 包,实际上 重新启动连接过程。Retry 包表示 Initial 包已被接收但未被处理。Retry 包不能被视为 确认,因为它并不表示包已被处理,也没有 指定包编号。

收到 Retry 包的客户端会重置拥塞控制和丢包恢复 状态,包括重置任何待处理的定时器。其他连接状态, 特别是加密握手消息,会被保留;见 [QUIC-TRANSPORT] 的 第 17.2.5 节

客户端 MAY 将从发送第一个 Initial 包到收到 Retry 或 Version Negotiation 包之间的时间段计算为到服务器的 RTT 估计。客户端 MAY 使用此值替代其 默认的初始 RTT 估计。

6.4. 丢弃密钥和 包状态

当 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 节

7. 拥塞控制

本文档为 QUIC 指定了一个发送方侧拥塞控制器,类似于 TCP NewReno [RFC6582]

QUIC 为拥塞控制提供的信号是通用的,并设计为 支持不同的发送方侧算法。发送方可以单方面选择使用 另一种算法,例如 CUBIC [RFC8312]

如果发送方使用不同于本文档所指定的控制器, 所选择的控制器 MUST 符合 [RFC8085] 的 第 3.1 节中指定的拥塞控制指南。

与 TCP 类似,仅包含 ACK 帧的包不计入传输中 字节数,也不受拥塞控制。与 TCP 不同,QUIC 可以检测这些包的 丢失,并且 MAY 使用该信息来调整拥塞 控制器或正在发送的仅 ACK 包的速率,但本文档 未描述实现这种调整的机制。

拥塞控制器按路径分别维护,因此在其他路径上发送的包不会 改变当前路径的拥塞控制器,如 [QUIC-TRANSPORT] 的 第 9.4 节所述。

本文档中的算法以字节为单位指定并使用控制器的拥塞 窗口。

如果发送某个包会导致 bytes_in_flight(见 附录 B.2)大于拥塞窗口, 则端点 MUST NOT 发送该包,除非该包 是在 PTO 定时器过期时发送(见 第 6.2 节),或在进入 恢复时发送 (见 第 7.3.2 节)。

7.1. 显式拥塞 通知

如果某条路径已被验证支持显式拥塞通知 (ECN) [RFC3168] [RFC8311],QUIC 会将 IP 头部中的 Congestion Experienced (CE) 码点 视为拥塞信号。本文档指定了 当对端报告的 ECN-CE 计数增加时端点的响应;见 [QUIC-TRANSPORT] 的 第 13.4.2 节

7.2. 初始和最小 拥塞窗口

QUIC 的每个连接都从慢启动开始,拥塞窗口设置为一个 初始值。端点 SHOULD 使用十倍于 最大数据报大小 (max_datagram_size) 的初始拥塞窗口,同时将窗口限制为 14,720 字节或两倍最大数据报大小中的较大值。这遵循 [RFC6928] 中的分析和建议, 增加字节限制是为了 计入 UDP 相较于 TCP 的 20 字节开销而言更小的 8 字节开销。

如果最大数据报大小在连接期间发生变化,则初始 拥塞窗口 SHOULD 使用新的大小重新计算。如果为了完成握手而 减小最大 数据报大小,则拥塞窗口 SHOULD 设置为新的初始拥塞 窗口。

在验证客户端地址之前,服务器还可能受到 [QUIC-TRANSPORT] 的 第 8.1 节 所指定的反放大限制的进一步限制。 虽然反放大限制可能阻止拥塞窗口 被充分利用,从而减慢拥塞窗口的增长, 但它不会直接影响拥塞窗口。

最小拥塞窗口是拥塞窗口在响应丢包、 对端报告的 ECN-CE 计数增加, 或持续拥塞时可以达到的最小值。RECOMMENDED 值为 2 * max_datagram_size。

7.3. 拥塞控制 状态

本文档描述的 NewReno 拥塞控制器具有三个 不同的状态,如图 1所示。

                新路径或          +------------+
            持续拥塞              |   慢       |
        (O)---------------------->|   启动     |
                                  +------------+
                                        |
                                丢包或 |
                        ECN-CE 增加    |
                                        v
 +------------+     丢包或       +------------+
 | 拥塞       |  ECN-CE 增加     |  恢复      |
 | 避免       |------------------>|  时期      |
 +------------+                   +------------+
           ^                            |
           |                            |
           +----------------------------+
              对恢复期间发送的包的确认
图 1拥塞控制状态 和转换

这些状态及其之间的转换将在后续 小节中描述。

7.3.1. 慢启动

只要拥塞窗口低于 慢启动阈值,NewReno 发送方就处于慢启动状态。发送方从慢启动 开始,因为慢启动阈值被初始化为无限值。

当发送方处于慢启动状态时,拥塞窗口在每次处理确认时 按确认的字节数增加。这会导致 拥塞窗口呈指数增长。

当包丢失或对端报告的 ECN-CE 计数增加时, 发送方 MUST 退出慢启动并进入 恢复时期。

每当拥塞窗口小于 慢启动阈值时,发送方都会重新进入慢启动;这只会在声明 持续拥塞之后发生。

7.3.2. 恢复

NewReno 发送方在检测到包 丢失或对端报告的 ECN-CE 计数增加时进入恢复时期。已经 处于恢复时期的发送方会保持在其中,而不会重新进入。

进入恢复时期时,发送方 MUST 将慢启动阈值设置为 检测到丢包时拥塞窗口值的一半。在 退出恢复时期之前,拥塞 窗口 MUST 设置为慢启动阈值的降低后 数值。

实现 MAY 在进入 恢复时期时立即降低拥塞窗口,或使用其他机制,例如 Proportional Rate Reduction [PRR],来更逐步地降低拥塞窗口。如果 拥塞窗口被立即降低,则可以在降低之前发送一个包。 如果丢失包中的数据被重传,这会加速丢包恢复, 并类似于 [RFC6675] 的 第 5 节中描述的 TCP 行为。

恢复时期旨在将拥塞窗口降低限制为 每个往返时间一次。因此,在恢复时期内,拥塞窗口不会因 新的丢包或 ECN-CE 计数增加而改变。

当恢复时期内发送的包得到确认时, 恢复时期结束,发送方进入拥塞避免。 这与 TCP 对恢复的定义略有不同,后者在启动恢复的丢失段 得到确认时结束 [RFC5681]

7.3.3. 拥塞 避免

只要拥塞窗口处于或高于慢启动阈值,并且不在恢复时期中, NewReno 发送方就处于拥塞避免状态。

处于拥塞避免状态的发送方使用加性增加 乘性降低 (AIMD) 方法,该方法 MUST 将 拥塞窗口的增加限制为:对于每一个被确认的拥塞窗口, 最多增加一个最大数据报大小。

当包丢失或对端报告的 ECN-CE 计数增加时, 发送方退出拥塞避免并进入恢复时期。

7.4. 忽略无法解密包的 丢失

在握手期间,某些包保护密钥在包到达时可能尚不可用, 接收方可以选择丢弃该包。特别是, Handshake 和 0-RTT 包在 Initial 包 到达之前无法处理,1-RTT 包在握手完成之前无法处理。 端点 MAY 忽略可能 在对端拥有处理这些包所需的包保护密钥之前到达的 Handshake、0-RTT 和 1-RTT 包的丢失。 端点 MUST NOT 忽略在给定包编号空间中 最早已确认包之后发送的包的丢失。

7.5. 探测超时

探测包 MUST NOT 被拥塞 控制器阻塞。不过,发送方 MUST 将这些包计为额外处于传输中,因为这些包 会增加网络负载,却不建立包丢失。请注意,发送探测 包可能会导致发送方的传输中字节数超过拥塞窗口, 直到收到能够确定包丢失或交付的 确认。

7.6. 持续拥塞

当发送方确定在足够长的 持续时间内发送的所有包都丢失时, 网络被认为正在经历持续拥塞。

7.6.1. 持续时间

持续拥塞持续时间按如下方式计算:

(smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay) *
    kPersistentCongestionThreshold

不同于第 6.2 节中的 PTO 计算,该持续时间无论在哪些包编号空间中确定了丢失, 都包含 max_ack_delay。

该持续时间允许发送方在确定 持续拥塞之前发送尽可能多的包,包括一些响应 PTO 过期而发送的包, 与 TCP 使用 Tail Loss Probes [RFC8985] 和 RTO [RFC5681] 时相当。

kPersistentCongestionThreshold 的较大值会使发送方对网络中的 持续拥塞响应较不敏感,这可能导致 向拥塞网络中激进发送。过小的值可能导致发送方 不必要地声明持续拥塞,从而降低发送方的吞吐量。

kPersistentCongestionThreshold 的 RECOMMENDED 值为 3,这会产生 大致等价于 TCP 发送方在两个 TLP 之后声明 RTO 的 行为。

该设计不使用连续 PTO 事件来确定 持续 拥塞,因为应用模式会影响 PTO 过期。例如, 发送方如果发送少量数据,并在其间有静默期, 每次发送都会重新启动 PTO 定时器,即使没有收到任何确认, 也可能阻止 PTO 定时器在很长一段时间内过期。 使用持续时间使发送方能够在不依赖 PTO 过期的情况下确定 持续拥塞。

7.6.2. 确定 持续拥塞

发送方在收到确认后,如果两个诱发 ACK 的包被声明为丢失,并且 满足以下条件,则确定发生了持续拥塞:

  • 在所有包编号空间中,在这两个包的发送 时间之间发送的包没有任何一个被确认;
  • 这两个包的发送时间之间的持续时间超过 持续拥塞持续时间(第 7.6.1 节);并且
  • 发送这两个包时已经存在先前的 RTT 样本。

这两个包 MUST 是诱发 ACK 的, 因为接收方只被要求在其最大确认延迟内确认诱发 ACK 的包; 见 [QUIC-TRANSPORT] 的 第 13.2 节

持续拥塞时期 SHOULD NOT 在至少有一个 RTT 样本之前开始。在第一个 RTT 样本之前,发送方会基于 初始 RTT(第 6.2.2 节)启动其 PTO 定时器,该值可能 显著大于实际 RTT。要求先前存在 RTT 样本可以防止发送方用 可能过少的探测确定持续拥塞。

由于网络拥塞不受包编号空间影响, 持续 拥塞 SHOULD 考虑跨包编号空间发送的包。发送方 如果没有所有包编号空间的状态,或者实现 无法跨包编号空间比较发送时间,MAY 只使用 已被确认的那个包编号空间的状态。这可能会导致错误地 声明持续拥塞,但不会导致无法检测到 持续拥塞。

当声明持续拥塞时,发送方的拥塞 窗口 MUST 降低到最小拥塞窗口 (kMinimumWindow), 类似于 TCP 发送方对 RTO 的响应 [RFC5681]

7.6.3. 示例

以下示例说明发送方可能如何确定 持续 拥塞。假设:

smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay = 2
kPersistentCongestionThreshold = 3

考虑以下事件序列:

表 1
时间 动作
t=0 发送包 #1(应用数据)
t=1 发送包 #2(应用数据)
t=1.2 接收对 #1 的确认
t=2 发送包 #3(应用数据)
t=3 发送包 #4(应用数据)
t=4 发送包 #5(应用数据)
t=5 发送包 #6(应用数据)
t=6 发送包 #7(应用数据)
t=8 发送包 #8(PTO 1)
t=12 发送包 #9(PTO 2)
t=12.2 接收对 #9 的确认

当在 t = 12.2 收到对 包 9 的确认时,包 2 到 8 被声明为丢失。

拥塞时期计算为最早 和最新丢失包之间的时间:8 - 1 = 7。持续拥塞持续时间为 2 * 3 = 6。 由于已达到阈值,并且在最早和最新丢失包之间 没有任何包被确认,因此网络被认为 经历了持续拥塞。

虽然此示例展示了 PTO 过期,但确定 持续 拥塞并不要求发生 PTO 过期。

7.7. 节奏控制

发送方 SHOULD 基于来自 拥塞控制器的输入,对所有传输中包的发送进行节奏控制。

在网络中连续发送多个包而不在它们之间加入任何延迟,会产生 包突发,这可能导致短期拥塞和丢包。发送方 MUST 使用节奏控制或限制此类突发。发送方 SHOULD 将突发限制为 初始拥塞窗口;见 第 7.2 节。如果发送方 知道通向接收方的网络路径可以吸收更大的突发,MAY 使用更高的 限制。

实现应注意将其拥塞控制器设计为 能够很好地与节奏控制器配合。例如,节奏控制器可以包装拥塞 控制器并控制拥塞窗口的可用性,或者节奏控制器 可以对拥塞控制器交给它的包进行节奏化发送。

ACK 帧的及时交付对于高效丢包恢复很重要。为了 避免延迟它们向对端的交付,仅包含 ACK 帧的包 因此 SHOULD 不进行节奏控制。

端点可以按其选择实现节奏控制。完美节奏控制的发送方 会在时间上完全均匀地分散 包。对于基于窗口的拥塞控制器,例如 本文档中的控制器,该速率可以通过在 RTT 上平均 拥塞窗口来计算。以字节每时间为单位的 速率表示,其中 congestion_window 以字节为单位:

rate = N * congestion_window / smoothed_rtt

或者以时间为单位的包间隔表示:

interval = ( smoothed_rtt * packet_size / congestion_window ) / N

使用一个较小但至少为 1 的 N 值(例如 1.25)可以确保 RTT 的变化不会导致 拥塞窗口利用不足。

实际考虑因素,例如分包、调度延迟和 计算效率,可能导致发送方在远短于 RTT 的时间段内 偏离该速率。

节奏控制的一种可能实现策略使用漏桶 算法,其中“桶”的容量被限制为最大突发大小,而 “桶”填充的速率由上述函数确定。

7.8. 未充分利用 拥塞窗口

当传输中字节数小于拥塞窗口,并且发送不受 节奏控制限制时,拥塞窗口处于未充分利用状态。这可能由于 应用数据不足或流量控制限制而发生。当这种情况发生时, 无论是在慢启动还是拥塞避免中, 拥塞窗口 SHOULD NOT 增加。

对包进行节奏控制的发送方(见 第 7.7 节)可能会因该延迟而推迟发送包, 并且未能充分利用拥塞窗口。如果没有节奏控制延迟时发送方本可以 充分利用拥塞窗口,则发送方 SHOULD NOT 认为自己受应用限制。

发送方 MAY 实现替代机制,以便在 未充分利用时期之后更新其拥塞窗口,例如 [RFC7661] 中为 TCP 提出的机制。

8. 安全考虑

8.1. 丢包和拥塞 信号

丢包检测和拥塞控制从根本上涉及使用来自未经认证 实体的信号,例如延迟、丢包和 ECN 标记。 攻击者可以通过操纵这些信号来导致端点降低其发送速率: 通过丢弃包、策略性地改变路径延迟, 或改变 ECN 码点。

8.2. 流量分析

仅承载 ACK 帧的包可以通过观察 包大小以启发式方式识别。 确认模式可能暴露关于链路 特征或应用行为的信息。为了减少泄露的信息, 端点可以将确认与其他帧捆绑,或者可以使用 PADDING 帧,但这可能以性能为代价。

8.3. 误报 ECN 标记

接收方可以误报 ECN 标记来改变 发送方的拥塞响应。抑制 ECN-CE 标记报告可能导致发送方 提高其发送速率。这种提高可能导致拥塞和丢包。

发送方可以通过将其发送的偶发包标记为 ECN-CE 标记来 检测报告抑制。如果使用 ECN-CE 标记发送的包在被确认时 没有被报告为 CE 标记,则 发送方可以通过不再为该路径上随后发送的包设置 ECN-Capable Transport (ECT) 码点来为该路径禁用 ECN [RFC3168]

报告额外的 ECN-CE 标记会导致发送方降低其 发送 速率,这在效果上类似于宣告降低的连接流量控制 限制,因此这样做并不能获得好处。

端点会选择它们使用的拥塞控制器。拥塞 控制器会通过降低速率来响应 ECN-CE 报告,但响应可能有所不同。 标记可以被视为等同于丢包 [RFC3168],但也可以指定其他 响应,例如 [RFC8511][RFC8311]

9. 参考文献

9.1. 规范性参考文献

[QUIC-TLS]
Thomson, M., Ed.S. Turner, Ed.“使用 TLS 保护 QUIC”RFC 9001DOI 10.17487/RFC9001<https://www.rfc-editor.org/info/rfc9001>
[QUIC-TRANSPORT]
Iyengar, J., Ed.M. Thomson, Ed.“QUIC:基于 UDP 的多路复用且安全的传输”RFC 9000DOI 10.17487/RFC9000<https://www.rfc-editor.org/info/rfc9000>
[RFC2119]
Bradner, S.“用于 RFC 中表示 要求级别的关键词”BCP 14RFC 2119DOI 10.17487/RFC2119<https://www.rfc-editor.org/info/rfc2119>
[RFC3168]
Ramakrishnan, K.Floyd, S.D. Black“向 IP 添加显式拥塞 通知 (ECN)”RFC 3168DOI 10.17487/RFC3168<https://www.rfc-editor.org/info/rfc3168>
[RFC8085]
Eggert, L.Fairhurst, G.G. Shepherd“UDP 使用指南”BCP 145RFC 8085DOI 10.17487/RFC8085<https://www.rfc-editor.org/info/rfc8085>
[RFC8174]
Leiba, B.“RFC 2119 关键词中大写与 小写的歧义”BCP 14RFC 8174DOI 10.17487/RFC8174<https://www.rfc-editor.org/info/rfc8174>

9.2. 资料性参考文献

[FACK]
Mathis, M.J. Mahdavi“转发确认:改进 TCP 拥塞控制”ACM SIGCOMM Computer Communication ReviewDOI 10.1145/248157.248181<https://doi.org/10.1145/248157.248181>
[PRR]
Mathis, M.Dukkipati, N.Y. Cheng“用于 TCP 的比例速率降低”RFC 6937DOI 10.17487/RFC6937<https://www.rfc-editor.org/info/rfc6937>
[RETRANSMISSION]
Karn, P.C. Partridge“改进可靠传输协议中的往返时间估计”ACM Transactions on Computer SystemsDOI 10.1145/118544.118549<https://doi.org/10.1145/118544.118549>
[RFC2018]
Mathis, M.Mahdavi, J.Floyd, S.A. Romanow“TCP 选择性确认选项”RFC 2018DOI 10.17487/RFC2018<https://www.rfc-editor.org/info/rfc2018>
[RFC3465]
Allman, M.“使用 适当字节计数 (ABC) 的 TCP 拥塞控制”RFC 3465DOI 10.17487/RFC3465<https://www.rfc-editor.org/info/rfc3465>
[RFC5681]
Allman, M.Paxson, V.E. Blanton“TCP 拥塞控制”RFC 5681DOI 10.17487/RFC5681<https://www.rfc-editor.org/info/rfc5681>
[RFC5682]
Sarolahti, P.Kojo, M.Yamamoto, K.M. Hata“Forward RTO-Recovery (F-RTO):一种用于检测 TCP 伪 重传超时的算法”RFC 5682DOI 10.17487/RFC5682<https://www.rfc-editor.org/info/rfc5682>
[RFC5827]
Allman, M.Avrachenkov, K.Ayesta, U.Blanton, J.P. Hurtig“TCP 和流 控制传输协议 (SCTP) 的早期重传”RFC 5827DOI 10.17487/RFC5827<https://www.rfc-editor.org/info/rfc5827>
[RFC6298]
Paxson, V.Allman, M.Chu, J.M. Sargent“计算 TCP 的重传定时器”RFC 6298DOI 10.17487/RFC6298<https://www.rfc-editor.org/info/rfc6298>
[RFC6582]
Henderson, T.Floyd, S.Gurtov, A.Y. Nishida“对 TCP 快速恢复算法的 NewReno 修改”RFC 6582DOI 10.17487/RFC6582<https://www.rfc-editor.org/info/rfc6582>
[RFC6675]
Blanton, E.Allman, M.Wang, L.Jarvinen, I.Kojo, M.Y. Nishida“一种基于 TCP 选择性确认 (SACK) 的保守丢包恢复算法”RFC 6675DOI 10.17487/RFC6675<https://www.rfc-editor.org/info/rfc6675>
[RFC6928]
Chu, J.Dukkipati, N.Cheng, Y.M. Mathis“增加 TCP 的初始窗口”RFC 6928DOI 10.17487/RFC6928<https://www.rfc-editor.org/info/rfc6928>
[RFC7661]
Fairhurst, G.Sathiaseelan, A.R. Secchi“更新 TCP 以支持 速率受限流量”RFC 7661DOI 10.17487/RFC7661<https://www.rfc-editor.org/info/rfc7661>
[RFC8311]
Black, D.“放宽对显式 拥塞通知 (ECN) 实验的限制”RFC 8311DOI 10.17487/RFC8311<https://www.rfc-editor.org/info/rfc8311>
[RFC8312]
Rhee, I.Xu, L.Ha, S.Zimmermann, A.Eggert, L.R. Scheffenegger“用于高速长距离网络的 CUBIC”RFC 8312DOI 10.17487/RFC8312<https://www.rfc-editor.org/info/rfc8312>
[RFC8511]
Khademi, N.Welzl, M.Armitage, G.G. Fairhurst“带 ECN 的 TCP 替代退避 (ABE)”RFC 8511DOI 10.17487/RFC8511<https://www.rfc-editor.org/info/rfc8511>
[RFC8985]
Cheng, Y.Cardwell, N.Dukkipati, N.P. Jha“用于 TCP 的 RACK-TLP 丢包检测算法”RFC 8985DOI 10.17487/RFC8985<https://www.rfc-editor.org/info/rfc8985>

附录 A. 丢包恢复伪代码

现在,我们描述第 6 节中所述丢包检测机制的一个 示例实现。

本节中的伪代码片段按代码组件授权;见 版权声明。

A.1. 跟踪已发送包

为正确实现拥塞控制,QUIC 发送方会跟踪每个 诱发 ACK 的包,直到该包被确认或丢失。 预期实现能够通过包编号和加密上下文访问此信息,并存储每包字段 (附录 A.1.1),用于丢包恢复和 拥塞控制。

在包被声明为丢失后,端点仍可在一段时间内维护其状态, 以允许包重排序;见 [QUIC-TRANSPORT] 的 第 13.3 节。这 使发送方能够检测伪重传。

已发送包按每个包编号空间进行跟踪,ACK 处理仅适用于单个空间。

A.1.1. 已发送包字段

packet_number:

已发送包的包编号。

ack_eliciting:

一个布尔值,指示包是否 诱发 ACK。 如果为 true,预期会收到确认, 尽管对端可能会将包含该确认的 ACK 帧延迟发送, 最多延迟 max_ack_delay。

in_flight:

一个布尔值,指示该包是否计入 传输中字节数。

sent_bytes:

包中发送的字节数,不包括 UDP 或 IP 开销,但包括 QUIC 成帧开销。

time_sent:

包发送的时间。

A.2. 相关常量

丢包恢复中使用的常量基于 RFC、论文 和 通用实践的组合。

kPacketThreshold:

在包阈值丢包检测将一个包 视为丢失之前,允许的最大包重排序。 第 6.1.1 节中推荐的值为 3。

kTimeThreshold:

在时间阈值丢包检测将一个包 视为丢失之前,允许的最大时间重排序。以 RTT 乘数指定。 第 6.1.2 节中推荐的值为 9/8。

kGranularity:

定时器粒度。这是一个依赖系统的值,第 6.1.2 节 推荐值为 1 ms。

kInitialRtt:

在取得 RTT 样本之前使用的 RTT。 第 6.2.2 节中推荐的值为 333 ms。

kPacketNumberSpace:

一个用于枚举三个包编号空间的枚举:

enum kPacketNumberSpace {
  Initial,
  Handshake,
  ApplicationData,
}

A.3. 相关变量

实现拥塞控制机制所需的变量 在本节中描述。

latest_rtt:

在收到对 先前未确认包的确认时所作的最近一次 RTT 测量。

smoothed_rtt:

连接的平滑 RTT,按 第 5.3 节所述计算。

rttvar:

RTT 变动,按第 5.3 节所述计算。

min_rtt:

一段时间内看到的最小 RTT,忽略 确认延迟,如 第 5.2 节所述。

first_rtt_sample:

获得第一个 RTT 样本的时间。

max_ack_delay:

接收方打算延迟确认 Application Data 包编号 空间中的包的最长时间,由同名传输参数定义([QUIC-TRANSPORT] 的 第 18.2 节)。请注意,收到的 ACK 帧中的实际 ack_delay 可能因定时器较晚、重排序或丢失而更大。

loss_detection_timer:

用于丢包检测的多模式定时器。

pto_count:

在未收到确认的情况下已发送 PTO 的次数。

time_of_last_ack_eliciting_packet[kPacketNumberSpace]:

最近一个诱发 ACK 的包发送的时间。

largest_acked_packet[kPacketNumberSpace]:

到目前为止在该包编号空间中被确认的最大包编号。

loss_time[kPacketNumberSpace]:

该包编号空间中的下一个包可因超过时间上的重排序窗口而 被视为丢失的时间。

sent_packets[kPacketNumberSpace]:

包编号空间中的包编号到其相关信息的 关联。上文附录 A.1中已有详细描述。

A.4. 初始化

在连接开始时,按如下方式初始化丢包检测变量:

loss_detection_timer.reset()
pto_count = 0
latest_rtt = 0
smoothed_rtt = kInitialRtt
rttvar = kInitialRtt / 2
min_rtt = 0
first_rtt_sample = 0
for pn_space in [ Initial, Handshake, ApplicationData ]:
  largest_acked_packet[pn_space] = infinite
  time_of_last_ack_eliciting_packet[pn_space] = 0
  loss_time[pn_space] = 0

A.5. 发送包时

包发送后,会存储关于该包的信息。 OnPacketSent 的参数已在上文附录 A.1.1中详细描述。

OnPacketSent 的伪代码如下:

OnPacketSent(packet_number, pn_space, ack_eliciting,
             in_flight, sent_bytes):
  sent_packets[pn_space][packet_number].packet_number =
                                           packet_number
  sent_packets[pn_space][packet_number].time_sent = now()
  sent_packets[pn_space][packet_number].ack_eliciting =
                                           ack_eliciting
  sent_packets[pn_space][packet_number].in_flight = in_flight
  sent_packets[pn_space][packet_number].sent_bytes = sent_bytes
  if (in_flight):
    if (ack_eliciting):
      time_of_last_ack_eliciting_packet[pn_space] = now()
    OnPacketSentCC(sent_bytes)
    SetLossDetectionTimer()

A.6. 接收 数据报时

当服务器受到反放大限制阻塞时,接收 一个数据报会解除其阻塞,即使该 数据报中的包都没有被成功处理。在这种情况下,PTO 定时器需要重新启动。

OnDatagramReceived 的伪代码如下:

OnDatagramReceived(datagram):
  // If this datagram unblocks the server, arm the
  // PTO timer to avoid deadlock.
  if (server was at anti-amplification limit):
    SetLossDetectionTimer()
    if loss_detection_timer.timeout < now():
      // Execute PTO if it would have expired
      // while the amplification limit applied.
      OnLossDetectionTimeout()

A.7. 接收 确认时

收到 ACK 帧时,它可能新近确认任意数量的 包。

OnAckReceived 和 UpdateRtt 的伪代码如下:

IncludesAckEliciting(packets):
  for packet in packets:
    if (packet.ack_eliciting):
      return true
  return false

OnAckReceived(ack, pn_space):
  if (largest_acked_packet[pn_space] == infinite):
    largest_acked_packet[pn_space] = ack.largest_acked
  else:
    largest_acked_packet[pn_space] =
        max(largest_acked_packet[pn_space], ack.largest_acked)

  // DetectAndRemoveAckedPackets finds packets that are newly
  // acknowledged and removes them from sent_packets.
  newly_acked_packets =
      DetectAndRemoveAckedPackets(ack, pn_space)
  // Nothing to do if there are no newly acked packets.
  if (newly_acked_packets.empty()):
    return

  // Update the RTT if the largest acknowledged is newly acked
  // and at least one ack-eliciting was newly acked.
  if (newly_acked_packets.largest().packet_number ==
          ack.largest_acked &&
      IncludesAckEliciting(newly_acked_packets)):
    latest_rtt =
      now() - newly_acked_packets.largest().time_sent
    UpdateRtt(ack.ack_delay)

  // Process ECN information if present.
  if (ACK frame contains ECN information):
      ProcessECN(ack, pn_space)

  lost_packets = DetectAndRemoveLostPackets(pn_space)
  if (!lost_packets.empty()):
    OnPacketsLost(lost_packets)
  OnPacketsAcked(newly_acked_packets)

  // Reset pto_count unless the client is unsure if
  // the server has validated the client's address.
  if (PeerCompletedAddressValidation()):
    pto_count = 0
  SetLossDetectionTimer()


UpdateRtt(ack_delay):
  if (first_rtt_sample == 0):
    min_rtt = latest_rtt
    smoothed_rtt = latest_rtt
    rttvar = latest_rtt / 2
    first_rtt_sample = now()
    return

  // min_rtt ignores acknowledgment delay.
  min_rtt = min(min_rtt, latest_rtt)
  // Limit ack_delay by max_ack_delay after handshake
  // confirmation.
  if (handshake confirmed):
    ack_delay = min(ack_delay, max_ack_delay)

  // Adjust for acknowledgment delay if plausible.
  adjusted_rtt = latest_rtt
  if (latest_rtt >= min_rtt + ack_delay):
    adjusted_rtt = latest_rtt - ack_delay

  rttvar = 3/4 * rttvar + 1/4 * abs(smoothed_rtt - adjusted_rtt)
  smoothed_rtt = 7/8 * smoothed_rtt + 1/8 * adjusted_rtt

A.8. 设置丢包 检测定时器

QUIC 丢包检测使用一个单一定时器进行所有超时丢包检测。 定时器的持续时间基于定时器的模式,该模式在下方的包 和定时器事件中设置。下面定义的函数 SetLossDetectionTimer 展示如何设置这个单一定时器。

该算法可能导致定时器被设置到过去的时间,尤其是在 定时器唤醒较晚时。设置在过去的定时器会立即触发。

SetLossDetectionTimer 的伪代码如下(其中 “^” 运算符 表示 幂运算):

GetLossTimeAndSpace():
  time = loss_time[Initial]
  space = Initial
  for pn_space in [ Handshake, ApplicationData ]:
    if (time == 0 || loss_time[pn_space] < time):
      time = loss_time[pn_space];
      space = pn_space
  return time, space

GetPtoTimeAndSpace():
  duration = (smoothed_rtt + max(4 * rttvar, kGranularity))
      * (2 ^ pto_count)
  // Anti-deadlock PTO starts from the current time
  if (no ack-eliciting packets in flight):
    assert(!PeerCompletedAddressValidation())
    if (has handshake keys):
      return (now() + duration), Handshake
    else:
      return (now() + duration), Initial
  pto_timeout = infinite
  pto_space = Initial
  for space in [ Initial, Handshake, ApplicationData ]:
    if (no ack-eliciting packets in flight in space):
        continue;
    if (space == ApplicationData):
      // Skip Application Data until handshake confirmed.
      if (handshake is not confirmed):
        return pto_timeout, pto_space
      // Include max_ack_delay and backoff for Application Data.
      duration += max_ack_delay * (2 ^ pto_count)

    t = time_of_last_ack_eliciting_packet[space] + duration
    if (t < pto_timeout):
      pto_timeout = t
      pto_space = space
  return pto_timeout, pto_space

PeerCompletedAddressValidation():
  // Assume clients validate the server's address implicitly.
  if (endpoint is server):
    return true
  // Servers complete address validation when a
  // protected packet is received.
  return has received Handshake ACK ||
       handshake confirmed

SetLossDetectionTimer():
  earliest_loss_time, _ = GetLossTimeAndSpace()
  if (earliest_loss_time != 0):
    // Time threshold loss detection.
    loss_detection_timer.update(earliest_loss_time)
    return

  if (server is at anti-amplification limit):
    // The server's timer is not set if nothing can be sent.
    loss_detection_timer.cancel()
    return

  if (no ack-eliciting packets in flight &&
      PeerCompletedAddressValidation()):
    // There is nothing to detect lost, so no timer is set.
    // However, the client needs to arm the timer if the
    // server might be blocked by the anti-amplification limit.
    loss_detection_timer.cancel()
    return

  timeout, _ = GetPtoTimeAndSpace()
  loss_detection_timer.update(timeout)

A.9. 超时时

当丢包检测定时器过期时,定时器的模式决定 要执行的动作。

OnLossDetectionTimeout 的伪代码如下:

OnLossDetectionTimeout():
  earliest_loss_time, pn_space = GetLossTimeAndSpace()
  if (earliest_loss_time != 0):
    // Time threshold loss Detection
    lost_packets = DetectAndRemoveLostPackets(pn_space)
    assert(!lost_packets.empty())
    OnPacketsLost(lost_packets)
    SetLossDetectionTimer()
    return

  if (no ack-eliciting packets in flight):
    assert(!PeerCompletedAddressValidation())
    // Client sends an anti-deadlock packet: Initial is padded
    // to earn more anti-amplification credit,
    // a Handshake packet proves address ownership.
    if (has Handshake keys):
      SendOneAckElicitingHandshakePacket()
    else:
      SendOneAckElicitingPaddedInitialPacket()
  else:
    // PTO. Send new data if available, else retransmit old data.
    // If neither is available, send a single PING frame.
    _, pn_space = GetPtoTimeAndSpace()
    SendOneOrTwoAckElicitingPackets(pn_space)

  pto_count++
  SetLossDetectionTimer()

A.10. 检测丢失包

每次收到 ACK 或时间 阈值丢包检测定时器过期时,都会调用 DetectAndRemoveLostPackets。 该函数对相应包编号空间的 sent_packets 进行操作, 并返回新近检测为丢失的包列表。

DetectAndRemoveLostPackets 的伪代码如下:

DetectAndRemoveLostPackets(pn_space):
  assert(largest_acked_packet[pn_space] != infinite)
  loss_time[pn_space] = 0
  lost_packets = []
  loss_delay = kTimeThreshold * max(latest_rtt, smoothed_rtt)

  // Minimum time of kGranularity before packets are deemed lost.
  loss_delay = max(loss_delay, kGranularity)

  // Packets sent before this time are deemed lost.
  lost_send_time = now() - loss_delay

  foreach unacked in sent_packets[pn_space]:
    if (unacked.packet_number > largest_acked_packet[pn_space]):
      continue

    // Mark packet as lost, or set time when it should be marked.
    // Note: The use of kPacketThreshold here assumes that there
    // were no sender-induced gaps in the packet number space.
    if (unacked.time_sent <= lost_send_time ||
        largest_acked_packet[pn_space] >=
          unacked.packet_number + kPacketThreshold):
      sent_packets[pn_space].remove(unacked.packet_number)
      lost_packets.insert(unacked)
    else:
      if (loss_time[pn_space] == 0):
        loss_time[pn_space] = unacked.time_sent + loss_delay
      else:
        loss_time[pn_space] = min(loss_time[pn_space],
                                  unacked.time_sent + loss_delay)
  return lost_packets

A.11. 丢弃 Initial 或 Handshake 密钥时

当 Initial 或 Handshake 密钥被丢弃时,该空间中的包 会被丢弃,并且丢包检测状态会被更新。

OnPacketNumberSpaceDiscarded 的伪代码如下:

OnPacketNumberSpaceDiscarded(pn_space):
  assert(pn_space != ApplicationData)
  RemoveFromBytesInFlight(sent_packets[pn_space])
  sent_packets[pn_space].clear()
  // Reset the loss detection and PTO timer
  time_of_last_ack_eliciting_packet[pn_space] = 0
  loss_time[pn_space] = 0
  pto_count = 0
  SetLossDetectionTimer()

附录 B. 拥塞控制 伪代码

现在,我们描述第 7 节中所述拥塞控制器的一个 示例实现。

本节中的伪代码片段按代码组件授权;见 版权声明。

B.1. 相关常量

拥塞控制中使用的常量基于 RFC、 论文 和通用实践的组合。

kInitialWindow:

对初始传输中字节数的默认限制,如第 7.2 节所述。

kMinimumWindow:

以字节为单位的最小拥塞窗口,如第 7.2 节所述。

kLossReductionFactor:

检测到新的丢包事件时,用于降低拥塞窗口的 缩放因子。第 7 节推荐值 为 0.5。

kPersistentCongestionThreshold:

确定持续拥塞所需的一段时间, 指定为 PTO 乘数。第 7.6 节推荐 值为 3。

B.2. 相关变量

实现拥塞控制机制所需的变量 在本节中描述。

max_datagram_size:

发送方当前最大载荷大小。这不包括 UDP 或 IP 开销。最大数据报大小用于拥塞窗口 计算。端点基于其路径最大 传输单元(PMTU;见 [QUIC-TRANSPORT] 的 第 14.2 节)设置此变量的值,且 最小值为 1200 字节。

ecn_ce_counters[kPacketNumberSpace]:

对端在 ACK 帧中报告的、该包 编号空间内 ECN-CE 计数器的最高值。该值用于检测所报告 ECN-CE 计数器的增加。

bytes_in_flight:

所有已发送包的大小之和(以字节计),这些包至少包含一个 诱发 ACK 的帧或 PADDING 帧,且尚未被确认或声明为 丢失。该大小不包括 IP 或 UDP 开销,但包括 QUIC 头部和带关联数据的认证加密 (AEAD) 开销。 仅包含 ACK 帧的包不计入 bytes_in_flight, 以确保拥塞控制不会阻碍拥塞反馈。

congestion_window:

允许处于传输中的最大字节数。

congestion_recovery_start_time:

由于检测到丢包 或 ECN 而开始当前恢复时期的时间。当在此时间之后发送的包被确认时, QUIC 会退出拥塞恢复。

ssthresh:

以字节为单位的慢启动阈值。当拥塞窗口 低于 ssthresh 时, 模式为慢启动,窗口按确认的字节数 增长。

拥塞控制伪代码还会访问丢包 恢复伪代码中的一些变量。

B.3. 初始化

在连接开始时,按如下方式初始化拥塞控制 变量:

congestion_window = kInitialWindow
bytes_in_flight = 0
congestion_recovery_start_time = 0
ssthresh = infinite
for pn_space in [ Initial, Handshake, ApplicationData ]:
  ecn_ce_counters[pn_space] = 0

B.4. 包已发送时

每当发送一个包且该包包含非 ACK 帧时,该包 会增加 bytes_in_flight。

OnPacketSentCC(sent_bytes):
  bytes_in_flight += sent_bytes

B.5. 包 确认时

该函数从丢包检测的 OnAckReceived 调用,并提供来自 sent_packets 的新近 acked_packets。

在拥塞避免中,使用整数表示 congestion_window 的实现者应谨慎处理除法,并可使用 [RFC3465] 的 第 2.1 节中建议的替代方法。

InCongestionRecovery(sent_time):
  return sent_time <= congestion_recovery_start_time

OnPacketsAcked(acked_packets):
  for acked_packet in acked_packets:
    OnPacketAcked(acked_packet)

OnPacketAcked(acked_packet):
  if (!acked_packet.in_flight):
    return;
  // Remove from bytes_in_flight.
  bytes_in_flight -= acked_packet.sent_bytes
  // Do not increase congestion_window if application
  // limited or flow control limited.
  if (IsAppOrFlowControlLimited())
    return
  // Do not increase congestion window in recovery period.
  if (InCongestionRecovery(acked_packet.time_sent)):
    return
  if (congestion_window < ssthresh):
    // Slow start.
    congestion_window += acked_packet.sent_bytes
  else:
    // Congestion avoidance.
    congestion_window +=
      max_datagram_size * acked_packet.sent_bytes
      / congestion_window

B.6. 出现新的拥塞 事件时

当检测到新的拥塞事件时,该函数从 ProcessECN 和 OnPacketsLost 调用。如果尚未处于恢复状态,这会启动恢复时期,并 立即降低慢启动阈值和拥塞窗口。

OnCongestionEvent(sent_time):
  // No reaction if already in a recovery period.
  if (InCongestionRecovery(sent_time)):
    return

  // Enter recovery period.
  congestion_recovery_start_time = now()
  ssthresh = congestion_window * kLossReductionFactor
  congestion_window = max(ssthresh, kMinimumWindow)
  // A packet can be sent to speed up loss recovery.
  MaybeSendOnePacket()

B.7. 处理 ECN 信息

当收到来自对端、带有 ECN 小节的 ACK 帧时调用此函数。

ProcessECN(ack, pn_space):
  // If the ECN-CE counter reported by the peer has increased,
  // this could be a new congestion event.
  if (ack.ce_counter > ecn_ce_counters[pn_space]):
    ecn_ce_counters[pn_space] = ack.ce_counter
    sent_time = sent_packets[ack.largest_acked].time_sent
    OnCongestionEvent(sent_time)

B.8. 包丢失时

当 DetectAndRemoveLostPackets 判定包丢失时调用此函数。

OnPacketsLost(lost_packets):
  sent_time_of_last_loss = 0
  // Remove lost packets from bytes_in_flight.
  for lost_packet in lost_packets:
    if lost_packet.in_flight:
      bytes_in_flight -= lost_packet.sent_bytes
      sent_time_of_last_loss =
        max(sent_time_of_last_loss, lost_packet.time_sent)
  // Congestion event if in-flight packets were lost
  if (sent_time_of_last_loss != 0):
    OnCongestionEvent(sent_time_of_last_loss)

  // Reset the congestion window if the loss of these
  // packets indicates persistent congestion.
  // Only consider packets sent after getting an RTT sample.
  if (first_rtt_sample == 0):
    return
  pc_lost = []
  for lost in lost_packets:
    if lost.time_sent > first_rtt_sample:
      pc_lost.insert(lost)
  if (InPersistentCongestion(pc_lost)):
    congestion_window = kMinimumWindow
    congestion_recovery_start_time = 0

B.9. 从传输中字节数中移除已丢弃 包

当 Initial 或 Handshake 密钥被丢弃时,该空间中发送的包 不再计入传输中字节数。

RemoveFromBytesInFlight 的伪代码如下:

RemoveFromBytesInFlight(discarded_packets):
  // Remove any unacknowledged packets from flight.
  foreach packet in discarded_packets:
    if packet.in_flight
      bytes_in_flight -= size

贡献者

IETF QUIC 工作组获得了许多人的大量支持。 以下人员对本文档作出了实质性贡献:

作者地址

Jana Iyengar(编辑
Fastly
Ian Swett(编辑
Google