QUIC协议详解 - 流量控制

蒸汽
蒸汽
发布于 2025-03-26 / 8 阅读
0
0

QUIC协议详解 - 流量控制

在数据通信中,发送方的发送速度和接收方的接收速度不一定是相等的, 如果发送方的速度太快,会导致接收方处理不过来,需要把处理不来的数据放在缓冲区里(buffer)。如果接收方的缓冲区已经满了,发送方继续发送数据将会导致数据丢失。在具有不同网络速度的机器进行通信的环境中,具有流量控制机制是必不可少的。例如,如果一台个人电脑向正在缓慢处理接收数据的智能手机发送数据,智能手机必须调节数据流(data flow),以免不堪重负。

为了防止发送方(fast sender)的速度压倒接收方(slow receiver),或者防止恶意发送方(malicious sender)消耗接收方大量内存,TCP 提供了流量控制(flow control)机制来限制发送方的速度。而 QUIC 协议为了保证传输的可靠性了,也提供了类似的流量控制机制。

如何进行流量控制?

接收方每次收到数据包,可以在发送 ACK 报文的时候,同时告诉发送方自己的缓存区还剩余多少是空闲的,我们也把缓存区的剩余大小称之为接收窗口(Receive Window,缩写:rwnd)。

发送方收到接收窗口 rwnd 之后,便会调整自己的发送速率,也就是调整自己发送窗口的大小,当发送方收到接收窗口 rwnd 的大小为 0 时,发送方就会停止发送数据,防止出现大量丢包情况的发生。

TCP 的流量控制策略

TCP 保证了数据的有序和可达性,所以原则上是数据按照序号依次发送和接收,下一个包的发送需要等到上一个包 ACK 到达。这样的话,在相邻两个包的发送间隙存在很长时间的空闲等待,好在 TCP 采用了滑动窗口机制来减少了排队等待时间,双方约定一定大小的窗口,在这个窗口内的包都可以同步发送,接收方收到一个 packet 时会回复 ACK 给发送方,发送方收到 ACK 后移动发送窗口,发送后续数据。

但是如果某个 packet 丢失或者其对应的 ACK 包丢失,同样会出现一方不必要的等待。如下图情况,packet 5的 ACK 包丢失,导致发送方无法移动发送窗口,但接收方已经在等待后面的包了。必须等到接收方超时重传这个 ACK 包,接收方超收到这个 ACK 包后,发送窗口才会移动,继续后面的发送行为。

QUIC的流量控制策略

