mylinuxer

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

转载 http://weibo.com/p/1001603859360483780913

理解TCP协议的重传时间(全三篇&修订版)

2015年6月30日 09:11 阅读 2807

作者:新浪微博(@NP等不等于P

计算机学习微信公众号(jsj_xx)

(上)
基础概念

TCP可靠性的一个保证机制就是超时重传,而超时重传的核心是重传超时时间的计算。我们分享自己的理解,分三部分:上部分侧重RFC解读,中部分侧重linux实现,下部分侧重问题探讨。

本端发送一个数据包给对端,然后对端返回一个ack。这样,本端就能计算出这个包来回所需的时间,这个时间就是RTT(Round-TripTime),很简单,就是一个时间差。具体讲,它由三部分组成:链路层的传播时间、端点协议栈的处理时间、中间设备的处理时间(如路由器缓存)。主要时间应该耗在中间设备的中转,可见,RTT一定程度上体现了网络的拥塞程度。

重传超时时间RTO(Retransmission TimeOut)是基于RTT(而RTT是动态变化的,所以RTO也得动态调整)计算出的一个定时器超时时间。它的作用就是在发送一个数据包之后,开始计时,如果超时时这个数据包还没有被ack,表明传输中出现问题了则进入超时处理流程(就是重传流程)。

这里插入一点方法论。有些人看网络协议部分的代码时迷糊,其实就是由于没有参考RFC。所以我们是先讲RFC,再看代码。

1 相关RFC解读

RTO相关的RFC,目前以RFC6298(2011年发布,废弃RFC2988,更新RFC1122)为准。虽然它本身仅仅是追加了一个小需求:将初始化RTO从3秒改为1秒,但是内容上还是完整呈现了RTO相关流程。

以下是我们对RFC6298的总结和理解:

1)RTO的初始值得考虑两个均衡:太大,不能及早检测出丢包;太小会增加重传频率。本RFC规定设置RTO初始值为1秒(之前是3秒),具体原因如下:

  • RTO初始值为3,是在RFC 1122(1989年发布)中定义的,现在的网络比当时的网络要快太多。

  • 97.5%的网络的RTT小于1秒。

  • 三次握手期间的重传概率很低,只有2%。

  • 有2.5%的网络的RTT是大于1秒的,故它们会在三次握手期间重传,但是之后RTO就重新设置为3(保守性体现)了。

  • 不会影响RFC5681,三次握手的重传期间的拥塞窗口还是为1,即网络上仅仅多了一个syn报文而已。可以说,对重传的控制压缩到极点了。

  • 如果支持时间戳选项,则不需要将RTO重置为3了。也就是说,初始化RTO改为1秒不会影响到支持时间戳选项的TCP连接。

  • 初始化RTO的缩小令握手速度加快(指检测网络拥塞的场景),会让性能提升10%到50%。

2)再次阐述RTO的计算过程:

 

公式很明显,同时考虑了均值和均值偏差。

4)必须用karn算法:不对重传的报文做RTT计算(因为分不清ack的是哪个重传包),同时采取定时器退避(就是每次重传时,RTO的值都增倍)策略。

5)RTO_MIN必须大于等于1秒,如果小于则向上取整为1秒(保守性体现)。RTO_MAX规定为60秒,其实就是2*MSL,跟TCP的TIME_WAIT状态时的等待是一个意思(RFC1122里定义为240秒,可见也是根据网络加速而调整了)。时钟粒度G,本质是中断的粒度,应小于100ms,但是当今中断粒度(1毫秒)已经很小了,所以此值没有存在必要了,一定说有就是在初始花RTO时用到,但意义已经变了。

RTO_MIN设置好为200毫秒和RTO_MAX设置为120秒两个固定值的原因,不外乎考虑到保守性和通用性(参考http://osdir.com/ml/linux.network.general/2002-12/msg00055.html)。

6)时间戳选项在RTT的选取上有很大优势:每个ack其实都可以得出一个RTT。原因就是颠覆了karn算法:能够识别出ack对应的重传包。我们以后会讲TCP为了提升性能新增的几个重要选项,主要参考RFC1323。

7)一个RTT时间内可以多次计算,但规定至少一次。理论上多了会令求得的RTO更准,但是其实也无益,因为多计算的那些RTO都是有误差的,或者说alpha和beta等参数,甚至公式都需要调整。

8)定时器的处理流程:

  • 发送数据时(包括重传期间),检查是否已经启动,若没有则启动。当该定时器监测的报文被ack,则删除定时器。

  • 如果定时器超时,说明该重传了:重传早先的还没有被ack的segment,同时进行定时器退避将RTO值增倍,最后重启该定时器。

  • 若是三次握手期间的定时器超时,则将RTO重新设置为3秒。

