TCP系列46—拥塞控制—9、SACK下的快速恢复与Limited transmit
一、概述
1、SACK下的特殊处理过程
SACK下的拥塞控制处理是linux中拥塞控制的实现依据,再次强调一遍RFC6675的重要性,linux中拥塞控制主体框架的实现是与RFC6675一致的,所以如果要理解linux中拥塞控制的实现,强烈建议看一下RFC6675。我这里给出RFC6675中SACK处理的2个关键点,下面的描述实际上不太严谨,严谨定义请参考RFC6675。
1、SACK下对于dup ACK的定义简单说是指反馈了新的SACK信息,也就是说SACK下ack number不同的ACK报文如果携带了新的SACK信息那么也是dup ACK,这是与SACK关闭场景下dup ACK的重大区别,这种场景下的示例请参考SACK重传的介绍文章。另外这一点也意味着在Disorder状态下,cwnd实际上是可以更新的。
2、SACK下会维护一个scoreboard,它可以标记出那些数据包已经lost需要重传,SACK下可以根据SACK信息,同时把多个数据包标记为lost。而在SACK关闭的时候,因为dup ACK不带有SACK信息,数据发送端收到dup ACK数量到达dupthresh的时候只能标记一个数据包为lost状态。实际上SACK把选择TCP待发送数据包的过程(如选择发送新的未发送数据还是发送重传数据?如果发送重传数据时候有多个数据包被标记为lost那么选择那个数据包来重传?)和拥塞控制解耦了。
3、SACK下引入了Pipe变量来表示目前还在网络中传输的数据量的估计值,这个是和linux实现中in_flight的含义一致的,关于in_flight和Pipe变量我在前面已经给出过比较详细的介绍,这里不再重复了。
至于发送端切换进入Recovery状态的处理以及从Recovery切换出来时候cwnd和ssthresh的更新处理与SACK关系场景是一致的,具体过程请参考前文。
二、Limited transmit
这里要介绍的limited transmit并不是SACK下特有的处理过程,在SACK关闭场景下也是一样的处理思路,之所以放在这里介绍,只是想把介绍内容分散开避免在一篇文章中介绍太多内容。
limited transmit中文字面意思就是受限传输,由RFC3042定义,在RFC5681和RFC6675中都有提及,limited transmit的主要目的是当发送窗口比较小的时候同样能有效的触发快速重传。例如dupthresh=3,发送窗口大小为3的时候,如果第一个数据包丢失,那么发送端只能收到两个dup ACK,按照传统快速重传算法的要求并不能触发快速重传,dup ACK意味着接收端收到了新的数据包,如果允许发送端收到dup ACK的时候发送新数据包,那么新发送的数据包就会触发足够的dup ACK从而触发快速重传(这里没有仅考虑传统的快速重传并没有考虑ER或者thin stream下的重传)。这种允许发送端在收到dup ACK时候发送新数据包的行为就是limited transmit,RFC3042限制limited transmit传输的最大数据包的个数不能超过(dupthresh-1)。实际上对应到Linux的实现,limited transmit就是对应TCP拥塞控制的Disorder状态的。
但是limited transmit不能解决的一种场景就是收到dup ACK的时候没有新的待发送的数据,此时就不足以触发传统形式的快速重传,这时候ER和thin stream重传就可以派上用途了。
三、wireshark示例
在执行本示例的测试前设置tcp_fack=0,关闭FACK功能,后面我们会单独介绍FACK的拥塞控制。同时如下设置initcwnd、ssthresh和reno拥塞控制算法,另外如果对于SACK下的dup ACK定义不清楚,请先参考之前介绍SACK重传的文章。
******@Inspiron:~$ sudo ip route add local127.0.0.2 dev lo congctl reno initcwnd 12 ssthresh lock 30 #参考本系列destination metric文章
******@Inspiron:~$ sudo ethtool -K lo tso off gso off #关闭tso gso以方便观察cwnd变化
1、SACK下的快速恢复综合示例
业务场景:server端在建立连接后,休眠1000ms,然后以3ms为间隔连续发送12个数据包,每个数据包大小为50bytes,接着一次write写入18*50bytes的数据。其中server端的发送缓存设置的足够大,应用层write不会因为发送缓存受限而休眠。server端发出的数据在传输的过程中,No10、No11、No13、No19这几个数据包丢失,下图中我手动高亮标记了(wireshark中手动标记的数据包为黑底白字,wireshark自动标记的数据包为黑底红字)。另外No9数据包虽然没有丢失,但是传输发生了乱序,导致No9数据包在No12之后到达,其余未特殊说明的数据包则按序到达client端,client按照数据包到达的顺序依次回复对应的ACK确认包。为了能在后面截图中显示更多的SACK信息,我设置降低了wireshark显示的时间精度并隐藏了部分列。
No1-No5:client与server建立连接,此时server端cwnd=12,ssthresh=30,即当前处于慢启动阶段。client发送请求报文,server端回复ACK确认包。
No6-No17:server端以3ms为间隔连续写入12次50bytes的数据包,发出后对应No6-No17,此时packets_out=12。server端在第12次write操作后立即写入18*50bytes的数据,但是此时受限与cwnd,而不能立即发出,只能暂时存放在发送缓存中。
No18-No20:No18是对应No6的ACK确认包,server端在收到这个No18后,更新cwnd=cwnd+1=13,同时packets_out=packets_out-1=11,in_flight= packets_out - ( sacked_out + lost_out) + retrans_out = 11-(0+0)+0=11。因此允许额外发出两个数据包,即对应No19和No20。发出No20后packets_out=13。
No21-No23、No24-No26:No21为No7的ACK确认包,No24为No8的ACK确认包。这两组数据包的处理流程与No18-No20类似。发出No26后,cwnd=15,packets_out=15。
No27-No28:因为No10、No11数据包丢失,而No9在No12后到达,因此No27实际上是client收到No12后回复的ACK报文。可以看到No27通过SACK确认了No12,通过ack number确认了No8数据包。server端在收到No27后,更新sacked_out=1,并从Open状态切换到DIsorder状态,初始化high_seq=901,接着尝试更新cwnd但是因为No27的ack number没有发生变化,所以此时reno的慢启动也不会更新cwnd。接着server端尝试发送新数据,此时in_flight= packets_out - ( sacked_out + lost_out) + retrans_out = 15-(1+0)+0=14,因此允许发出一个TCP数据包,即对应No28,更新packets_out=16。
No29-No31:No9在No12后乱序到达,No29即为对应No9的确认包,从截图可以看到No29的ack number确认了No9数据包,SACK确认了No12数据包,注意server端因为先收到No12在收到No9,检测到这个乱序传输后,就会更新dupthresh=4(dupthresh的更新参考之前的文章)。server端TCP在收到这个数据包的时候更新packets_out=packets_out-1=15。然后进入reno慢启动过程,No29的ack number新确认了一个数据包,因此cwnd=cwnd+1=16。接着server端尝试发送新数据包,此时in_flight=15-(1+0)+0=14,因此允许发出两个新的数据包,即对应No30和No31。发出No31后 packets_out=17。注意此时server端是处于Disorder状态的,因此在Disorder状态下cwnd也是会发生变化的。
No32-No33:因为No13数据包在传输过程中丢失,No32是No14触发的确认包,SACK中确认了No12和No14,server在收到No32后,更新sacked_out= sacked_out+1=2。No32的ack number并没有确认新数据,因此reno拥塞控制算法并没有更新cwnd。接着server端尝试发送新的数据,此时in_flight=17-(2+0)+0=15,cwnd=16,拥塞控制允许tcp发出一个数据包,即对应No33。发出No33后,packets_out=18。
No34-No35:server端在收到No34后更新sacked_out=sacked_out+1=3。此时已经满足默认的快速重传门限,但是因为No29的时候更新dupthresh=4,因而3个dup ACK未能触发快速重传。接着server端发出No35。更新packets_out=19。
No36-No37:server端在收到No36后,更新sacked_out=sacked_out+1=4,此时已经满足sacked_out>=dupthresh的快速重传门限,server端TCP从Disorder状态切换到Recovery状态,初始化prior_ssthresh=max(ssthresh,3/4*cwnd)=12, ssthresh=max(cwnd/2,2)=8, prior_cwnd=cwnd=16, prr_delivered=0,prr_out=0,high_seq=1151。接着server端开始标记丢失的数据包,对于SACK块我们按照系列号从大到小的顺序拆解成发出去的数据包如下:系列号最大的数据包系列号(501,551),系列号第2大的数据包(451,501),第3的为(401,451),第4的为(301,351),dupthresh为4就意味这,已经发送的还未被ACK确认的数据包到系列号为第4大的数据包之间的数据包都标记为lost。对于本例则意味这系列号(201,301)这个范围的两个数据包标记为lost,即对应No10和No11,因为更新lost_out=2。前面我们介绍的SACK关闭场景下,server收到dup ACK的时候只会把第一个还未确认的数据包标记为lost,而在有了SACK的信息后,server则可以一次标记多个数据包为lost。接着server端按照之前文章介绍的流程更新cwnd,此时in_flight=19-(4+2)-0=13, newly_acked_sacked=1, delta = ssthresh - in_flight=8-13=-5<0,prr_delivered=prr_delivered+1=1, sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out = (8*1+16-1)/16-0=1,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(1,1)=1,cwnd = in_flight + sndcnt=14。接着server端进入快速重传流程,虽然有两个数据包被标记为lost,但是此时拥塞窗口只允许TCP发出一个数据包,即No37,更新prr_out=1,retrans_out=1。接着server端尝试发送新数据同样由于cwnd限制而没能发出去。
No38:No38中的SACK块又重新确认了一个数据包,因此更新sacked_out=sacked_out+1=5。同样按照上面方法把SACK确认的数据包按照系列号从大到小排序,第四个则为(401,451),因此系列号401之前的未被sack确认的数据包(351,401)也会被标记为lost,更新lost_out=lost_out+1=3。此时in_flight=19- (5+3)+1=12,newly_acked_sacked=1, delta = ssthresh - in_flight=8-12=-4<0, prr_delivered=prr_delivered+1=2, sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out = (8*2+16-1)/16-1=0,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(0,0)=0,cwnd = in_flight + sndcnt =12。此时in_flight与cwnd相同,因此拥塞控制不允许发出TCP数据包,随后的快速重传流程和尝试发送新数据的流程都由于拥塞控制而没能发出新的数据包。
No39-No40:在No19数据包丢失后,No20触发client回复No39这个确认包,可以看到在No39中又新增了一个SACK块确认No20这个数据包。server收到这个数据包后,更新sacked_out=sacked_out+1=6, 此时in_flight=19-(6+3)+1=11, newly_acked_sacked=1, delta = ssthresh - in_flight=8-11=-3<0, prr_delivered=prr_delivered+1=3, sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out = (8*3+16-1)/16-1=1,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(1,0)=1,cwnd = in_flight + sndcnt =12。接着server进入快速重传流程,把标记为lost的(251,301)重传出去,此时虽然数据包(351,401)也标记为lost但是拥塞窗口已经不允许发出额外的数据包了,因此更新prr_out=2,retrans_out=2,退出快速重传流程,随后TCP尝试发送新数据的时候同样由于cwnd限制没能发出。
No41-No43:这组数据包的处理与No38-No40类似, 不在重复介绍,最终No43把标记为lost的第三个数据包发出后,packets_out=19,sacked_out=8, lost_out=3, retrans_out=3,prr_delivered=5, prr_out=3,ssthresh=8, cwnd=11。
No44:No44是对应No25的确认包,可以看到No44的SACK选项中新确认了No25数据包,因而更新sacked_out=9。同样把No44的SACK确认的数据包按照系列号从大到小进行排序,第4个则为(651,701),因此651系列号之前的还没有被SACK确认也没有被标记为lost的(601,651)被标记为lost,对应更新lost_out=4。此时in_flight=19-(9+4)+3=9,newly_acked_sacked=1, delta = 8-9=-1<0, prr_delivered= 6,sndcnt=(8*6+16-1)16-3=0, sndcnt=max(0,0)=0,cwnd=9。此时拥塞窗口不允许发出额外的数据包,因此后面的快速重传和尝试发送新数据的流程都没有发出数据包。
No45:No45是对应No26的ACK确认数据包,server收到No45后,更新sacked_out=10,此时in_flight=19-(10+4)+3=8,更新newly_acked_sacked=1, prr_delivered=7, delta = 8-8=0>=0,此时delta值变为大于等于零了,依据前面文章介绍的内容,则需要做如下更新sndcnt = min(delta, newly_acked_sacked)=0,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=0,因此cwnd=in_flight+sndcnt=8。此时server端虽然连续收到了两个dup ACK,但是更新后的cwnd并不允许发送新的数据包。
No46-No47:server端在收到No46的时候更新sacked_out=11,此时in_flight=7, 更新prr_delivered=8,delta=1>=0, sndcnt=min(1,1)=1, snd=max(1,0)=1, cwnd=in_flight+sndcnt=8,也就是说此时拥塞窗口允许额外发出一个数据包。接着server进入快速重传流程,把在收到No44时标记为lost的数据包以No47重传出去,随后更新prr_out=4,retrans_out=4。
No48-No49:这组数据包cwnd的更新与No46-No47相同,不同的是,更新完cwnd后,允许发出一个数据包,但是此时已经没有标记为lost还未重传的数据包了,因此tcp尝试发送新数据,最终发出No49数据包。发出No49后,packets_out=20,sacked_out=12, lost_out=4, retrans_out=4, prr_delivered=9, prr_out=5,ssthresh=8, cwnd=8。
No50-No51:这组数据的处理与No48-No49相同,发出No51后,packets_out=21,sacked_out=13, lost_out=4, retrans_out=4, prr_delivered=10, prr_out=6,ssthresh=8, cwnd=8。
No52-No53:这组数据的处理与No48-No49相同,发出No53后,packets_out=22,sacked_out=14, lost_out=4, retrans_out=4, prr_delivered=11, prr_out=7,ssthresh=8, cwnd=8。
No54-No55:这组数据的处理与No48-No49相同,发出No55后,packets_out=23,sacked_out=15, lost_out=4, retrans_out=4, prr_delivered=12, prr_out=8,ssthresh=8, cwnd=8。
No56-No57:client收到No37这个重传包后,回复No56确认包。No55的ack number确认了一个数据包,因此更新packets_out=22,而新确认的这个数据包正好是之前被SACK标记为lost的数据包,因而并没有被统计在sacked_out里面,因此sacked_out不变,lost_out=lost_out-1=3,retrans_out=retrans_out-1=3。接着进行cwnd的更新过程,此时in_flight=22-(15+3)-3=7,prr_delivered=prr_delivered+1=13,delta=1>=0, sndcnt=min(1,1)=1, snd=max(1,0)=1, cwnd=in_flight+sndcnt=8,此时拥塞窗口允许在发出一个数据包,由于此时已经没有待重传的数据包了,因此TCP发出了新数据,对应No56,更新packets_out=23,prr_out=9。
No58-No59:No58是No40这个重传包的Ack确认包,可以看到No58相比No56少了一个SACK块,说明client已经有一个hole被填上了,这个被填上的洞即为No10和No11丢包造成的。No58的ack number新确认了2个数据包。因此更新packets_out=21,两个数据包中一个是之前被SACK确认的数据包,因此更新sacked_out=14,另外一个被SACK标记为lost并在No40重传的数据包,因此更新lost_out=2, retrans_out=2。 接下来进行cwnd的更新,此时in_flight=21-(14+2)-2=7,prr_delivered=prr_delivered+1=14,delta=1>=0, sndcnt=min(1,1)=1, snd=max(1,0)=1, cwnd=in_flight+sndcnt=8,此时server端TCP拥塞控制允许发出一个数据包,当前已经没有标记为lost还未重传的数据包了,因此发出了新数据包,对应No59,并更新packets_out=22,prr_out=10。
No60-No61:这组数据包的处理与No58-No59类似,server端发出No61后, packets_out=18,sacked_out=10, lost_out=1, retrans_out=1, prr_delivered=15, prr_out=11,ssthresh=8, cwnd=8。
No62:client收到No47这个重传包后回复No62这个确认包,No62的Ack=1151=high_seq,但是此时是SACK功能打开的状态,因而可以安全的从Recovery状态切换到Open状态。最终server收到No62后,更新packets_out=7,sacked_out=0, lost_out=0, retrans_out=0,接着切换到Open状态,更新cwnd=ssthresh=8(实际上cwnd本来就是8,其值并没有发生变化)。接着server端tcp进入reno的拥塞避免过程,No62的ack number新确认了11个数据包,11/cwnd=1,因此更新cwnd_cnt=11-1*8=3,cwnd=cwnd+1=9。注意这里要与前面介绍的SACK关闭情况下的拥塞撤销的文章进行对比。
No63-No69:server端依次收到这7个数据包后通过拥塞避免过程更新cwnd,这7个确认包后,packets_out=0,sacked_out=0, lost_out=0, retrans_out=0,ssthresh=8, cwnd=10, cwnd_cnt=3+7-9=1。
最后给出系列号时序图如下:
补充说明:
1、Limited Transmit:https://www.eecis.udel.edu/~amer/856/LimitedTx-Amer.11s.ppt