QUIC 是基于 UDP 传输的,而 UDP 没有流量控制,因此 QUIC 实现了自己的流量控制机制,分为 Stream 和 Connection 两种级别:

  • Stream 级别的流量控制:通过限制 stream 可以发送的数据量,防止单个 stream 消耗连接(connection)的全部缓冲区。与 TCP 不同,就算此前有 packet 没有接收到,它的滑动只取决于接收到的最大偏移字节数(highest received byte offset)。只要还有可用窗口,发送方可以继续发送数据。

  • 在握手时,接收方通过[传输参数](transport parameters)设置 stream 的初始限制。

  • 发送方根据这个值进行流量控制,大致过程跟 TCP 相似。

  • 如果发送方达到限制,(可选)则可以发送[STREAM_DATA_BLOCKED](https://zhida.zhihu.com/search?content_id=163058291&content_type=Article&match_order=1&q=STREAM_DATA_BLOCKED&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3NDMxNTQ1NDMsInEiOiJTVFJFQU1fREFUQV9CTE9DS0VEIiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MTYzMDU4MjkxLCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjEsInpkX3Rva2VuIjpudWxsfQ.8dc9NgBDzyI5DtByAkkfq-Ww4X-SQi0RbaoCQbYlXuM&zhida_source=entity)帧给接收方,以告示它有数据要发送,但被流量控制限制阻止。

  • 接收方如果有更大的窗口值,可以发送[MAX_STREAM_DATA]帧通知发送方增加。

  • 如果发送方违反流量控制的限制,接收方可以关闭连接并返回FLOW_CONTROL_ERROR错误。

可用窗口 = 最大窗口数 - 接收到的最大偏移数

  • Connection 级别的流量控制:限制 connection 中所有 streams 相加起来的总字节数,防止发送方超过 connection 的缓冲(buffer)容量。

  • 在握手时,接收方通过传输参数(transport parameters)设置 connection 的初始限制。

  • 发送方根据计算 connection 中所有 streams 的可用窗口,与这个连接窗口值对比进行流量控制。

  • 如果发送方达到限制,(可选)则可以发送STREAM_BLOCKED帧给接收方,以告示它有数据要发送,但被流量控制限制阻止。

  • 接收方如果有更大的窗口值,可以发送`[MAX_DATA]帧通知发送方增加。

  • 如果发送方违反流量控制的限制,接收方可以关闭连接并返回FLOW_CONTROL_ERROR错误。

下图所示的例子,所有 streams 的最大窗口数为 120,其中:

  • stream 1 的最大接收偏移为 100,可用窗口 = 120 - 100 = 20

  • stream 2 的最大接收偏移为 90,可用窗口 = 120 - 90 = 30

  • stream 3 的最大接收偏移为 110,可用窗口 = 120 - 110 = 10

那么整个 Connection 的可用窗口 = 20 + 30 + 10 = 60

可用窗口 = stream 1 可用窗口 + stream 2 可用窗口 + stream 3 可用窗口

下面将基于 IETF QUIC 协议草案详细了解下 QUIC 是如何实现连接迁移的:

数据流量控制(Data Flow Control)

QUIC 采用了一种基于限制(limit-based)的流量控制方案,在该方案中,接收方通告在 stream 或整个连接(connection)上准备接收的总字节数的限制。这导致 QUIC 中有两个级别的数据流控制:

  • Stream 流量控制,通过限制在任何 stream 上可以发送的数据量,防止单个 stream 消耗连接(connection)的全部接收缓冲(buffer)。

  • 连接(Connection)流量控制,通过限制在所有STREAM帧的数据总字节数,防止发送方超过接收方的连接缓冲(buffer)容量。

发送方发送的数据不能(MUST NOT)超过这两个限制。

在握手过程中,接收方通过传输参数(transport parameters)为所有 streams 设置初始限制。随后,接收方向发送方发送MAX_STREAM_DATAMAX_DATA帧,以通告更大的限制。

接收方可以通过发送带对应 stream ID 的MAX_STREAM_DATA帧来通告对 stream 的更大限额。MAX_STREAM_DATA帧表示 stream 的最大绝对字节偏移量。接收方可以根据 stream 上接收数据的当前偏移量来确定要通告的流量控制偏移量。

接收方可以通过发送MAX_DATA帧来通告连接(connection)的更大限额,该MAX_DATA帧指示所有 streans 的绝对字节偏移量之和的最大值。接收方维护所有 streams 累积接收到字节总和,用于检查发送方是否违反了公布的连接或 stream 数据限制。接收方可以根据所有 streams 上接收的字节总数来确定要通告的最大数据限额。

一旦接收方公布了 connection 或 stream 的限制,它可能(MAY)会公布一个较小的限制,但这没有任何效果。

如果发送方违反了公布的连接或 stream 数据限制,接收方必须(MUST)使用FLOW_CONTROL_ERROR关闭连接。

发送方必须(MUST)忽略任何没有增加流量控制限制的MAX_STREAM_DATAMAX_DATA帧。

如果发送方发送的数据达到限制,它将无法发送新数据,并被视为被阻塞(blocked)。发送方应(SHOULD)发送STREAM_DATA_BLOCKEDDATA_BLOCKED帧,以向接收方标识它有数据要写入,但被流量控制限制阻止。如果发送方被阻塞的时间超过空闲超时时间(idle timeout),接收方可能会关闭连接,即使发送方有可用于传输的数据。为了防止连接关闭,流量控制受限的发送方应该(SHOULD)在没有 ack-eliciting 数据包的情况下,定期发送STREAM_DATA_BLOCKEDDATA_BLOCKED帧。

增加流量控制限值(Increasing Flow Control Limits)

实现(Implementations)决定何时以及在MAX_STREAM_DATA帧和MAX_DATA帧中通告流量控制限额,但本节提供了一些注意事项。

为了避免阻塞发送方,接收方可以在一个往返中(round trip)多次发送MAX_STREAM_DATAMAX_DATA帧,或者尽早地发送,为帧丢失和后续恢复留出时间。

控制帧(Control frames)会增加连接开销。因此,频繁地发送改动很小的MAX_STREAM_DATAMAX_DATA帧是不可取的。另一方面,如果更新不太频繁,则需要更大的限额增量,以避免阻塞发送方,从而要求接收方有更多的资源承诺(resource commitments)。在确定公布的限额有多大时,需要在资源承诺(resource commitments)和开销之间有一个权衡。

接收方可以基于往返时间(round-trip time)估时和接收应用(receiving application)消费数据的速率,使用自动调整机制来调整通告的额外限额的频率和数量,类似于常见的 TCP 实现。作为一种优化,端点可以在有其他帧要发送时,同时发送与流量控制相关的帧,以确保流量控制不会导致发送额外的包。

被阻塞的发送方不是必需发送STREAM_DATA_BLOCKEDDATA_BLOCKED帧。因此,在发送MAX_STREAM_DATAMAX_DATA 帧之前,接收方不能(MUST NOT)等待STREAM_DATA_BLOCKEDDATA_BLOCKED 帧;这样做可能会导致发送方在连接的其余时间被阻塞。即使发送方发送了这些帧,等待它们也会导致发送方至少在一整个往返(an entire round trip)中被阻塞。

当发送方在被阻塞后收到限额时,它可能会发送大量数据作为响应,从而导致短期拥塞。

流量控制性能(Flow Control Performance)

如果一个端点不能确保它的对端(peer)始终具有大于连接中对端的带宽时延积([bandwidth-delay product])的可用流量控制限额,则其接收吞吐量将受到流量控制的限制。

数据包丢失可能会导致接收缓冲区出现间隙,从而阻止应用程序消费数据、以及释放接收缓冲区空间。

及时发送流量控制限值的更新可以提高性能。仅为提供流量控制更新而发送数据包可能会增加网络负载并对性能产生不利影响。将流量控制更新与其他帧(如 ACK 帧)一起发送,可以降低这些更新的成本。

处理流取消(Handling Stream Cancellation)

端点需要最终就每个 stream 上消耗的流量控制限额达成一致,以便能够为连接级别的流量控制计算所有字节数。

在接收到`[RESET_STREAM]帧时,端点将断开匹配到的 stream 的状态,并忽略该 stream 后续的数据。

RESET_STREAM突然终止 stream 的一个方向。对于双向流,RESET_STREAM对相反方向的数据流(data flow)没有影响。两个端点都必须(MUST)保持 stream 在未终止方向上的流量控制状态,直到该方向进入终止状态。

流最终大小(Stream Final Size)

final size 是 stream 所消费的流量控制限额值。假设 stream 中的每个连续字节都被发送一次,那么 final size 就是发送的字节数。更常见的是,这比 stream 发送的最大偏移量高。如果没有发送发送字节,则为零。

不管 stream 如何终止,发送方总是将 stream 的 final size 可靠地传递给接收方。final size 是带有 FIN 标志的STREAM帧的偏移(Offset)和长度(Length)字段的总和,注意这些字段可能是隐式的。或者,RESET_STREAM帧的 Final Size 字段携带该值。这保证了两个端点都同意发送方在该流上消耗了多少流量控制限额。

当 stream 的接收部分进入“已知大小(Size Known)”或“重置接收(Reset Recvd)”状态时,端点将知道 stream 的 final size。接收方必须(MUST)使用 stream 的 final size 来说明其连接级别流量控制器中 stream 发送的所有字节。

端点(MUST NOT)不能在 stream 上发送大于或等于 final size 的数据。

一旦知道 stream 的 final size,它就不能更改。如果接收到标识 stream 的 final size 更改的RESET_STREAMSTREAM帧,则端点应(SHOULD)响应[FINAL_SIZE_ERROR]错误。即使在 stream 关闭之后,接收方也应(SHOULD)将接收到的数据大小等于或超过最终大小的数据视为FINAL_SIZE_ERROR`错误。生成这些错误并不是强制性的,因为要求端点生成这些错误也意味着端点需要维护 closed streams 的 final size 状态,这意味着重要的状态承诺。

控制并发(Controlling Concurrency)

端点限制对端(peer)可以打开的 incoming streams 的累积数量。只有 stream ID 小于(max_stream * 4 + initial_stream_id_for_type)的 streams 可以打开;见下表。初始限值(Initial limits)在传输参数中设定。后续限制使用MAX_STREAMS帧进行公告。单向流和双向流使用单独的限制。

Stream ID 类型

如果接收到的max_streams传输参数或MAX_STREAMS帧的值大于2^60,则允许最大 stream ID 不使用变长整数(variable-length integer)表示。在下面两种情况,连接必须(MUST)立即关闭并返回连接错误:如果是在传输参数中接收到有问题的值则返回TRANSPORT_PARAMETER_ERROR错误,或如果是在帧中接收到问题的值则返回 FRAME_ENCODING_ERROR错误。

端点不能(MUST NOT)超过其对端(peer)设置的限制。如果端点接收到 stream ID 超过其发送限制的帧,则必须(MUST)将其视为STREAM_LIMIT_ERROR类型的连接错误。

一旦接收放使用MAX_STREAMS帧发布的 stream 限制,那么后续发布的较小 stream 限制将无效。接收方必须(MUST)忽略任何没有增加 stream 限制的MAX_STREAMS帧。

与 stream 和 connection 流量控制一样,本文档让 QUIC 实现(implementations)来决定何时以及应该通过MAX_STREAMS向对端通告多少 streams 的数量。实现(implementations)可能会选择在 stream 关闭时增加限制,以保持对端可用的 streams 数量大致一致。

由于对端(peer)的限制而无法打开新 stream 的端点应该(SHOULD)发送STREAMS_BLOCKED 帧。此信号被认为对调试有用。端点在公布额外的限额之前不能(MUST NOT)等待接收到这个信号,因为这样做将意味着对对端将被阻塞至少一整个往返(an entire round trip),并且如果对端选择不发送STREAMS_BLOCKED 帧,则可能无限期地阻塞。


评论