QUIC是一种安全的通用传输协议,在[QUIC-TRANSPORT]中进行了描述。 本文档介绍了QUIC的丢包检测和拥塞控制机制。
本文中的关键词"MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", 和"OPTIONAL",当且只当它们以全大写形式存在时,应当以BCP 14 [RFC2119] [RFC8174]中描述的方式来解释。
本文中使用的定义如下所示:
触发ACK的帧(Ack-eliciting frames):除ACK、PADDING和CONNECTION_CLOSE以外的所有帧都触发ACK。
触发ACK的包(Ack-eliciting packets):包含ack-eliciting帧、且会触发接收者在最大ACK延迟之内生成一个ACK的包,叫做触发ACK的包。
在传输中的包(In-flight packets):包如果是ack-eliciting的,或者包含了一个PADDING帧,已被发送,但没被确认、没有宣称丢失、或者没有和旧密钥一起丢弃,那么这个包正在传输中(in-flight)。
QUIC中所有的传输的Packet在Header中有一个packet-level字段,用来显示加密的等级和所属于的包序号空间。正如在[QUIC-TRANSPORT]的12.3节所描述,加密等级标识所属于的包序号空间。 在一个连接的生命周期内,一个包序号空间中的包序号不会发生重复。 发送过程中一个空间内的包序号单调递增,从而来避免歧义。 这是允许某些数据包编号从不使用,从而留出故意的空间。
这个设计不再关注传输与重传的歧义性,这极大降低了QUIC对TCP丢包检测机制演绎的复杂性
QUIC包可以包含多个不同类型的帧。恢复机制保证了需要可靠性传输的数据和帧会被确认或被被声明丢失,如果有必要的话,会在新的包里边进行重传。 一个包中的帧的类型会影响恢复与拥塞控制逻辑:
- 所有包都会被确认,尽管不包含ack-eliciting的帧只会和ack-eliciting包一起被确认。
- 包含CRYPTO帧的长头包对于QUIC握手性能要求十分苛刻,并且使用更短的定时器来进行确认。
- 包含ACK和CONNECTION_CLOSE以外帧的包会被计入到拥塞控制限制,并且被认为是in-flight数据。
- 包含PADDING帧的包会使得传输中的字节数增加,且不会直接触发一个确认。
熟悉TCP丢包检测和拥塞控制的读者会发现这里的算法和著名的TCP算法是极其相似的。 因此QUIC和TCP协议上的差异导致了算法上的差异。我们在下文简单描述这些协议上的差异。
QUIC为每个加密等级使用了隔离的包序号空间,除了0-RTT和所有1-RTT密钥生成的使用了相同的包序号空间。 隔离的报序号空间保证了,使用一个加密等级发送的包的确认信息,不会导致使用不同加密等级的包被伪重传。 不同包序号空间之间,拥塞控制和RTT测量是统一的。
TCP将发送方的传输顺序与接收方的交付顺序合并,导致重传二义性问题([RETRANSMISSION])。 QUIC将传输顺序和交付顺序分离:用包号表示传输顺序,交付顺序由STREAM帧中的流偏移量(offset)决定。
QUIC的报序号在一个包序号空间内严格递增,并直接编码了传输顺序。高的包序号意味着后发,小的包序号意味着先发。 当一个包含ack-eliciting帧的包丢失了,QUIC将必要的帧包含在一个使用新的包序号的新包中,移除了收到一个ACK时哪个包被确认的二义性。 因此,可以进行更精确的RTT测量,很少发生伪重传,因此像快速重传这样的机制可以只根据包序号被广泛的应用。
这个设计点极大的简化了QUIC的丢包检测机制。大多数TCP机制根据TCP序列号隐式地推断传输顺序——这是一项重要的任务,尤其是TCP时间戳不可用时。
当一个包丢失时,QUIC进入丢包期;丢包期开始之后,只要发送的任意一个包被确认,就结束了丢包期。 TCP等待包序号空间之间的空隙被填满,因此当一行中的一个段丢失多次,丢包期可能会持续多个往返。 因为两端在一个丢包期内只应该减小一次拥塞窗口,QUIC会在每个经历丢包的往返后都减小一次拥塞窗口, 而TCP可能在多个往返后才减小一次。
QUIC ACK帧包含的信息与TCP选择性确认(SACKs,[RFC2018])中的信息类似。 但是QUIC不允许已被ACK的包被否认,大大的简化了两端的实现,并减小的发送端的内存压力。
QUIC支持很多ACK范围,相对于TCP的3个SACK范围。 在高丢包的场景,这加速了重传,减少了伪重传,并保证了前向处理是不依赖超时。
QUIC终端测量了包收到和包的确认信息发出两个节点之间的时间(接收方处理造成的延迟),允许对端维护一个更精确的RTT估值(见[QUIC-TRANSPORT]的13.2节)。
QUIC使用了一个探测超时和一个根据TCP的RTO计算的定时器(PTO;见6.2节),参见[RFC6297]。 QUIC的PTO包含了对端的期望最大确认延时,而不是一个固定的最小超时。 与TCP的RACK-TLP丢包检测算法([RACK])类似,QUIC在PTO过期时不减小拥塞窗口,因为在尾部的单个数据包丢失并不表示持续的拥塞。 相反,当宣布出现持续拥塞时,QUIC会减小拥塞窗口(见第7.6节)。
通过这样的方式,QUIC避免了不必要的拥塞窗口缩减,避免了纠正机制的需要,比如F-RTO([RFC5682])。 由于QUIC不会在一个PTO超时的时候缩减拥塞窗口,如果碰到了PTO超时,但是还有可用拥塞窗口,一个QUIC发送端可以发送更多的in-flight包。 这中情况在发送端是应用限制的且PTO定时器超时的时候发生。当受应用限制,QUIC的这个机制比TCP的RTO机制更激进,但是如果不收应用限制,那么跟TCP就差不多。
QUIC允许探测数据包在定时器到期时暂时超过拥塞窗口。
TCP最小拥塞窗口是一个包。然而,如果这个单独的包丢失,意味着发送者需要等待一个PTO(6.2节)来进行重传,这可能比一个RTT更久。 当接收者延迟了确认,发送一个单独的ack-eliciting包,同样增加了引入额外延迟的风险。
QUIC因此建议最小的拥塞窗口应该是2个包。虽然这增加了网络负载,但是确认安全的,因为在持续拥塞的情况下,发送者依旧会级数递减它的发送速率(6.2节)。
在上层,终端将发包到收到确认之间的时间成为RTT样本。终端使用RTT样本和对端上报的主机延迟(peer-reported host delay)来生成网络链路RTT的统计描述。 终端计算了每条链路上的如下三个值:链路生命周期内的最小延迟(min_rtt),指数加权动态平均值(smoothed_rtt),和观察到的RTT样本的平均偏差(rttvar,下文中将称为”variation“)
在收到一个满足了如下两个条件的ack帧之后,终端就会生成一个RTT样本:
- 拥有最大的确认包序号,且是新确认的;
- 至少有一个新确认的包是ack-eliciting的
RTT采样中的lastest_rtt是 当前被确认的最大的包发出后经过的时间。
latest_rtt = ack_time - send_time_of_largest_acked
只使用收到的ACK帧中的最大确认包来计算RTT采样。这是因为对端在ACK帧中只上报最大确认包的延迟。 但是RTT采样测量不会直接使用上报的ACK延迟,而是来调整后续smoothed_rtt和rttvar的计算时的RTT采样。
为了避免一个包产生多个RTT采样,一个ACK帧如果不是最新的确认最大包的ACK,那么不应该(SHOULD NOT)将它用来计算RTT。
如果一个用于确认至少一个ack-eliciting包ACK不是最新的,那么收到这个包的时候,必须不能(MUST NOT)根据它生成RTT采样。 当只收到non-ack-eliciting包时,对端通常不会发送ACK帧。因此如果一个ACK帧中只包含了对non-ack-eliciting包的确认的话,它可能包含了任意大的ACK延迟数值。 忽略这样的ACK帧可以来避免后续smoothed_rtt和rttvar计算时的困难。
当一个RTT钟收到多个ACK帧,发送者可能在一个RTT中生成多个RTT采样。 这样做可能导致smoothed_rtt和rttvar出现历史样本不充分的情况。确保RTT评估保留足够历史记录是一个公开的研究问题。
min_rtt是发送方对一段时间内给定网络链路观察到的最小RTT的估计。 在本文中,min_rtt被用在丢包检测中用来拒绝小到不合理的rtt采样。
min_rtt必须设置为第一个RTT采样的最新RTT。
在计算min_rtt时,终端只使用本地观测时间,同时不会调整对端上报的ACK延迟。 这样做允许终端完全依赖于终端的观察,为smoothed_rtt设置一个更低的边界,并且限制了因对端错误报告的延迟而产生的低估。
网络链路的RTT可能随着时间而改变。 如果链路的真实RTT减小,出现第一个更低的采样之后min_rtt会立刻进行调整。 如果链路的真实RTT增加,min_rtt也不会采用它, 使得未来比这个值更小的RTT采样可以被包括到smoothed_rtt中。
终端应该在建立持续拥塞后,将min_rtt设置为最新的RTT采样。 这是为了允许连接在发生中断性网络事件后重新设置min_rtt和smoothed_rtt的估计值(第5.3节),同时由于路径延迟的增加可能导致错误地声明持续拥塞。
终端可以在连接中的其他时间重新建立min_rtt,例如当流量较小且收到的确认延迟较低时。 实现不应该(SHOULD NOT)过于频繁地刷新min_rtt值,因为路径的实际最小RTT并不是经常可以观察到的。
smoothed_rtt是一个终端RTT采样的指数加权动态平均数,rttvar用平均变化估计RTT采样的变化。
smoothed_rtt的计算中,将链路延迟减去确认延迟后得到。这些延迟使用ACK帧中的ACK Delay字段来进行计算;见[QUIC-TRANSPORT]的19.3节。
在握手过程中,对端可能会报告大于其max_ack_delay的确认延迟([QUIC-TRANSPORT]第13.2.1节)。 为了解决这个问题,终端应该忽略max_ack_delay,直到握手被确认为止,如[QUIC-TLS]第4.1.2节所定义。 当它们发生时,这些较大的确认延迟可能是不重复的,并且仅限于握手。 因此,终端可以使用它们而不会将它们限制在max_ack_delay之内,从而避免RTT估计值的不必要膨胀。
请注意,如果在对端的确认延迟报告或终端的min_rtt估计中存在错误,那么较大的确认延迟会导致smoothed_rtt大幅膨胀。因此,在握手确认之前,如果调整确认延迟的RTT采样导致采样小于min_rtt,终端可以忽略RTT采样。
在握手确认后,任何由对端报告的大于其max_ack_delay的确认延迟都被归结为无意的但可能重复的延迟,例如对端的调度器延迟或之前确认的丢失。过多的延迟也可能是由于接收器不兼容造成的。因此,这些额外的延迟被认为是路径延迟的一部分,并被纳入RTT估计中。
因此,当使用对端上报的确认延迟来调整RTT采样时,终端:
- 可以(MAY)忽略Initial包的确认延迟,因为这些确认不会被对端延迟([QUIC-TRANSPORT]第13.2.1节)。
- 应该(SHOULD)忽略对端的max_ack_delay直到握手确认。
- 必须(MUST)使用确认延迟和握手确认后对端的max_ack_delay中较小的一个。
- 如果结果值小于min_rtt,则不得从RTT采样中减去确认延迟。这限制了由于对端误报而导致对smoothed_rtt的低估。
此外,当相应的解密密钥不能立即使用时,终端可能会推迟对确认的处理。 例如,客户端可能会收到一个0-RTT数据包的确认,但由于1-RTT数据包保护密钥尚不可用,所以它无法解密。 在这种情况下,终端应该从它的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采样上,使用该样本重置估计器。 这确保了估计器不会保留过去采样的历史。
在初始化后的第一个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 (min_rtt + ack_delay < latest_rtt):
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
QUIC发送端使用了确认来检测丢包,使用了探测超时来保证确认被接收。 这一节提供了这些算法的描述。
如果一个包丢失了,QUIC传输层就需要对其进行恢复,比如重传数据,发送更新后的帧,或者丢弃这个帧。 详情参考 [QUIC-TRANSPORT]第13.3小节。
不同于RTT测量和拥塞控制,丢包检测是独立于每个包序号空间的。 因为RTT和拥塞控制是路径的属性,而丢包检测还依赖于密钥可用性。
基于确认的丢包检测实现了TCP快速重传[RFC5681]、Early Retransmit [RFC5827]、FACK、SACK丢包恢复[RFC6675]、RACK的精髓。这一节简要说明了QUIC是如何实现这些算法的。
如果满足如下所有的条件,一个包就被宣称丢失了:
- 此包尚未被确认,在传输过程中,且在一个已被确认的包之前发送。
- 此包发送kPacketThreshold数据包(6.1.1节)早于确认数据包,或者发出去已经超过了一定时间(6.1.2节)。
确认说明了一个后发的包送达了,并且包个数阈值和时间阈值允许了一定程度的包重排序。
伪丢包会导致不必要的重传,并会因为拥塞控制器应对丢包而导致性能降级。 实现可以检测伪重传,并增加包和时间上的重排序阈值来减少伪重传和丢包时间。 具备自适应时间阈值的实现可以(MAY)选择以更小的初始重排序阈值开始,从而来最小化恢复延迟。
根据TCP丢包检测[RFC5681] [RFC6675]的最佳实践,推荐(RECOMMENDED)包重排序阈值(kPacketThreshold)的初始值为3。为了和TCP [RFC5681]保持一致,实现不应该(SHOULD NOT)使用一个小于3的阈值。
一些网络可能表现出更高的乱序程度,导致发送者检测到伪丢包。 此外,数据包重排在QUIC中可能比TCP中更常见,究其原因,能够观察和重排TCP数据包的网络元素不能对QUIC这样做,因为QUIC的数据包号是加密的。 在错误检测到丢包后增加重排阈值的算法,如RACK[RACK],已被证明在TCP中是有用的,并期望它能在QUIC中也起到作用。
一旦一个在相同包序号空间的后发的包被确认,且已经超过了一定阈值的时间,一个终端应当断定这个包丢失。 为了防止过早断定丢包,必须(MUST)把这个时间阈值设置为至少本地定时器粒度(以kGranularity常量表示)。 这个时间阈值计算方法如下:
max(kTimeThreshold * max(smoothed_rtt, latest_rtt), kGranularity)
如果在最大被确认的包之前发送的包还不能被断定为丢包,那么接下来应当(SHOULD)为剩余的事件设置一个定时器。
使用max(smoothed_rtt, latest_rtt)的方式,防止了如下两个问题:
- 最新的RTT采样比sRTT小,这有可能是因为ACK通过更短的链路进行了传输而引起的乱序。
- 最新的RTT采样比sRTT大,这可能是因为真实RTT的剧增,但是sRTT却无法及时跟上。
推荐(RECOMMENDED)的时间阈值(kTimeThreshold),成为RTT乘数,是9/8。 推荐(RECOMMENDED)的时间粒度(kGranularity)是1ms。
注:TCP的RACK([RACK])规定了一个稍大的阈值,相当于5/4,以达到类似的目的。QUIC的经验表明,9/8的效果很好。
实现可以(MAY)尝试绝对阈值,前一次连接的阈值,自适应阈值,或者包括RTT检查。 更小的阈值减少了乱序的还原能力,增加了伪重传;而更大的阈值则增加了丢包检测延迟。
当ack-eliciting包在期望时间内没有被确认,或者服务端没有校验客户端的地址,PTO超时就会触发一个或者两个探测数据报的发送。 PTO使连接可以恢复尾部数据、确认的丢失
与丢包检测一样,探测超时是按包序号空间计算的。也就是说,每个包序号空间计算一个PTO值。
PTO定时器超时事件不意味着丢包,必须不能(MUST)引起先前未确认的包被标记位丢失。 当收到一个确认时, 最新被确认的包将被丢包检测按照包和事件阈值机制的方式来进行处理。
QUIC中的PTO算法实现了TCP的Tail Loss Probe [RACK], RTO [RFC5681], 和 F-RTO算法。 超时根据TCP的重传超时周期来进行计算。
当传输了一个ack-eliciting包之后,发送端就开启了一个有如下PTO周期的定时器:
PTO = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay
PTO周期是发送者应当等待这个包确认消息的时间量。这个时间周期包含了smoothed_rtt,4*rttvar和max_ack_delay, 来计算接收者发送确认时可以延迟的时间。
当在initial或者handshake包序号空间使用PTO时,PTO周期计算中的max_ack_delay被设置为0,因为预计对端不会故意延迟这些包([QUIC-TRANSPORT]的13.2.1节)。
PTO周期必须(MUST)至少为kGranularity,以避免定时器立即超时。 当在多个数据包编号空间中发送确认数据包时,必须将定时器设置为初始和握手数据包序号空间中的较早值。
在确认握手之前,终端不得为应用数据包序号空间设置其PTO定时器。 这样做可以防止终端在对端还没有密钥来处理它们或终端还没有密钥来处理确认时,重新传输数据包中的信息。 例如,当客户端向服务器发送0-RTT数据包时,就可能发生这种情况;它是在不知道服务端是否能够解密这些数据包的情况下进行的。同样,当服务端在确认客户端已经验证了服务端的证书并因此可以读取这些1-RTT数据包之前发送1-RTT数据包时,也会发生这种情况。
每当发送或确认一个触发ACK的包,或者丢弃初始或握手密钥时,发送方应重新启动其PTO定时器([QUIC-TLS]的4.9节)。这确保了PTO总是根据最新估计的往返时间和跨包号序空间的正确数据包来设置。
当一个PTO定时器超时,必须(MUST)增加PTO补偿,使得PTO周期被设置为它当前的两倍。 当收到确认后,PTO补偿因子就会被重置,除了以下的情况:在握手期间,服务端比其他时候需要更久的响应时间。 为了保护这样的一个服务端被客户端重复探测,如果客户端还没有确认服务端是否完成了客户端地址校验,那么PTO补偿不会在这个客户端被重置。 也就是说,直到收到Initial包的确认,客户端不会重置PTO补偿因子。
持续发生PTO超时的总时长受空闲超时限制。
如果为时间阈值丢包检测设置了定时器,则不得(MUST NOT)设置 PTO 定时器;请参见第 6.1.2 节。 在大多数情况下,为时间阈值丢包检测而设置的定时器将比PTO提前到期,并且伪重传的概率更小。
同一个网络中,一个恢复后的连接可以(MAY)将上一次连接的最终sRTT作为当前连接的初始RTT。 当之前没有RTT可用,初始RTT应当(SHOULD)设置为333ms,这导致握开始于PTO为1秒,这也正是TCP的初始重传超时所推荐的那样(见[RFC6298]第2节)。
一个连接可以(MAY)使用发送PATH_CHALLENGE和收到PATH_RESPONSE之间的时间作为新链路的初始RTT(参考kInitialRtt),但是不应该(SHOULD NOT)这个延迟当做RTT采样。
Initial包和handshake包可能永远不会被确认,但是如果initial和handshake密钥被丢弃,它们就不会被不计入到in-flight数据。 当丢弃了initial或者handshake密钥,必须(MUST)重置PTO和丢包检测定时器,因为丢弃密钥意味着为当前丢弃的报序号空间设置了前向处理和丢包检测定时器。
直到服务端校验了链路上客户端的地址,服务端能够发送的数据量只能是收到的数据量的3倍。 如果没有额外的数据要发送,在收到客户端的数据报之前,必须不能(MUST NOT)激活服务端的PTO定时器,因为PTO时发送的不利于反放大攻击限制。 值得注意的是,即使收到了0-RTT,服务端也可能无法成功校验客户端地址。
由于接收更多客户端数据之前,服务端可能被阻塞,所以客户端有责任发送数据来让服务端解除阻塞,直到客户端确定服务端完成了客户端的地址校验。 也就是说,如果客户端没有收到任何一个握手包的确认,并且握手没被confirmed(见[QUIC-TLS]的4.1.2节),即使没有在传输中的数据包,客户端也必须设置探测定时器。 当PTO触发时,如果客户端有握手密钥,必须发送一个握手包,否则必须在UDP数据报中发送一个Initial包,有效载荷至少为1200字节。
当服务端收到包含重复CRYPTO数据的initial包,它可以假设客户端没有收到服务端在initial包中发送的所有CRYPTO数据, 或者客户端测算出来的RTT太小了。 当客户端在获得握手密钥之前收到Handshake 或者 1-RTT包,客户端可以假定服务端一些或者所有的initial包丢失了。
为了在这些条件下加速握手的完成,终端可以(MAY)在每个连接的有限次数内,在PTO到期之前发送一个包含未确认的CRYPTO数据的数据包,但要受到[QUIC-TRANSPORT]第8.1节的地址校验限制。 对每个连接最多这样做一次,就足以从单个数据包丢失中快速恢复。 端点如果总是在接收到它无法处理的数据包时重传数据包,就有可能造成数据包的无限交换。
端点也可以使用聚合数据包(见[QUIC-TRANSPORT]第12.2节)来确保每个数据报至少引起一个确认。 例如,客户端可以在其第一次传输中,将包含PING和PADDING帧的Initial数据包与一个0-RTT数据包聚合在一起,服务器可以将包含PING帧的Initial数据包与一个或多个数据包聚合在一起。
当PTO定时器超时,发送端必须(MUST)在包序号空间发送至少一个ack-eliciting包作为探测。 终端可以(MAY)发至多两个包含ack-eliciting包的全尺寸UDP数据报,来避免因单个UDP数据报或者多个包空间中的传输数据丢失导致的严重连续PTO超时。 所有在PTO中发送的探测数据必须(MUST)是ack-eliciting的。
除了在定时器超时的报序号空间中发送数据以外,发送者应当(SHOULD)在另外的报序号空间中使用in-flight数据来发送ack-eliciting包,如果有可能的话最好合并。 因为对端可能只有一个或者两个包序号空间的密钥,因此当服务端的initial和handshake数据都在传输中,或者客户端的handshake和ApplicationData在传输中,这种方法是很有效的。
如果发送者想触发一个更快的PTO确认,它可以忽略包序号来阻止ack延迟。
终端应该(SHOULD)在PTO超时时发送的数据包中包含新数据。 如果不能发送新数据,则可以发送以前发送的数据。 实现可以(MAY)使用另外的策略来决定探测包的内容,包括根据应用的特性发送新的或者被重传的数据。
发送者可能没有新的数据或者之前发送过的数据需要进行发送。举个例子,注意如下事件的顺序: 在STREAM帧中发送了新的应用数据,被认为丢失了, 然后在一个心得包中重传了这个数据, 接着原本的传输被确认了。 当没有数据要发送时,发送者应当(SHOULD)在一个单包中发送一个PING或者其他ack-eliciting帧,重新设置PTO定时器。
除了发送一个ack-eliciting包,另一种方法是发送者可以(MAY)将所有还在发送的包标记为丢失。 这么做避免了额外包的发送,但是面临着将丢包描述的更加剧烈的风险,从而可能引发拥塞控制器不必要地减小发送率。
连续的PTO周期会呈指数增长,因此,随着网络中的包继续被丢弃,连接恢复延迟也会呈指数增长。 在PTO超时的时候发送两个包增加了丢包还原能力,从而来减小连续PTO事件的可能性。
当PTO定时器多次超时,并且无法发送新数据,实现必须在每次发送同样载荷和发送不同载荷之间做选择。 发送相同载荷比较简单并保证了最高优先级的帧先到达。每次发送不同载荷减小了伪重传的概率。
Retry包会引发客户端重新发送另一个initial包,有效的重启了连接的过程。 一个retry包意味着收到了initial包,但还没处理。retry包不能被当成确认,因为它既不能说明一个包是被处理了,也不能说明包的序号。
收到retry包的客户端需要重置拥塞控制和丢包恢复状态,包括所有挂起状态定时器的重启。 其他连接状态,特别是加密握手消息,则会被回收。
客户端可以(MAY)根据第一个initial发出的时间,到retry或版本协商包收到的时间之间的时间间隔,计算一个RTT估值来告诉服务端。 初始RTT测算时,客户端可以(MAY)用这个值来替代它的默认值。
丢弃了Initial和Handshake包保护密钥之后( [QUIC-TLS]第4.10小节),所有以这些密钥加密的包不会再被确认,因为不能再处理它们的确认信息。发送者必须(MUST)丢弃所有和这些包有关的恢复状态,并且必须(MUST)把它们从拥塞控制的正在传输数据量统计中移除。
一旦终端开始交换握手包,它们就停止了Initial包的收发,参考 [QUIC-TRANSPORT]的17.2.2.1小节。 此时,所有in-flight状态的Initial包的恢复状态就被丢弃了。
当拒绝了0-RTT包,所有in-flight的0-RTT包的恢复状态就被丢弃了。
如果服务端接受了0-RTT,但是没有缓存在initial包之前到达的0-RTT,那么早先的0-RTT包就会被宣称为丢失。(这种情况并不常见。)
预计在用密钥加密的数据包被确认或宣布丢失后,密钥将被丢弃。 但是,一旦握手和1-RTT密钥被证明对客户端和服务端都可用,Initial和Handshake的secret就会被丢弃;见[QUIC-TLS]第4.9.1节。
本文详细说明了QUIC的类似于TCP NewReno的发送端拥塞控制器。
QUIC提供用于拥塞控制的信号都是通用且可以支持不同的发送端算法。各个发送端可以单边选择使用不同算法
如果发送端使用了和本文不同的控制器,那么这个被选的控制器必须(MUST)遵守[RFC8085]3.1小节描述的拥塞控制指导方针。
类似于TCP,值包含ACK帧的包不会计入传输,且不受拥塞控制。 不像TCP,QUIC可以检测这些包的丢失并可以(MAY)使用这个信息来调整拥塞控制器或者正在发送的纯ACK包的速率,但本文不会涉及如此的机制。
本文中的算法描述和使用的控制器拥塞窗口大小单位为字节。
如果终端再发数据会导致bytes_in_flight比拥塞窗口大,必须不能(MUST NOT)发送包,除非包在PTO定时器超时(6.2节)或进入恢复状态(7.3.2节)的时候发送。
如果连接支持ECN,QUIC会把IP头部中的Congestion Experienced(CE)码点当做是拥塞的信号。 本文描述了终端的在对端报告的ECN-CE计数增加时的响应。
QUIC以设置了初始值拥塞窗口的slow start开始每个连接。终端应该(SHOULD)使用最大数据报尺寸(max_datagram_size)10倍大小的初始拥塞窗口, 并且限制为大于14720字节或者最大数据报尺寸的2倍大小。这遵循了RFC6928的分析与建议,得益于8字节UDP头部(相对于TCP的20字节头部),增加字节限制。
如果最大数据报大小在连接过程中发生变化,初始拥塞窗口应该用新的大小重新计算。 如果为了完成握手而减小最大数据报大小,则拥塞窗口应设置为新的初始拥塞窗口。
在校验客户端地址之前,服务端可能还会受制于反放大限制。尽管范放大限制可以防止拥塞窗口被完全塞满并因此减缓了拥塞窗口的增长,它不会直接影响到拥塞窗口。
最小拥塞窗口是发生丢包、对端报告的ECN-CE计数增加、持续拥塞时,拥塞窗口可以缩减的最小值。 推荐值是 2 * max_datagram_size。
本文所述的NewReno拥塞控制器有三种不同的状态,如图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
Figure 1: Congestion Control States and Transitions
这些状态以及它们之间的转换将在后面的章节中描述。
当拥塞窗口低于慢启动阈值时,NewReno发送端就处于慢启动状态。 由于慢启动阈值初始化为无限值,所以发送端开始处于慢启动状态。
当发送端处于慢启动时,拥塞窗口在处理每个确认时按确认的字节数增加,这将导致拥塞窗口的指数级增长。 当数据包丢失对端报告的ECN-CE计数增加时,发送端必须退出慢启动,进入恢复期。 当拥塞窗口小于慢速启动阈值时,发送端随时会重新进入慢速启动,这只有在宣布持续拥塞后才会发生。
当NewRen发送端检测到数据包丢失或其对端报告的ECN-CE计数增加时,就会进入恢复期。 已经进入恢复期的发送端会停留在恢复期中,不会再进入恢复期。
在进入恢复期时,发送端必须将慢启动阈值设置为检测到丢包时拥塞窗口值的一半。 在退出恢复期之前,必须将拥塞窗口设置为比慢启动阈值更小的值。
实现可以在进入恢复期后立即减少拥塞窗口,也可以使用其他机制,如比例速率减少([PRR]),以更缓慢地减少拥塞窗口。 如果立即减少拥塞窗口,可以在减少之前发送一个数据包。 如果丢失的数据包中的数据被重新发送,这加快了丢包恢复的速度,类似于[RFC6675]第5节中描述的TCP。
恢复期的目的是将拥塞窗口减少限制为每次往返一次。 因此在恢复期间,拥塞窗口不会响应新的丢包或ECN-CE计数增加。
当恢复期内发送的数据包被确认时,恢复期结束,发送方进入拥塞避免状态。 这与TCP对恢复的定义略有不同,TCP定义其恢复期在开始恢复的丢失段得到确认([RFC5681])时结束。
在拥塞窗口达到或超过慢启动阈值且不在恢复期的任何时候,NewReno发送端都会处于拥塞避免状态。
处于拥塞避免中的发送端使用加法增加乘法减少(AIMD)的方法,必须将对拥塞窗口的增加限制为每个被确认的拥塞窗口的最大数据报大小。
当数据包丢失或对端报告的ECN-CE计数增加时,发送方退出拥塞避免,进入恢复期。
在握手过程中,当包到达时一些包保护密钥可能不可用,接收者可以选择丢包。 实际上,在initial包到达之前,handshake和0-rtt包都不能被处理;在握手完成之前,1-rtt包都不能被处理。 在对端获得包保护密钥之前,终端可以(MAY)忽视handshake、0-rtt、1-rtt包的丢失。 终端必须不能(MUST NOT)在指定的包序号空间忽视在最早被确认包后发送的包的丢失。
Probe包必须不能(MUST NOT)被拥塞控制器阻塞。发送端必须(MUST)将这些包计入到in-flight中,因为这些包增加了网络负载,却没有确认包丢失。 值得注意的是,发送probe包可能导致发送者在链路上的数据量超过拥塞窗口,直至收到证实包丢失或者送达的ACK。
当发送方确定在足够长的持续时间内发送的所有数据包都丢失时,网络将被视为持续拥塞。
持续拥塞时间的计算方法如下:
(smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay) *
kPersistentCongestionThreshold
与第6.2节中的PTO计算不同,此持续时间包括了max_ack_delay,而不考虑发生丢包的包序号空间。
这个持续时间允许发送方在建立持续拥塞之前发送尽可能多的数据包,包括一些响应PTO超时的数据包,就像TCP使用尾部丢包探针([RACK])和重传超时([RFC5681])一样。
kPersistentCongestion Threshold的值越大,发送方对网络中的持续拥塞的响应就越弱,这可能导致向拥塞网络中的激进发送。太小的值会导致发送者不必要地宣布持续拥塞,导致发送者的吞吐量降低。
kPersistentCongestion Threshold的推荐值是3,其结果是大约相当于TCP发送者在两个TLP之后宣布RTO的行为。
这种设计不使用连续的PTO事件来建立持续拥塞,因为应用程序模式会影响PTO的超时时间。 例如,发送端发送少量数据,其间有静默期,每次发送时都会重新启动PTO计时器,即使没有收到确认,也有可能防止PTO计时器长时间超时。持续时间的使用使发送方可以建立持续的拥塞,而不必依赖于PTO超时。
发送端在收到确认后,如果有两个触发ack的数据包被宣布丢失,就会产生持续拥塞,并且:
- 在所有的数据包序号空间中,这两个数据包的发送时间之间没有一个数据包被确认;
- 这两个数据包的发送时间之间的持续时间超过了持续拥塞的持续时间(第7.6.1节);
- 在发送这两个数据包时,存在一个先前的RTT采样。
这两个数据包必须是触发ACK的数据包,因为接收者必须在其最大ACK延迟内只确认触发ACK的数据包;见[QUIC-TRANSPORT]第13.2节。
在至少有一个RTT采样之前,持续拥塞期不应该开始。 在第一个RTT采样之前,发送方根据初始RTT(第6.2.2节)来启动它的PTO定时器,它可能比实际RTT大得多。 需要事先的RTT采样可以防止发送方用可能过少的探测建立持续拥塞。
由于网络拥塞不受包序号空间的影响,因此持续拥塞应该考虑跨包序号空间发送的数据包。 没有所有包序号空间状态的发送者,或者不能比较包序号空间发送时间的实现,可能仅将状态用于被确认的包序号空间。这可能会导致错误地声明持续拥塞,但不会导致检测不到持续拥塞。
当宣布持续拥塞时,发送者的拥塞窗口必须减少到最小拥塞窗口(kMinimumWindow),类似于TCP发送者对RTO的响应([RFC5681])。
下面的例子说明了发送端如何建立持续拥塞。 假设:
smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay = 2
kPersistentCongestionThreshold = 3
考虑以下一系列事件:
+========+===========================+
| Time | Action |
+========+===========================+
| t=0 | Send packet #1 (app data) |
+--------+---------------------------+
| t=1 | Send packet #2 (app data) |
+--------+---------------------------+
| t=1.2 | Recv acknowledgment of #1 |
+--------+---------------------------+
| t=2 | Send packet #3 (app data) |
+--------+---------------------------+
| t=3 | Send packet #4 (app data) |
+--------+---------------------------+
| t=4 | Send packet #5 (app data) |
+--------+---------------------------+
| t=5 | Send packet #6 (app data) |
+--------+---------------------------+
| t=6 | Send packet #7 (app data) |
+--------+---------------------------+
| t=8 | Send packet #8 (PTO 1) |
+--------+---------------------------+
| t=12 | Send packet #9 (PTO 2) |
+--------+---------------------------+
| t=12.2 | Recv acknowledgment of #9 |
+--------+---------------------------+
Table 1
当在t=12.2时收到数据包9的确认时,就会宣布数据包2至8丢失。
拥塞时间的计算方法是最早丢失的数据包与最新丢失的数据包之间的时间:8 - 1 = 7。 持续拥塞持续时间为:2 * 3 = 6。 由于达到了阈值,并且在最早丢失的数据包与最新丢失的数据包之间没有一个数据包被确认,因此网络被认为处于持续的拥塞状态。
虽然这个例子显示了PTO超时,但它并不是建立持续拥塞的必要条件。
发送方应该根据拥塞控制器的输入, 来定速发送所有传输中的数据包。
在没有任何延迟的情况下向网络中发送多个数据包,会产生数据包突发,可能会造成短期拥塞和丢包。 发送方必须(MUST)使用定速发送或限制这种突发。 发送方应将突发限制在初始拥塞窗口内;见第7.2节。 如果发送方知道通往接收方的网络路径可以吸收更大的突发,可以使用更高的限制。
实现需要注意完美契合拥塞控制器和定速器。比如,定速器可能包含了拥塞控制器,并且控制了拥塞窗口的可利用性,或者定速器可以根据拥塞控制器对交给它的待发送数据进行定速。
ACK帧的及时送达对于高效的丢包恢复是非常重要的。因此应当(SHOULD)对只包含ACK帧的包进行定速,从而来避免他们的延迟发送。
终端可以选择他们定速的实现。一个完美定速的发送端可以根据时间完美的分布包。 对于一个基于窗口的拥塞控制器,比如本文中的,速率可以这么计算:拥塞窗口除以往返时间。 以单位时间的字节来表示速率,其中congestion_window以字节为单位:
rate = N * congestion_window / smoothed_rtt
或者以单位时间的内部包间隔来表示速率:
interval = ( smoothed_rtt * packet_size / congestion_window ) / N
N是一个比较小的数值,但是至少是1(比如1.25),保证了在一个RTT中的差异不会导致拥塞窗口的利用不足。
实际的注意事项,比如分包,调度延迟和计算效率,都可能导致发送端在比RTT小的多的时间段内偏离这个速率。
一种合理的定速实现策略是使用漏桶算法,在这个算法中,桶的容量被限制为最大冲击数,并且上层功能决定了桶的填充速度。
当正在传输的数据字节量比拥塞窗口小,并且发送过程不做限速,那么拥塞窗口就没有被充分利用。 如果发生这种情况,在慢启动和拥塞避免过程中,都不应该(SHOULD NOT)增加拥塞窗口。 低效的应用数据或者流控限制会导致这个问题的发生。
对包进行定速的发送端可能延迟发包,并因这个延迟而没有充分利用拥塞窗口。 如果发送端在没有定速延迟的情况下已经充分利用了拥塞窗口,则发送端不应(SHOULD NOT)认为自己的应用受限。
发送端可以(MAY)实现另外的机制,在未充分利用拥塞窗口周期之后更新拥塞窗口,比如[RFC7661]中TCP的机制。
丢包检测和拥塞控制从根本上涉及未认证实体对信号的消耗,如延迟、包和ECN标记。攻击者可以通过操纵这些信号、通过丢包、通过战略性地改变路径延迟,或通过改变ECN码点,使终端降低其发送速率。
可以通过观察分析包的大小,来试探并识别只带ACK帧的包。确认信息的格式可以泄露连接特性和应用行为的信息。 为了减少信息泄露,终端可以将确认与其他帧捆绑在一起,也可以使用PADDING帧,但可能要付出性能的代价。
接收者可能误报ECN标记,从而改变发送者的拥塞响应。 抑制ECN-CE标记的报告可能导致发送端增加发送速率。这样的增速可能导致拥塞和丢包。
发送端可以(MAY)尝试偶尔在它们发送的包中标记ECN-CE,来检测ECN的抑制。 如果发送的带有ECN-CE标记的数据包在被确认时没有被报告为已被CE标记,则发送方可以通过在该路径上发送的后续数据包中不设置ECT代码点来禁用该路径的ECN [RFC3168]。
上报更多的ECN-CE标记会导致发送端减少发送率,这跟通报连接流控制限制的效果类似,并且这么做也没有什么特别的好处。
终端选择他们使用的拥塞控制器。 拥塞控制器通过降低其速率来响应ECN-CE的报告,但响应可能有所不同。 标记可以被视为等同于丢包([RFC3168]),但可以指定其他响应,例如([RFC8511])或([RFC8311])。