这个流程里需要注意的是,即使重传期间也可能得出一个新的RTT,因为可能重传的包(假定起始序号为seq)和一些新数据被ack了。也就是说,重传的包的seq与当前RTO跟踪的包的seq是两个独立的过程(我们的理解是小于等于)!另外,在多次指数回避之后,可能需要清SRTT/RTTVAR,然后应该走RTO初始化值流程,而不是第一次计算RTO的流程。

通过RFC,我们给出自己的理解是:

在可靠性要求下,网络出问题是需要重传多次的,每次的超时时间是增倍的,有上限限制。在这个大背景下,能够通过改变超时时间的下限和上限来适应对网络拥塞处理的不同需求:减小上限和下限可以适应高速网络,而通过增加上限和下限可以增强保守性。

解读完RFC,我们再看linux实现(参照最新kernel4.0),就很容易明白了:

 

#define TCP_RTO_MAX ((unsigned)(120*HZ))

#define TCP_RTO_MIN ((unsigned)(HZ/5))

#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ)) /* RFC6298 2.1 initial RTO value    */

#define TCP_TIMEOUT_FALLBACK ((unsigned)(3*HZ)) /* RFC 1122 initial RTO value, now

                  * used as a fallback RTO for the

                  * initial data transmission if no

                  * valid RTT sample has been acquired,

                  * most likely due to retrans in 3WHS.

                  */

 

我们感觉有几个linux实现与RFC违背的地方:

1)RFC规定是60秒,但是linux的RTO_MAX是120秒。

2)RFC规定的1秒,但是linux是RTO_MIN的倍数,出现200毫秒、400毫秒、800毫秒的小于1秒的RTO值。

如果这些确实不是问题的话(如是,请指正我们,谢谢),我们的理解如下:

对于linux的实现而言,缩小了RTO_MIN,是激进的表现;而扩大了RTO_MAX是更加保守的表现。综合这两点,就是即照顾了当今网络的高速传输,也照顾了传输的可靠性。

2 RTO公式

之前的RFC1122仅考虑均值权重,不是很准确。RFC2988(实际是1988 VanJacobson )开始引入均值偏差mdev(meandeviation),将RTT数值的波动也考虑进去,更加准确了。

我们重新阐释这个公式,每个变量的含义:

m:新得到的RTT

a:srtt,平滑RTT

err:m和a的差

d:mdev,平滑均值偏差

h:偏差权重,定为1/4

g:均值权重,定为1/8

rto:计算结果

原公式如下:

代码实现时为规避掉浮点数,需要将分数调整为整数运算。针对上式:令A=8a,D=4d。我们可以推导出:

此公式详细的介绍在VanJacobson 1988年发表的论文《CongestionAvoidance and Control》,该论文引出这个公式,并最终收入RFC2988。另外,论文里面的推导公式中的sa就是我们这里的A,sv就是我们这里的D。

好了,上部分就结束了,纯属我们自己的理解,如有问题,还望指点。接下来,我们结合linux实现继续理解。。。

(中)

我们上部分讲了跟RTO相关的RFC,主要是一个RTO的计算公式(RFC2988引入)和握手阶段3秒到1秒的初始化RTO的改变(RFC6298引入)。现在可以接着看linux代码的相关实现了,本文尽量将内容收敛到RTO主题。至于重传和拥塞算法等话题,我们以后再谈。

3 RTO计算的代码之实现公式

具体实现和我们之前描述的公式是一致的:均值权重是1/8,偏差权重是1/4。另外,为代码的方便需要做些调整。我们先看之前的公式:

实现中完全可以消除掉err这个变量,所以结果调整为:(记作实现公式)

这个公式就是代码中真正的实现,后文都是直接引用这个实现公式!再重复阐述一次:为了避免浮点运算做的两个调整,一是rttvar放大了4倍,即调整为实现公式中的D,二是srtt放大了8倍,即调整为实现公式中的A。

实现中的RTO_MIN设置为200毫秒,但是可以通过ip route change命令修改。很多人觉得这个值设置太大了,以为这条命令就是为了将这个值往小调整。其实不然,引入修改需求的本意不是改小,而是改大,为了适应无线传输,因为无线延时更大。再次体现设置为200毫秒的那些RFC的本意:保证通用性和保守性,考虑的是互联网,而不是以太网或局域网。

实现中增加了两个辅助变量:mdev和mdev_max。mdev为整个连接周期的平均偏差,体现RTT的波动。mdev_max则是一个RTT周期内(以snd_una>rtt_seq为间隔条件)的每次平均偏差(mdev)的最大值。在linux实现中,每个周期都会计算一次的rttvar,其实用的mdev_max的值,也就是说rttvar体现取本周期内最大的波动。可见,liunx很大程度地考虑了波动的因素。

下面是我们再从RTT周期的角度,深入理解下这两个变量:

