7. 拥塞控制 (Congestion Control)
本文档为 QUIC 指定了一个发送方侧拥塞控制器, 类似 TCP NewReno [RFC6582].
QUIC 为拥塞控制提供的信号是通用的, 设计上支持不同的发送方侧算法. 发送方可以单方面选择使用不同算法, 例如 CUBIC [RFC8312].
如果发送方使用不同于本文档指定的控制器, 所选控制器必须符合 [RFC8085] 第 3.1 节指定的拥塞控制指南.
与 TCP 类似, 只包含 ACK frame 的 packet 不计入 bytes in flight, 也不受拥塞控制. 与 TCP 不同, QUIC 能检测这些 packet 的丢失, 并且可以使用该信息调整拥塞控制器或调整仅 ACK packet 的发送速率, 但本文档不描述这样做的机制.
拥塞控制器按路径维护, 因此在其他路径上发送的 packet 不会改变当前路径的拥塞控制器, 如 [QUIC-TRANSPORT] 第 9.4 节所述.
本文档中的算法以 bytes 为单位指定和使用拥塞控制器的 congestion window.
除非 packet 是在 PTO timer 过期时发送, 见第 6.2 节, 或在进入 recovery 时发送, 见第 7.3.2 节, endpoint 禁止在发送某个 packet 后导致 bytes_in_flight 超过 congestion window, 见附录 B.2.
7.1. 显式拥塞通知
如果某条路径已经验证支持 Explicit Congestion Notification (ECN) [RFC3168] [RFC8311], QUIC 会把 IP header 中的 Congestion Experienced (CE) codepoint 视为拥塞信号. 本文档指定当对端报告的 ECN-CE 计数增加时 endpoint 如何响应; 见 [QUIC-TRANSPORT] 第 13.4.2 节.
7.2. 初始和最小拥塞窗口
QUIC 每个连接都从 slow start 开始, congestion window 设置为初始值. Endpoint 应当使用 10 倍最大 datagram size (max_datagram_size) 作为初始 congestion window, 同时把窗口限制为 14,720 bytes 或 2 倍最大 datagram size 中较大的值. 这遵循 [RFC6928] 的分析和建议, 并提高 byte 限制以计入 UDP 相比 TCP 更小的 8-byte 开销, TCP 开销为 20 bytes.
如果最大 datagram size 在连接期间改变, 应当使用新大小重新计算 initial congestion window. 如果为了完成 handshake 而降低最大 datagram size, congestion window 应当设置为新的 initial congestion window.
在验证客户端地址之前, 服务器还会受到 [QUIC-TRANSPORT] 第 8.1 节指定的 anti-amplification limit 限制. 虽然该限制可能阻止 congestion window 被充分利用, 从而减慢 congestion window 增长, 但它不会直接影响 congestion window.
Minimum congestion window 是 congestion window 在响应丢包、对端报告的 ECN-CE 计数增加或 persistent congestion 时可以达到的最小值. 推荐值为 2 * max_datagram_size.
7.3. 拥塞控制状态
本文档描述的 NewReno 拥塞控制器有 3 个不同状态, 如图 1 所示.
New path or +------------+
persistent congestion | Slow |
(O)----------------------> | Start |
+------------+
|
Loss or
ECN-CE increase
|
v
+------------+ Loss or +------------+
| Congestion | ECN-CE increase | Recovery |
| Avoidance |------------------>| Period |
+------------+ +------------+
^ |
| |
+----------------------------+
Acknowledgment of packet
sent during recovery
图 1: 拥塞控制状态和转换
这些状态及状态之间的转换在后续小节中描述.
7.3.1. Slow Start
只要 congestion window 小于 slow start threshold, NewReno 发送方就处于 slow start. 发送方开始时处于 slow start, 因为 slow start threshold 初始化为无限大.
当发送方处于 slow start 时, 每处理一个 acknowledgment, congestion window 按被确认的 bytes 数增加. 这会导致 congestion window 指数增长.
当 packet 丢失或对端报告的 ECN-CE 计数增加时, 发送方必须退出 slow start 并进入 recovery period.
每当 congestion window 小于 slow start threshold 时, 发送方重新进入 slow start. 这只会在声明 persistent congestion 后发生.
7.3.2. Recovery
NewReno 发送方在检测到 packet 丢失或对端报告的 ECN-CE 计数增加时进入 recovery period. 已处于 recovery period 的发送方保持该状态, 不会重新进入.
进入 recovery period 时, 发送方必须把 slow start threshold 设置为检测到丢包时 congestion window 值的一半. 在退出 recovery period 之前, congestion window 必须设置为降低后的 slow start threshold 值.
实现可以在进入 recovery period 时立即降低 congestion window, 也可以使用其他机制更渐进地降低 congestion window, 例如 Proportional Rate Reduction [PRR]. 如果立即降低 congestion window, 可以在降低前发送单个 packet. 如果丢失 packet 中的数据被重传, 这会加速丢包恢复, 且与 [RFC6675] 第 5 节描述的 TCP 行为类似.
Recovery period 的目标是把 congestion window 降低限制为每个 round trip 最多一次. 因此, 在 recovery period 中, congestion window 不会因为新的丢包或 ECN-CE 计数增加而改变.
当 recovery period 中发送的 packet 被确认时, recovery period 结束, 发送方进入 congestion avoidance. 这与 TCP 对 recovery 的定义略有不同, TCP 的 recovery 在触发 recovery 的丢失 segment 被确认时结束 [RFC5681].
7.3.3. Congestion Avoidance
只要 congestion window 大于或等于 slow start threshold, 且不处于 recovery period, NewReno 发送方就处于 congestion avoidance.
处于 congestion avoidance 的发送方使用 Additive Increase Multiplicative Decrease (AIMD) 方法. 该方法必须把 congestion window 的增长限制为每确认一个 congestion window 的数据, 最多增加一个 maximum datagram size.
当 packet 丢失或对端报告的 ECN-CE 计数增加时, 发送方退出 congestion avoidance 并进入 recovery period.
7.4. 忽略不可解密 Packet 的丢失
Handshake 期间, 某些 packet 到达时对应 packet protection key 可能尚不可用, 接收方可以选择丢弃该 packet. 特别是, Handshake 和 0-RTT packet 只有在 Initial packet 到达后才能处理, 1-RTT packet 只有在 handshake 完成后才能处理. Endpoint 可以忽略可能在对端拥有处理这些 packet 所需 packet protection key 之前到达的 Handshake、0-RTT 和 1-RTT packet 的丢失. Endpoint 禁止忽略在给定 packet number space 中最早已确认 packet 之后发送的 packet 丢失.
7.5. Probe Timeout
Probe packet 禁止被拥塞控制器阻塞. 不过, 发送方必须把这些 packet 计为额外 in flight, 因为这些 packet 增加网络负载, 但并不建立 packet 丢失事实. 注意, 发送 probe packet 可能导致发送方的 bytes in flight 超过 congestion window, 直到收到 acknowledgment 并由此确定 packet 丢失或交付.
7.6. 持续拥塞
当发送方确定足够长时间内发送的所有 packet 都丢失时, 网络被认为正在经历 persistent congestion.
7.6.1. 持续时间
Persistent congestion duration 计算如下:
(smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay) * kPersistentCongestionThreshold
与第 6.2 节中的 PTO 计算不同, 该持续时间无论丢包在哪些 packet number space 中确定, 都包含 max_ack_delay.
该持续时间允许发送方在确定 persistent congestion 前发送与 TCP 使用 Tail Loss Probe [RFC8985] 和 RTO [RFC5681] 时一样多的 packet, 包括部分因 PTO 过期而发送的 packet.
较大的 kPersistentCongestionThreshold 会使发送方对网络中的 persistent congestion 反应较慢, 可能导致向拥塞网络激进发送. 过小的值可能导致发送方不必要地声明 persistent congestion, 从而降低发送方吞吐量.
kPersistentCongestionThreshold 的推荐值为 3, 这会产生大致等价于 TCP 发送方在两个 TLP 后声明 RTO 的行为.
该设计不使用连续 PTO event 来确定 persistent congestion, 因为应用模式会影响 PTO 过期. 例如, 发送方如果发送少量数据且两次发送之间存在静默期, 每次发送都会重启 PTO timer, 即使一直没有收到 acknowledgment, PTO timer 也可能长时间不过期. 使用持续时间可以让发送方不依赖 PTO 过期来确定 persistent congestion.
7.6.2. 确定持续拥塞
发送方在收到 acknowledgment 后, 如果两个 ack-eliciting packet 被声明为丢失, 并满足以下条件, 就确定 persistent congestion:
- 在所有 packet number space 中, 这两个 packet 发送时间之间发送的 packet 没有任何一个被确认.
- 这两个 packet 发送时间之间的持续时间超过 persistent congestion duration, 见第 7.6.1 节.
- 发送这两个 packet 时已经存在先前 RTT sample.
这两个 packet 必须是 ack-eliciting, 因为接收方只被要求在其 maximum acknowledgment delay 内确认 ack-eliciting packet; 见 [QUIC-TRANSPORT] 第 13.2 节.
Persistent congestion period 不应在至少有一个 RTT sample 之前开始. 在第一个 RTT sample 之前, 发送方基于 initial RTT armed PTO timer, 见第 6.2.2 节, 而 initial RTT 可能显著大于实际 RTT. 要求存在先前 RTT sample, 可防止发送方用过少 probe 确定 persistent congestion.
由于网络拥塞不受 packet number space 影响, persistent congestion 应当考虑跨 packet number space 发送的 packet. 如果发送方没有所有 packet number space 的状态, 或实现无法跨 packet number space 比较发送时间, 可以只使用被确认的 packet number space 的状态. 这可能导致错误声明 persistent congestion, 但不会导致无法检测 persistent congestion.
声明 persistent congestion 时, 发送方的 congestion window 必须降低到 minimum congestion window (kMinimumWindow), 类似 TCP 发送方对 RTO 的响应 [RFC5681].
7.6.3. 示例
以下示例说明发送方如何确定 persistent congestion. 假设:
smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay = 2
kPersistentCongestionThreshold = 3
考虑以下事件序列:
| Time | Action |
|---|---|
| t=0 | Send packet #1 (application data) |
| t=1 | Send packet #2 (application data) |
| t=1.2 | Receive acknowledgment of #1 |
| t=2 | Send packet #3 (application data) |
| t=3 | Send packet #4 (application data) |
| t=4 | Send packet #5 (application data) |
| t=5 | Send packet #6 (application data) |
| t=6 | Send packet #7 (application data) |
| t=8 | Send packet #8 (PTO 1) |
| t=12 | Send packet #9 (PTO 2) |
| t=12.2 | Receive acknowledgment of #9 |
在 t = 12.2 收到 packet 9 的 acknowledgment 时, packet 2 到 packet 8 被声明为丢失.
拥塞周期计算为最早和最晚丢失 packet 之间的时间: 8 - 1 = 7. Persistent congestion duration 为 2 * 3 = 6. 因为达到阈值, 且最早和最晚丢失 packet 之间没有任何 packet 被确认, 网络被认为经历了 persistent congestion.
虽然此示例展示了 PTO 过期, 但确定 persistent congestion 并不要求发生 PTO 过期.
7.7. 发送节奏 (Pacing)
发送方应当根据拥塞控制器输入对所有 in-flight packet 进行 pacing.
无间隔地向网络发送多个 packet 会形成 packet burst, 可能造成短期拥塞和丢包. 发送方必须使用 pacing 或限制这类 burst. 发送方应当把 burst 限制为 initial congestion window; 见第 7.2 节. 如果发送方知道到接收方的网络路径可以吸收更大 burst, 可以使用更高限制.
实现应谨慎设计其拥塞控制器, 使其能与 pacer 良好配合. 例如, pacer 可以包装拥塞控制器并控制 congestion window 的可用性, 或者 pacer 可以对拥塞控制器交给它的 packet 进行定速发送.
ACK frame 的及时交付对高效丢包恢复很重要. 为避免延迟向对端交付 ACK frame, 只包含 ACK frame 的 packet 因此不应被 pacing.
Endpoint 可以自行选择 pacing 实现. 完美 paced 发送方会把 packet 在时间上均匀分布. 对本文档中的 window-based 拥塞控制器, 该速率可以通过在 RTT 上平均 congestion window 计算. 以 bytes per time 表示速率, 其中 congestion_window 单位为 bytes:
rate = N * congestion_window / smoothed_rtt
或以时间单位表示 packet 间隔:
interval = (smoothed_rtt * packet_size / congestion_window) / N
使用较小但至少为 1 的 N 值, 例如 1.25, 可确保 RTT 变化不会导致 congestion window 利用不足.
实际因素, 如 packetization、调度延迟和计算效率, 可能导致发送方在远短于一个 RTT 的时间段内偏离该速率.
pacing 的一种可能实现策略是 leaky bucket algorithm. 其中 "bucket" 的容量限制为最大 burst size, "bucket" 填充速率由上述函数决定.
7.8. 未充分利用拥塞窗口
当 bytes in flight 小于 congestion window 且发送不受 pacing 限制时, congestion window 未被充分利用. 这可能因应用数据不足或流量控制限制发生. 出现这种情况时, 无论处于 slow start 还是 congestion avoidance, congestion window 都不应增加.
使用 pacing 的发送方, 见第 7.7 节, 可能因为 pacing 延迟而推迟发送 packet, 从而未充分利用 congestion window. 如果没有 pacing 延迟就能充分利用 congestion window, 发送方不应认为自己受应用限制.
发送方可以实现其他机制, 用于在一段时间未充分利用后更新 congestion window, 例如 [RFC7661] 为 TCP 提出的机制.