[toc]
1 简介
Quic是一个新的基于UDP的多路复用安全传输协议。
2 定义
- ACK-only:只包含ACK帧的包
- In-flight:一个已发送但未被确认或丢失的包,不包含ACK-only
- Retransmittable Frames(可重传帧):除了ACK和PADDIG的帧
- Retransmittable Packets(可重传包):包含Retransmittable Frames的包
- Crypto Packets:包含发送在Initial包或HandShake包的CRYPTO数据的包
3 Quic传输机制设计
Quic中的所有传输都有一个包级头部,该头部说明了加密级别(指示了包号空间)和一个包号。在一个连接的生命周期内,一个包号空间上的包号是单调递增的,不能重复使用,这有利于区分包是否是重传包,减低了丢包探测的复杂度。
一个Quic包中会包含不同类型的帧,这会导致不同的重传和拥塞控制逻辑:
- 所有包都会被确认,但只包含ACK或PADDING帧的包不会被立即确认。
- 包含CRYPTO帧的Long Header包对于Quic握手的性能很重要,需要为确认和重传使用更短的计时器。
- ACK-only包不计入拥塞控制的限制以及In-flight字节数。而PADDING帧会导致包计入In-flight字节数。
3.1 Quic与TCP的区别
Quic的丢包检测和拥塞控制算法与TCP类似,但协议的不同会导致算法的差异。
3.1.1 独立包号空间
除了0-RTT和所有1-RTT密钥使用相同的包号空间外,QUIC为每个加密级别使用单独的包号空间。独立的包号空间确保不同加密级别的包的确认不会导致错误重传。但对于不同包号空间,拥塞控制和RTT估计是一致的。
3.1.2 单调递增的包号
TCP将传输包中的数据的偏移量和包号合并在一起,这样就导致无法区分第一次发送包和重传包。而Quic将两者区分开,包号用于确定传输顺序,应用数据由STREAM帧中的偏移量字段决定。Quic包号是严格递增的,直接说明传输顺序。当一个包确认丢失后,包中的帧会在一个新的包中用新的包号进行传输,解决了重传二义性的问题。因此,使用包号,RTT估计将更加准确,虚假重传容易探测,快重传能够广泛的部署。
3.1.3 无反悔确认
Quic的ACK类似TCP的SACK,但Quic不允许要求已经确认的包重传。
3.1.4 更多的ACK块
与TCP的SACK只有三个SACK块不的同,Quic的ACK帧包含很多ACK块。在高丢包率的场景,这能加速恢复,减少伪重传,确保数据包转发进度。
3.1.5 延迟ACK的显示纠正
Quic的ACK帧在ACK Delay字段包含了接收到在接收到数据包到响应一个ACK的时间。这允许发送端在评估路径RTT时根据该字段进行调整,特别是ACK延迟计时器。
4 丢包检测
Quic发送端通过ACK信息和超时来检测包丢失。RTT估计是重要的一环。
4.1 计算RTT
RTT是在接收到ACK帧时计算的:
RTT = 接收ACK的时间 - 被确认包最新发送时间 - ACK Delay
if RTT < min_rtt
RTT = 接收ACK的时间 - 被确认包最新发送时间 // 忽略ACK Delay
min_rtt是在调整ACK时延之前,在连接上进行测量的。在RTT小于min_rtt时忽略ACK Delay能防止低估RTT,进而低估平滑RTT。
4.2 基于ACK的丢包检测
基于ack的丢包检测实现了TCP快重传、早期重传、FACK和SACK丢包恢复的意志。
4.2.1 快重传
一个未被确认的包,在该包之后的达到某个数量阈值(kReorderingThreshold)的包被确认,或者在该包发送之后一段时间未被确认,则该包被认为丢失了。接收到确认说明一个延迟的包接收到了。重排阈值(kReorderingThreshold)提供了网络中重排的包一定的容忍度。kReorderingThreshold的推荐初始值是3.
Quic可以使用基于时间的丢包探测,根据包从发送到现在经过了多少时间来处理重排问题,推荐的时间阈值(kTimeReorderingFraction)为1/8RTT。也可以由重排阈值或其他的方法代替。
4.2.2 早期重传
接近结束的未确认包之后可能没有足够的(kReorderingThreshold)包发送,这样包丢失是不能被快重传检测到的。为了使得基于ack的丢包检测在这种情况下也有效,接收到最后一个未完成的可重传数据包的确认,将触发早期重传。
如果有未确认的in-flight包还未处理,那么将视为丢失。为了补偿重排弹性,发送端应该设置一个计时器(时长不小于1.125*max(SRTT,lastest_RTT)),若在这段时间内未确认的in-flight包仍然未确认,那么必须视为丢失。
1.125*max(SRTT,lastest_RTT)可以应对以下两种情况:
- latest RTT 小于 SRTT:可能由于重排触发了早期重传,传输路径变短了。
- latest RTT 大于 SRTT:可能由于实际RTT的持续增加,SRTT还没跟上。
系数1.125增加重排弹性。也可以用其他系数,但是要注意:
- 太小的系数:减少了重排弹性,增加了伪重传;
- 太大的系数:增加了丢包发现的时延。
4.3 基于时间的丢失探测
基于ACK的丢包检测无法处理的包丢失可由基于时间的丢失探测来恢复,它使用一个计时器,在加密重传计时器、尾部探测计时器和重传超时机制之间切换。
4.3.1 加密超时重传
CRYPTO帧中的数据对于Quic传输和加密协商来说是至关重要的,因此其重传计时器比较aggressive,一般初始化为初始RTT的两倍。初始RTT设置如下:
- 同个网络的重连连接应该使用上一个连接最后的SRTT值作为其初始RTT;
- 若无上个连接RTT,或者网络发生了变化,初始RTT应该设为100ms。在收到一个ACK后,计算新的RTT,计时器设置为新计算的SRTT的两倍。
当加密包发送时,发送端必须为其设置加密重传计时器,如果超时了,发送端必须尽可能重传所有未确认的CRYPTO数据。直到服务器验证了客户端在路径上的地址之前,发送的数据量会受到限制。
- 若不是所有的未确认CRYPTO数据能被重传,Initial包中的所有未确认CRYPTO数据应该被重传;
- 若不能发送任何字节了,那么在客户端收到数据之前不能设置警报。
因为服务器可能被阻塞,直到收到更多的数据包,所以即使没有未确认的加密数据,客户端也必须启动加密重传计时器。
- 如果计时器过期,客户端没有要重新传输的加密数据,也没有握手密钥,那么它应该以UDP数据报发送至少1200字节的Initial包。
- 如果客户端有握手密钥,它应该发送HandShake包。
在连续多次未收到新包的ACK加密重传计时器就过期后,发送端必须使加密重传时间加倍。
如果加密包未解决,那么尾部探测(TLP)计时器和重传超时(RTO)计时器不会启动。
4.3.1.1 Retry包和Version Negotiation包
Retry包或Version Negotiation包导致一个客户端发送另一个Initial包,有效重启一个新的连接进程。两种包都指示Initial被接收但未处理,两种包都不能被当作Initial包的确认,但可以用于改进RTT估计。
4.3.2 尾部丢包探测(TLP)
尾包易受缓慢丢失探测攻击,因为需要其后续包来触发基于ACK的丢失探测。为了改善这情况,发送方在静止传输前的最后一个可重发包时设置一个计时器。在超时后,发送一个TLP包来引发接收端的ACK。这个探测超时(PTO)时间设置为:
PTO = max(1.5 * SRTT + MaxAckDelay, kMinTLPTimeout)
PTO = min(RTO, PTO)
- MaxAckDelay:会包含在所有探测超时中,因为Quic假设延迟确认会发生;
- 1.5*SRTT:确保ACK包已经过期;
为了减少延迟,建议发送方在设置RTO计时器之前设置并允许TLP计时器触发两次。也就是当TLP计时器第一次过期时,将发送一个TLP包,建议对TLP计时器进行第二次调度。当TLP计时器第二次过期时,将发送第二个TLP包,并应调度一个RTO计时器。
TLP包应该尽可能携带新数据。如果新数据不可用或由于流量控制无法发送新数据,TLP包可能会重新传输未确认的数据,从而减少恢复时间。由于TLP计时器用于在标记任何数据包丢失之前向网络发送探针,所以在TLP计时器过期前,不应该将之前未确认的数据包标记为丢失。
发送方可能不知道正在发送的包是尾包。因此,发送方可能需要在每个发送的可重发包上配置或调整TLP计时器。
4.3.3 重传超时(RTO)
RTO是丢包检测的最后解决方案。当最后的TLP包发送后,为RTO时期设置计时器。当该计时器过期后,发送端发送两个包来引发接收端的ACK,并重置RTO计时器。RTO时期设置:
如果最后一个TLP发送了,那么
RTO = max(SRTT + 4*RTTVAR + MaxAckDelay, kMinRTOTimeout)
如果RTO计时器过期,那么
RTO = 2 * RTO
发送方通常会在RTO计时器过期时进行高延迟惩罚,并且这种惩罚在随后的连续RTO事件中呈指数级增长。因此,在RTO事件上发送单个数据包会使连接对单个数据包丢失非常敏感,发送两个包则可以显著提高对两个方向丢包的恢复能力,从而降低连续RTO事件的概率。
QUIC的RTO算法与TCP的不同之处在于:
- RTO定时器的触发不被认为是一个足够强的数据包丢失信号,因此不会立即导致拥塞窗口或恢复状态的改变。RTO定时器仅在网络静默时间延长时过期,这可能是由于底层网络RTT的更改造成的。
- Quic在RTO时期使用了MaxAckDelay,因为Quic在计算SRTT和RTTVAR时纠正了MaxAckDelay的影响,所以需要在TLP和RTO计算时加上这部分。
当接收到RTO事件上发送的数据包的确认时,任何数据包号低于已确认的数据包的未确认数据包都必须标记为丢失。如果RTO上发送的包的确认与第一个RTO之前发送的包的确认同时收到,则认为RTO是假的,使用标准的丢失检测规则。
当RTO定时器过期时发送的数据包可能携带新的数据(如果可用或未确认的数据),从而减少恢复时间。由于此数据包在建立任何数据包丢失之前作为探针发送到网络中,因此不应该将之前未确认的数据包标记为丢失。
在RTO定时器上发送的数据包不能被发送方的拥塞控制器阻塞。然而,发送方必须将这些字节计入In-flight字节数,因为这个包增加了网络负载,而没有标记为丢失包。
4.4 生成ACK
Quic应该延迟确认接收到的数据包,但延迟时间会有限制,避免导致对端的伪超时。最大的延迟确认时间可以通过传输参数max_ack_delay协商,默认为25ms。
- 收到第二个数据包后应立即发送确认,但延迟不应超过最大延迟确认时间。QUIC恢复算法不假定对端在接收到第二个完整数据包时立即生成确认。
- 为了加速丢失恢复,应该更快确认无序的包。当接收的新包到不是比最大接收包号大1时,接收方应立即发送ACK。
- 带有EC码点的包应该立即确认,以减少对端对拥塞实践的响应时间。
- 作为优化,接收方可以在发送ACK之前处理多个包。在这种情况下,发送端可以确定在处理传入包之后应该生成即时确认还是延迟确认。
4.4.1 加密握手数据
为了快速完成握手并避免由于加密重传超时而造成的伪重传,加密数据包应该使用非常短的延迟确认时间,比如1ms。当加密栈指示已接收到该加密级别的所有数据时,可以立即发送ACK帧。
4.4.2 ACK块
当发送ACK帧时,包含一个或多个已确认的包范围。包含旧的包可以减少由于丢失先前发送的ACK帧而导致的伪重传,代价是ACK帧的大小。
ACK帧应该总是确认最近接收到的包,包的无序程度越高,快速发送更新的ACK帧就越重要,以防止对端声明包丢失并进行虚重传。
4.4.3 ACK帧接收端跟踪
当包含ACK帧的包被确认时,接收方可以停止确认小于或等于发送的ACK帧中已确认的最大的包。
4.5 伪代码
4.5.1 相关常量
- kMaxTLPs:在RTO过期前的最大TLP包数量,推荐数值为2。
- kReorderingThreshold:FACK模式丢包探测标记一个包丢失前的最大重排包号,推荐数值为3。
- kTimeReorderingFraction:基于时间的丢包检测标记一个包丢失前的最大重排时间系数,推荐数值为1/8。
- kUsingTimeLossDetection:是否使用基于时间的丢包耗检测。如果为false,则使用FACK样式的丢失检测。推荐数值为false。
- kMinTLPTimeout:在未来一个TLP定时器可以设置的最小时间,推荐数值为10ms。
- kMinRTOTimeout:在未来一个RTO定时器可以设置的最小时间,推荐数值为200ms。
- kDelayedAckTimeout:对端延迟确认时间长度,推荐数值为25ms。
- kInitialRtt:初始化RTT,推荐数值为100ms。
4.5.2 相关变量
- loss_detection_timer:用于丢包检测的多模式定时器。
- crypto_count:所有未确认CRYPTO数据在没有收到ack的情况下被重新传输的次数。
- tlp_count:在没有收到ack的情况下发送的TLP包的数量。
- rto_count:在没有收到ack的情况下发送的RTO包的数量。
- largest_sent_before_rto:在第一次重传超时之前发送的最后一个包号。
- time_of_last_sent_retransmittable_packet:最近的可重发数据包的发送时间。
- time_of_last_sent_crypto_packet:最近的加密包的发送时间。
- largest_sent_packet:最近发的包的包号。
- largest_acked_packet:ACK帧中最大的数据包号。
- latest_rtt:最近接收到一个未确认包的ACK时测量的RTT。
- smoothed_rtt:连接的SRTT。
- rttvar:RTT变量。
- min_rtt:连接目前为止,忽略了延迟确认的时长,最小的RTT值。
- max_ack_delay:最大延迟确认时间,单位ms。因为超时、重排和ACK丢失等原因,计时实际的值可能偏大。
- reordering_threshold:标记包丢失前,最大的已确认的可重发包与未确认的可重发包之间的最大包号间隔。
- time_reordering_fraction:重排窗口,max(smoothed_rtt, latest_rtt)的系数。
- loss_time:???根据早期传输,或超过重排窗口时,认为下一个数据包丢失的时间。
- sent_packets:一个包结构,该结构中的包根据包号进行排序,包在被确认或者标记为丢失前均保存在该结构中。每个包号空间维持一个这样的结构。结构包含了以下包信息:
- 包号字段:包号
- 时间字段:包发送的时间
- 布尔值:是否为ack-only
- 布尔值:是否计入In-flight字节
- 字节字段:包大小
4.5.3 初始化
连接建立之初,对丢包检测变量的初始化:
loss_detection_timer.reset()
crypto_count = 0
tlp_count = 0
rto_count = 0
if (kUsingTimeLossDetection):
reordering_threshold = infinite
time_reordering_fraction = kTimeReorderingFraction
else:
reordering_threshold = kReorderingThreshold
time_reordering_fraction = infinite
loss_time = 0
smoothed_rtt = 0
rttvar = 0
min_rtt = infinite
largest_sent_before_rto = 0
time_of_last_sent_retransmittable_packet = 0
time_of_last_sent_crypto_packet = 0
largest_sent_packet = 0
4.5.4 发包
发送任何数据包之后,不管是新的传输还是重新绑定的传输,都会调用下面的OnPacketSent函数。
OnPacketSent(packet_number, ack_only, in_flight, is_crypto_packet, sent_bytes):
// packet_number:发送的包的包号
// ack_only:该包是否只包含ACK或PADDING帧
// in_flight:该包是否计入in-flight字节数
// is_crypto_packet:该包是否包含完成Quic握手的关键加密握手信息
// sent_bytes:该包包含的字节数,不包括UDP和IP头部
largest_sent_packet = packet_number
sent_packets[packet_number].packet_number = packet_number
sent_packets[packet_number].time = now
sent_packets[packet_number].ack_only = ack_only
sent_packets[packet_number].in_flight = in_flight
if !ack_only:
if is_crypto_packet:
time_of_last_sent_crypto_packet = now
time_of_last_sent_retransmittable_packet = now
OnPacketSentCC(sent_bytes) // 见5.8.4小节
sent_packets[packet_number].bytes = sent_bytes
SetLossDetectionTimer() // 见4.5.7小节
4.5.5 接收ACK
OnAckReceived(ack):
largest_acked_packet = ack.largest_acked
// If the largest acknowledged is newly acked, update the RTT.
if (sent_packets[ack.largest_acked]):
latest_rtt = now - sent_packets[ack.largest_acked].time
UpdateRtt(latest_rtt, ack.ack_delay)
// Find all newly acked packets in this ACK frame
newly_acked_packets = DetermineNewlyAckedPackets(ack)
for acked_packet in newly_acked_packets:
OnPacketAcked(acked_packet.packet_number) // 见4.5.6小节
if !newly_acked_packets.empty():
// Find the smallest newly acknowledged packet
smallest_newly_acked = FindSmallestNewlyAcked(newly_acked_packets)
// If any packets sent prior to RTO were acked, then the
// RTO was spurious. Otherwise, inform congestion control.
if (rto_count > 0 && smallest_newly_acked > largest_sent_before_rto):
OnRetransmissionTimeoutVerified(smallest_newly_acked) // 见5.8.9小节
crypto_count = 0
tlp_count = 0
rto_count = 0
DetectLostPackets(ack.largest_acked_packet) // 见4.5.7小节
SetLossDetectionTimer() // 见4.5.8小节
// Process ECN information if present.
if (ACK frame contains ECN information):
ProcessECN(ack) // 见5.8.7小节
UpdateRtt(latest_rtt, ack_delay):
// min_rtt ignores ack delay.
min_rtt = min(min_rtt, latest_rtt)
// Adjust for ack delay if it’s plausible.
if (latest_rtt - min_rtt > ack_delay):
latest_rtt -= ack_delay
// Based on {{RFC6298}}.
if (smoothed_rtt == 0):
smoothed_rtt = latest_rtt
rttvar = latest_rtt / 2
else:
rttvar_sample = abs(smoothed_rtt - latest_rtt)
rttvar = 3/4 * rttvar + 1/4 * rttvar_sample
smoothed_rtt = 7/8 * smoothed_rtt + 1/8 * latest_rtt
4.5.6 确认包
一个ACK帧可能确认多个包,需要为每个被确认的包调用一次OnPacketAcked函数。
OnPacketAcked(acked_packet):
if (!acked_packet.is_ack_only):
OnPacketAckedCC(acked_packet) // 见5.8.4小节
sent_packets.remove(acked_packet.packet_number)
4.5.7 设置丢包检测计时器
QUIC丢包检测使用一个单一的定时器进行所有基于定时器的丢包检测。计时器的持续时间基于计时器的模式,该模式在下面的包和计时器事件中设置。下面定义的函数SetLossDetectionTimer显示了如何设置单个计时器。
SetLossDetectionTimer():
// Don’t arm timer if there are no retransmittable packets
// in flight.
if (bytes_in_flight == 0):
loss_detection_timer.cancel()
return
if (crypto packets are outstanding):
// Crypto retransmission timer.
if (smoothed_rtt == 0):
timeout = 2 * kInitialRtt
else:
timeout = 2 * smoothed_rtt
timeout = max(timeout, kMinTLPTimeout)
timeout = timeout * (2 ^ crypto_count)
loss_detection_timer.set(time_of_last_sent_crypto_packet + timeout)
return
if (loss_time != 0):
// Early retransmit timer or time loss detection.
timeout = loss_time - time_of_last_sent_retransmittable_packet
else:
// RTO or TLP timer
// Calculate RTO duration
timeout = smoothed_rtt + 4 * rttvar + max_ack_delay
timeout = max(timeout, kMinRTOTimeout)
timeout = timeout * (2 ^ rto_count)
if (tlp_count < kMaxTLPs):
// Tail Loss Probe
tlp_timeout = max(1.5 * smoothed_rtt + max_ack_delay, kMinTLPTimeout)
timeout = min(tlp_timeout, timeout)
loss_detection_timer.set(time_of_last_sent_retransmittable_packet + timeout)
4.5.8 超时
当丢失检测计时器过期时,计时器的模式确定要执行的操作。
OnLossDetectionTimeout():
if (crypto packets are outstanding):
// Crypto retransmission timeout.
RetransmitUnackedCryptoData()
crypto_count++
else if (loss_time != 0):
// Early retransmit or Time Loss Detection
DetectLostPackets(largest_acked_packet) // 见4.5.9小节
else if (tlp_count < kMaxTLPs):
// Tail Loss Probe.
SendOnePacket()
tlp_count++
else:
// RTO.
if (rto_count == 0)
largest_sent_before_rto = largest_sent_packet
SendTwoPackets()
rto_count++
SetLossDetectionTimer() // 见4.5.7小节
4.5.9 探测丢包
QUIC中,只有当在相同的包号空间中确认比一个包更大的包号时,包才会被认为丢失。
DetectLostPackets在每次收到ack时被调用,并对该包号空间的sent_packet进行操作。
如果丢包检测计时器过期,并且设置了loss_time,则提供之前最大的确认包。
DetectLostPackets(largest_acked):
loss_time = 0
lost_packets = {}
delay_until_lost = infinite
if (kUsingTimeLossDetection):
delay_until_lost = (1 + time_reordering_fraction) * max(latest_rtt, smoothed_rtt)
else if (largest_acked.packet_number == largest_sent_packet):
// Early retransmit timer.
delay_until_lost = 9/8 * max(latest_rtt, smoothed_rtt)
foreach (unacked < largest_acked.packet_number):
time_since_sent = now() - unacked.time_sent
delta = largest_acked.packet_number - unacked.packet_number
if (time_since_sent > delay_until_lost || delta > reordering_threshold):
// 包在被确认或标记为丢失前保存在sent_packets中
sent_packets.remove(unacked.packet_number)
if (!unacked.is_ack_only):
lost_packets.insert(unacked)
else if (loss_time == 0 && delay_until_lost != infinite):
// 只有进入早期重传阶段,才会设置loss_time
loss_time = now() + delay_until_lost - time_since_sent
// Inform the congestion controller of lost packets and
// lets it decide whether to retransmit immediately.
if (!lost_packets.empty()):
OnPacketsLost(lost_packets) // 见5.8.8小节
4.6 讨论
选择较短的延迟确认时间(25ms),是因为较长的延迟确认会导致延迟丢失恢复,对于少数连接来说,它们发送包的频率大于25ms/包,那么对每个数据包进行确认有利于拥塞控制和丢失恢复。
之所以选择默认的初始RTT(100ms),是因为它略高于公共互联网上观察到的min_rtt中位数和平均值。
5 拥塞控制
Quic的拥塞控制是基于TCP的NewReno算法的,该算法基于拥塞窗口进行控制。Quic的拥塞控制是通过字节数来进行的(TCP是根据包个数),因为Quic有更好的控制和恰当的字节计数。Quic主机不能发送超过拥塞窗口字节数的会增加bytes_in_flight的包,除非是在TLP或RTO计时器过期后发送的探测包。
不同终端可能使用不同的拥塞控制算法。QUIC提供的用于拥塞控制的信号是通用的,支持不同的拥塞控制算法。
5.1 显示拥塞通知(ECN)
如果一个路径被验证了支持ECN, QUIC将IP报头中有CE码点视为拥塞的信号。
5.2 慢开始
QUIC每个连接都是慢开始的,在包丢失或ECN-CE计数器增加时退出慢开始。
当拥塞窗口小于ssthresh时,QUIC就会重新进入慢开始,这通常只发生在RTO之后。
在慢开始时,QUIC会在处理每个ack时将阻塞窗口的字节数增加。
5.3 拥塞避免
慢开始过程中拥塞窗口增加到一个阈值后,进入拥塞避免。在NewReno中,拥塞避免使用一种加法增加乘减少(AIMD)方法,该方法在每个已确认的拥塞窗口中,将拥塞窗口增加一个最大包大小。当检测到丢失时,NewReno将拥塞窗口减半,并将慢开始阈值设置为新的阻塞窗口。
5.4 恢复阶段
恢复是从检测丢失的包或ECN-CE计数器的增加开始后的一段时间。由于QUIC重新传输流数据和控制帧,而不是数据包,所以将恢复结束定义为在确认恢复开始后发送的数据包。这与TCP的恢复定义稍有不同,TCP的恢复定义在确认启动恢复的丢失包时结束。
恢复期将拥塞窗口的减少限制为每次往返一次。在恢复期间,无论出现新的丢包或ECN-CE计数器增加,拥塞窗口都保持不变。
5.5 尾部丢包探测(TLP)
TLP包不能被发送方的拥塞控制器阻塞。
然而,发送方必须将这些字节计入bytes-in-flight,因为TLP在不确定数据包丢失的情况下增加了网络负载。TLP包的确认或丢失与任何其他包一样。
5.6 重传超时(RTO)
当由于RTO计时器而发送重传时,在下一个确认到达之前不会对拥塞窗口进行更改。
- 当此ACK确认在第一次重传超时之前发送的数据包时,重传超时被认为是假的。
- 当此ACK确认在第一次重传超时之前没有发送包时,重传超时被认为是有效的。在这种情况下,拥塞窗口必须减少到最小拥塞窗口,并重新进入慢开始。
5.7 同步
建议发送方根据拥塞控制器的输入对所有in-flight的数据包进行同步发送。例如,当与基于窗口的控制器一起使用时,pacer可以在SRTT上分配拥塞窗口,pacer可以使用基于速率的控制器的速率估计。
拥塞控制器应该要能够很好地与pacer协同工作。例如,pacer可以包装拥塞控制器并控制拥塞窗口的可用性,或者pacer可以对拥塞控制器传递给它的包进行定步。ACK帧的及时交付对于有效的丢包恢复是非常重要的。因此,只包含ACK帧的包不应该被定步,以避免延迟它们发送给对等方。
5.8 伪代码
5.8.1 相关常量
- kMaxDatagramSize:发送方的最大有效负载大小,不包括UDP或IP开销。用于计算初始和最小拥塞窗口。推荐数值为1200字节。
- kInitialWindow:未完成数据初始数量的默认限制(以字节为单位)。推荐数值为min(10 * kMaxDatagramSize, max(2 * kMaxDatagramSize, 14600))。
- kMinimumWindow:最小拥塞控制窗口。推荐数值为2 * kMaxDatagramSize。
- kLossReductionFactor:丢包事件发生时拥塞窗口的下降系数。推荐数值为0.5。
5.8.2 相关变量
- ecn_ce_counter:对端的ACK帧中报告的ECN-CE计数器最大值。此变量用于检测报告的ECN-CE计数器中的增长。
- bytes_in_flight:包含至少一个可重发或PADDING帧且未被标记或声明丢失的所有已发送包的字节大小之和。该大小不包括IP或UDP开销,但包括QUIC头和AEAD开销。仅包含ACK帧的数据包不计入bytes_in_flight,以确保拥塞控制不会妨碍拥塞反馈。
- congestion_window:可发送的最大in-flight字节数。
- end_of_recovery:当QUIC检测到丢失时发送的最大数据包号。当一个更大的数据包号被确认时,QUIC退出恢复。
- ssthresh:慢开始阈值(以字节为单位)。当拥塞窗口低于ssthresh时,模式为慢开始,窗口按已确认的字节数增长。
5.8.3 初始化
连接之初,初始化拥塞控制变量:
congestion_window = kInitialWindow
bytes_in_flight = 0
end_of_recovery = 0
ssthresh = infinite
ecn_ce_counter = 0
5.8.4 发包
无论何时发送一个包(它包含非ack帧),该包都会增加bytes_in_flight。
OnPacketSentCC(bytes_sent):
bytes_in_flight += bytes_sent
5.8.5 确认包
从丢失检测调用OnPacketAcked,并提供了来自sent_packet的acked_packet。
InRecovery(packet_number):
return packet_number <= end_of_recovery
OnPacketAckedCC(acked_packet):
// Remove from bytes_in_flight.
bytes_in_flight -= acked_packet.bytes
if (InRecovery(acked_packet.packet_number)):
// Do not increase congestion window in recovery period.
return
if (congestion_window < ssthresh):
// Slow start.
congestion_window += acked_packet.bytes
else:
// Congestion avoidance.
congestion_window += kMaxDatagramSize * acked_packet.bytes / congestion_window
5.8.6 新的拥塞事件
当检测到新的拥塞事件时,从ProcessECN和OnPacketsLost调用。启动一个新的恢复周期并减少阻塞窗口。
CongestionEvent(packet_number):
// Start a new congestion event if packet_number
// is larger than the end of the previous recovery epoch.
if (!InRecovery(packet_number)): // 见5.8.5小节
end_of_recovery = largest_sent_packet
congestion_window *= kLossReductionFactor
congestion_window = max(congestion_window, kMinimumWindow)
ssthresh = congestion_window
5.8.7 处理ECN信息
当从对端接收到带有ECN码点的ACK帧时调用。
ProcessECN(ack):
// If the ECN-CE counter reported by the peer has increased, this could be a new congestion event.
if (ack.ce_counter > ecn_ce_counter):
ecn_ce_counter = ack.ce_counter
// Start a new congestion event if the last acknowledged
// packet is past the end of the previous recovery epoch.
CongestionEvent(ack.largest_acked_packet) // 见5.8.6小节
5.8.8 包丢失
当检测到新包丢失时,由DetectLostPackets调用。
OnPacketsLost(lost_packets):
// Remove lost packets from bytes_in_flight.
for (lost_packet : lost_packets):
bytes_in_flight -= lost_packet.bytes
largest_lost_packet = lost_packets.last()
// Start a new congestion epoch if the last lost packet
// is past the end of the previous recovery epoch.
CongestionEvent(largest_lost_packet.packet_number) // 见5.8.6小节
5.8.9 重新传输超时证实
一旦证实了重新传输超时(RTO),QUIC将拥塞窗口减少到最小值,并删除在新确认的RTO包之前发送的任何包。
OnRetransmissionTimeoutVerified(packet_number):
congestion_window = kMinimumWindow
// Declare all packets prior to packet_number lost.
for (sent_packet: sent_packets):
if (sent_packet.packet_number < packet_number):
bytes_in_flight -= sent_packet.bytes
sent_packets.remove(sent_packet.packet_number)
6 安全考虑
6.1 拥塞信号
拥塞控制从根本上涉及到从未经身份验证的实体消耗信号(包括丢失和ECN码点)。On-path攻击者可以欺骗或改变这些信号。攻击者可以通过丢弃数据包来降低终端的发送速率,或者通过更改ECN码点来更改发送速率。
6.2 流量分析
ack-only包可以通过观察包大小来直观地识别。确认模式可能暴露关于链接特征或应用程序行为的信息。终端可以使用填充帧或将确认信息与其他帧捆绑在一起,以减少泄漏信息。
6.3 谎报ECN标记
接收方可以谎报ECN标记来更改发送方的拥塞响应。抑制ECN-CE标记的报告可能会导致发送方增加其发送速率,可能导致堵塞和丢包。发送方可能试图通过标记偶尔使用ECN-CE发送的包来检测报告是否被抑制。如果使用ECN-CE标记的包在被确认时没有被报告为已被标记,则发送方应针对该路径禁用ECN。
报告额外的ECN-CE标记将导致发送方降低其发送速率,这与发布减少连接流控制限制的效果类似,因此这样做没有任何好处。