mdev_max,只在一个RTT周期内有效,记录本周期内最大的mdev,用于本周期结束后,更新rttvar(如果大于rttvar,则更新rttvar)。从这个角度看,rttvar也是周期性质的。

新RTT周期开始时,如果上一个周期的mdev_max很小(即没有大到更新rttvar),则将rttvar(依据这个mdev_max)也做减小:rttvar -= (rttvar-mdev_max) >> 2。也就是说,不论波动大小,rttvar总需要体现上一个周期的波动!

好了,有了上面这么细致的理解,我们现在可以看懂tcp_rtt_estimator函数了,就是计算srtt(即实现公式中的A)和rttvar(即实现公式中的D)。而tcp_set_rto函数则是简单地根据srtt(即A/8)和rttvar(即D/4)去计算rto。

3.1 RTO计算的代码之第一次计算RTO

上面讲的是计算RTO顺畅跑起来之后的处理,我们再看看tcp_rtt_estimator函数对第一次计算RTO的处理流程:(假设第一次采样RTT值为m)

srtt设置为8m(srtt初始化为m,之后放大8倍,故为8m,即实现公式中的A),mdev设置为2m(mdev初始化为m/2,之后放大4倍,故为2m,即实现公式中的D),rttvar设置为max(RTO_MIN,4*mdev/4)=max(RTO_MIN,4*2m/4)=max(RTO_MIN,2m)。其实,这些最终就要让第一次计算的RTO值为3m(当然,前提是2m>RTO_MIN),也就是说第一次计算的RTO值应该是三倍的RTT采样值。另外几个初始值很简单,mdev_max设置为rttvar,就是本周期最大的mdev而已;rtt_seq设置为snd_nxt,这是在定义RTT周期。

可见,第一次计算RTO,实际上更像是一个初始化的过程,或者说是依据第一次的真实RTT采样值去初始化。但相比之下,没有RTT采样值的RTO计算才是真正的初始化,我们接着讲真正的初始化RTO。

3.2 RTO计算的代码之初始化RTO

初始化的RTO值其实就是上部分讲的3秒改为1秒了。扩展起来讲就是握手期间是1秒,但是如果握手期间出现超时重传那就将此值恢复到以前的3秒。

这涉及到一个虚假(spurious)重传的概念,就是说规定的(或者计算的)超时时间太早了,其实网络没那么快。这么理解,就是说1秒的初始化RTO下,如果出现重传,那么宣告此RTO是虚假的,此时处理措施(有很多相关算法,暂略)一般就是扩大RTO。

3.3 RTO相关的几点问题

1)一个RTT周期内取样几次

2)tcp_rtt_estimator函数中的Eifel处理的含义

3)在支持时间戳选项时RTO的计算会有何不同

4 代码实现相关的其它参考

patch 740b0f18:利用usecs_to_jiffies函数将RTO的计算提升到微妙粒度,这也是代码中相关变量名字带_us后缀的意义。

https://lwn.net/Articles/443696/:2011年依照RFC2988bis-02(即后来的RFC6298)将初始化RTO由3秒改1秒的patch。

 

(下)

这样,我们已经大致理解了RTO(重传超时时间)相关的RFC和linux代码实现。现在,我们再继续深入探讨其中的几个问题。

5 问题探讨

5.1 问题探讨之一:一个RTT周期里有多次采样么?

上文的RFC解读部分提过:一个RTT周期内可以采样多次,但至少一次。《tcpip详解卷1》(1st)中描述的是一次,可见当时的freebsd版本是按照采样一次的方式实现的。但是查看linux实现代码(kernel 4.0版本),发现是多次采样的。

我们对两种方式的理解是:

以前的实现是采用一个连接只维护一个RTT定时器(应该叫计时器),相应地,只能跟踪本连接的某一个包(的发送时间)。而现在linux的实现是跟踪每一个包(通过记住每个包的发送时间),这样就做到了一个RTT周期内多次采样了。

查看tcp_rtt_estimator函数的调用关系,有两处调用它:一是握手期间收到syn+ack时,二是数据期间收到ack时。从第一处调用可见,只要收到ack都会触发RTT采样,并不是以前的一个RTT周期只采样一次;从第二处调用可见,如上文所述:独立出握手阶段,目的是加速这个阶段(通过缩短重传超时时间来实现)。

很明显,加大采样的频率能让RTO的计算更精准!当今网络高速发展自然会让发送窗口很大(使用窗口扩大选项),也间接要求采样高频率化。

5.2 问题探讨之二:tcp_rtt_estimator函数的Eifel处理是什么意思?

我们再次考察上文里的公式:

公式本质上讲,就是在计算RTO时同时考虑历史的A(均值)和历史的D(均值偏差),并且达到预期的效果。

