欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

透彻解析QUIC协议中的流量控制机制

最编程 2024-07-31 16:33:28
...

我正在参加「掘金·启航计划」

为什么需要流控

先思考为什么需要流控。

假如没有数据发送的流控,发送端尽自己最大能力向接收端发送数据会出现什么现象:

image.png

如图中的例子,发送端发送数据的速度是70kbps,而应用端消费数据的速度只有50kbps。

由于数据不能及时的被上层数据处理,接收端的接收缓冲区可能很快就被打满,开始丢弃数据包。

这会对程序的运行产生负面影响,因此对发送数据的流量控制是个强需求。

QUIC对发送数据的控制

QUIC对发送数据的控制是按照层次划分的,分别是流的层面和连接的层面。

在QUIC协议中,一条连接(Connection)可以包含多条流(Stream),流量控制可以针对连接上的单条流,也可以针对整条连接。

无论是流级别的流控还是连接级别的流控,都是通过接收端向发送端宣告一个允许发送的字节数最大偏移量来实现的。

image.png

流级别的数据流控

流控限制是怎么由接收端告知给发送端的呢?有两种情况,分为初始时和传输时。

初始限制: 通过握手时传输参数设置

QUIC在建联握手的过程中,双方会传递传输参数,传输参数中的以下几个值与流控有关:

id name
0x05 initia_max_stream_data_bidi_local
0x06 initial_max_stream_data_bidi_remote
0x07 initial_max_stream_data_uni

观察下这几个参数,为什么双向流分为local和remote创建,但单向流却不区分呢?

要注意,这几个参数是提供给对端的发送部分使用的,有发送部分的单向流只能是对端自己创建的,所以无需区分local和remote。

下面以本端是发送端的视角,来说明这几个参数如何使用。

流创建者 流方向 本端发送部分使用的参数
对端 双向流 initial_max_stream_data_bidi_local
本端 双向流 initial_max_stream_data_bidi_remote
本端 单向流 initial_max_stream_data_uni

发送端就是用这几个参数来初始化发送窗口的大小。

后续限制: 通过发送MAX_STREAM_DATA帧。

上面提到的参数只能控制初始的发送窗口,后续的流控限制会使用MAX_STREAM_DATA帧来发送。

看下MAX_STREAM_DATA中包含的内容:

MAX_STREAM_DATA Frame {
     Type (i) = 0x11,
     Stream ID (i),
     Maximum Stream Data (i),
}

Type0x11标识了该帧是MAX_STREAM_DATA帧,Stream ID标识该帧所控制的流。Maximum Stream Data是控制该流可以发送的最大字节数。

正常情况下MAX_STREAM_DATA中的值应该是单调递增的,但不排除由于网络乱序情况下,发送端会收携带更小值的MAX_STREAM_DATA帧,此时只需要做忽略处理。

发送端被阻塞

当发送端预期发送的数据量大于流控限制时,数据发送将被阻塞,此时发送端会发送STREAM_DATA_BLOCKED帧通知给接收端。 要注意,在实现时,当发送端被阻塞后尽量保障能周期性的发送STREAM_DATA_BLOCKED帧,否则可能因为流闲置时间过长,被接收端关闭。

看下STREAM_DATA_BLOCKED帧包含的内容:

STREAM_DATA_BLOCKED Frame {
     Type (i) = 0x15,
     Stream ID (i),
     Maximum Stream Data (i),
}

Type0x15标识了该帧是STREAM_DATA_BLOCKED帧,Stream ID标识该帧所控制的流。Maximum Stream Data表示阻塞放生时已发送的字节数。

连接级别的数据流控

连接级别的流控,限制了允许在此连接(connection)上发送的总数据,各路流上发送的数据之和不允许超过此限制。

类似的,连接级别的流控也分为两种情况,分为初始时和传输时。

初始限制

初始的限制在传输参数中传递,字段是initial_max_data。

后续限制

后续限制会通过发送MAX_DATA帧。

看下MAX_DATA帧中包含的内容:

MAX_DATA Frame {
    Type (i) = 0x10,
    Maximum Data (i),
}

其中Type0x10标识了该帧的类型是MAX_DATA,此外只有Maximum Stream Data,该值表示允许在此连接上发送的数据量之和。

发送端被阻塞

当发送端在此连接上预期发送的数据量超过连接级别的流控限制时,在此连接上的数据发送应该被阻塞,并发送DATA_BLOCKED帧给接收端。 类似的,在该情况下DATA_BLOCKED帧也该被周期性的发送,避免连接闲置时间过长被接收端主动关闭。

DATA_BLOCKED的内容如下:

DATA_BLOCKED Frame {
     Type (i) = 0x14,
     Maximum Data (i),
}

其中Type0x14标识了该帧的类型是DATA_BLOCKEDMaximum Stream Data,该值表示在该连接被阻塞时已发送的字节数。

数据发送未遵守流控限制该如何处理?

理论上遵照标准、实现良好的QUIC终端并不会发送超过流控限制的数据。 但不能排除某些实现不去遵守流控限制,此时为了保障程序的正常运行,网络不被某一条连接影响,要实现以下保护性的策略。

  • 收到超出流控限制的数据后,接收端要将连接关闭。
  • 抛出一个FLOW_CONTROL_ERROR错误。

流控实现时的细节

在实现流控时,以下细节应该注意:

尽可能避免发送端被阻塞

这要求接受端及时的发送流控帧,甚至可能在一个rtt内发送多次。

尽可能减少流控相关控制帧本身的开销

由于流控帧也会产生额外的网络开销,可以通过跟随其他类型包一起发送,例如跟在每个ACK后,能有效降低流控帧带来的网络开销。

流控调节机制

流控调节机制能够调节发送流控限制帧的频率,以及流控限制的额度。流的调节机制可以有多种实现,QUIC标准给了我们一些参考。

  • 可以基于对rtt的估计值来调整流控帧发送的频率。
  • 流控限制的计算可以将应用消费数据的速率作为一个因素。

本篇总结

一张脑图总结下本篇内容。

image.png

QUIC的数据流控可以在两个层面对数据发送进行限制,分别是连接的层面和流的层面(一条连接可能包含多条流)。

无论是连接级别的流控还是流级别的流控,对发送数据的限制都分为初始限制和后续限制。初始限制在握手时的传输参数中携带,后续限制分别通过发送MA_DATA帧或MAX_STREAM_DATA帧来控制。

发送端受到流控限制时可以发送DATA_BLOCKED给接收端,告知自己的发送被流控限制阻塞了。若发送端没有遵守流控限制,接收端会主动的将连接关闭。

在流控实现时既要保证及时的发送流控帧以避免阻塞发送端,又要尽量减少发送流控帧的开销(跟随其它帧一并发送),在实现时由开发者自己做好权衡。

推荐阅读