我们预期的效果是:当新的RTT测量值(即公式中的m)变小时,RTO也变小;当新的RTT测量值变大时,RTO也变大。

很容易怀疑这两个场景的效果:RTT变小得明显,同时D变大得不明显,是否会造成RTO变大?RTT变大得不明显,同时D变小得明显,是否会造成RTO变小?我们分享自己的理解:(A、D、RTO代表上次的结果;A’、D’、RTO’代表本次的结果)

如果RTT变小了,即err<0(即m-A/8<0)时,我们看看RTO是否变小。

tcp_rtt_estimator函数中会分析RTT变小的程度大小(通过A/8-m与D/4的比较去判断)。

如果程度太大(A/8-m> D/4,即RTT减小得明显),则做所谓Eifel处理(否则确实会出现RTO’变大的恶果)。此时有

D’ =D+((A/8-m)-D/4)/8=31D/32 +(A/8-m)/8

RTO’=A’/8+D’=(A+m-A/8)/8+31D/32+(A/8-m)/8=A/8+31D/32

而A/8+31D/32<A/8+D,也就是说RTO’<RTO,所以达到了RTO’减小的目的!我们同时也看到了,其实是通过缩小了本次均值方差(本次大得离谱,所以要缩小它的影响)的作用而达到效果的。当然,代码实现中是缩小了8倍,也可以根据影响程度选择其它倍数。

RTT变大(即m-A/8>0)时,是否也会有D小得明显导致RTO减小的问题呢?

既然要D小得明显那我们可以再追加一个条件:m-A/8<D/4,此时

D’=D+D/4-(m-A/8)

我们计算一下

RTO’=A’/8+D’=(A+m-A/8)/8+D+D/4-(m-A/8)=A/8+D+D/4-(7/8)*(m-A/8)

根据追加的条件,我们推出RTO’>RTO,所以也达到了RTO’增加的目的!可见,在这种情况下,不需要所谓Eifel处理就达到目的了,究其原因应该是选择了合适的权重(均值权重是1/8,均值偏差权重是1/4)。

从这两种情况的分析可以看出,公式其实就是将本次的数据和历史的数据在均衡融合的过程。

5.3 问题探讨之三:如果支持时间戳选项呢?

时间戳选项看上去能更简单地为采样RTT提供便捷,其实不然。有个难点,就是ack机制不是每个包都回复ack,而是累加式回复。

我们看下使用时间戳选项采样的一个具体的过程:

接收端基于一个连接维护一个TsRecent变量(下一次ack时要使用的),它是针对收到的保序的包保留的一个最新时间戳(发送端发包时打上的)。另外,它还维护一个LastAck变量(实现有中已经有类似变量了,此变量可以省略),是自己发送过的最大ack。如果接收方收到包含(只能是包含,不能是大于)LastAck的包则用此包的时间戳,去更新TsRecent变量。

这个过程对于接收端而言,就是保证ack时将其最匹配(小于ack号但最接近ack号的seq)的包的时间戳回显给发送端。

这个过程可能会有这么几个问题:

ack时可能已经缓存了之前的多个包了,所以会有接收端额外处理这些包的延时,这会让RTT测量值错误地变大。

另外,如果发送端发送的含有LastAck的包丢失了,它会重传,最终接收端会延时收到,但是接收端在ack时还是使用之前的TsRecent去回显,这同样会导致RTT测量值错误地变大(增加了发送端检测包丢失的超时时间以及重传此ack的时间)。

这个方法的好处是屏蔽了karn算法,即使是对重传的包,发送方也能采样,因为它通过ack包里的回显的时间戳能分辨出此ack对应哪个重传包。具体的时间戳TCP扩展选项(还有窗口扩大、SACK)相关话题,我们以后再讲。

6 总结

本文在尽量不涉及重传流程(我们以后再谈重传等话题)的情况下详细论述了RTO相关内容。RTO值的大小很重要,可以说影响到TCP性能。究其原因,RTO值就是发送端感知的网络速度,它基于此去判断网络是否拥塞(比如丢包)并做相关适应性处理。如果这个判断都错了,那还谈何正确处理?现实中时时刻刻变化的网络拓扑使得每一次的RTT都动态化,而RTO必须随时跟着做动态调整,这就是难点。

即使是一个简单的RTO计算,也需要改来改去,可见一个真正的产品做出来是多么地不容易!就RTO本身来说,大使用机制和计算用到的公式都很简单,但是需要经历复杂网络环境的考验和验证,要的是一个实际效果。

以上是我们对RTO这个话题的后续理解,如有错误还望指正,谢谢!

关于我们

新浪微博(@NP等不等于P

计算机学习微信公众号(jsj_xx)

原创技术文章,感悟计算机,透彻理解计算机!

posted on 2016-01-26 10:20  mylinuxer  阅读(3947)  评论(0编辑  收藏  举报