TCP超时与重传中一个最重要的部分是对一个给定连接的往返时间(RTT)的测量。由于网络流量的变化,这个时间会相应地发生改变,TCP需要跟踪这些变化并动态调整超时时间RTO。
RFC2988中是这样描述RTO的:
“The Transmission Control Protocol(TCP) uses a retransmission timer to ensure data
delivery in the absence of any feedback from the remote data receiver.The duration of
this timer is referred to as RTO(retransmission timeout).”
RTT(Round Trip Time)由三部分组成:链路的传播时间(propagation delay),末端系统的处理时间,路由器缓存中的排队和处理时间(queuing delay)。
其中,前两个部分的值对于一个TCP连接相对固定,路由器缓存中的排队和处理时间会随着整个网络拥塞程度的变化而变化。所以RTT的变化在一定程度上反应了网络的拥塞程度。
平均偏差
平均偏差(mean deviation),简写为mdev。
It is the mean of the distance between each value and the mean.It gives us an idea of how spread out from the center the set of values is.
Here's the formula.
通过计算平均偏差,可以知道一组数据的波动情况。
在这里,平均偏差可以用来衡量RTT的抖动情况。
RTT测量原理
RTT的测量可以采用两种方法
(1)TCP Timestamp选项
TCP时间戳选项可以用来精确的测量RTT.
RTT=当前时间- 数据包中Timestamp选项的回显时间
这个回显时间是该数据包发出去的时间,知道了数据包的接收时间(当前时间)和发送时间(回显时间),就可以轻松的得到RTT的一个测量值。
(2)重传队列中数据包的TCP控制块
在TCP重传队列中保存着发送而未被确认的数据包,数据包skb中的TCP控制块包含着一个变量,tcp_skb_cb->when,记录了该数据包的第一次发送时间。
RTT=当前时间 - when
有人可能会问:既然不用TCP Timestamp选项就能测量出RTT,为啥还要多此一举?
这是因为方法一比方法二的功能更加强大,它们是有区别的。
“TCP must use Karn's algorithm for taking RTT samples.That is,RTT samples MUST NOT be made using segments that were retransmitted(and thus for which it is ambiguious whether the reply was for the first instance of the packet or a later instance).The only case when TCP can safely take RTT samples from retransmitted segments is when the TCP timestamp option is employed, since the timestamp option removes the ambiguity regarding which instance of the data segment triggered the acknowledgement.”
对于重传的数据包的响应,方法1可以用它来采集一个新的RTT测量样本,而方法二则不能。因为TCP Timestamp选项可以区分这个响应是原始数据包还是重传数据包触发的,从而计算出准确的RTT值。
RTT测量实现
发送方每接收到一个ACK,都会调用tcp_ack()来处理。
tcp_ack()中会调用tcp_clean_rtx_queue()来删除重传队列中已经被确认的数据段。
在tcp_clean_rtx_queue()中:
如果ACK确认了重传的数据包,则seq_rtt=-1
否则,seq_rtt = now - scb->when;
然后调用tcp_ack_update_rtt(sk,flag,seq_rtt)来更新RTT和RTO.
方法一:tcp_ack_saw_tstamp()
方法二:tcp_ack_no_tstamp()
OK,到这边RTT的测量已经结束了,接下来就是RTO值得计算
RTO计算原理
涉及到的变量
srtt为经过平滑后的RTT值,它代表着当前的RTT值,每收到一个ACK更新一次。
为了避免浮点运算,它是实际RTT值的8倍。
mdev为RTT的平均偏差,用来衡量RTT的抖动,每收到一个ACK更新一次。
mdev_max为上一个RTT内的最大mdev,代表上个RTT内时延的波动情况,有效期为一个RTT.
rttvar为mdev_max的平滑值,可升可降,代表着连接的抖动情况,在连接断开前都有效。
“To compute the current RTO,a TCP sender maintains two state variables,SRTT(smoothed round-trip time) and RTTVAR(round-trip time variation).”
实际上,RTO = srtt>>3+rttvar.
rtt表示新的RTT测量值。
old_srtt表示srtt更新前的srtt>>3,即旧的srtt值。
new_srtt表示srtt更新后的srtt>>3,即新的srtt值。
old_mdev表示旧的mdev。
new_mdev表示更新后的mdev。
(1)获得第一个RTT测量值
srtt = rtt<<3
mdev=rtt<<1
mdev_max = rttvar = max(mdev,rto_min)
所以,获得第一个RTT测量值后的RTO = rtt+rttvar,如果mdev=2*rtt>rto_min,
那么RTO = 3*rtt否则 RTO=rtt+rto_min.
(2)获得第一个RTT测量值
srtt = rtt<<3
mdev = rtt<<1
mdev_max=rttvar=max(mdev,rto_min)
所以获得第一个RTT测量值后的RTO=rtt+rttvar,如果mdev = 2*rtt>rto_min,
那么RTO=3*rtt否则,RTO=rtt+rto_min。
(2)获得第n个RTT测量值(n>=2)
srtt的更新:new_srtt = 7/8 old_srtt+1/8 rtt
mdev的更新:
err=rtt-old_srtt
当RTT变小时,即err<0时
1)如果|err|>1/4 old_mdev,则new_mdev = 31/32 old_mdev + 1/8|err|
此时:old_mdev<new_mdev<3/4 old_mdev + |err|
new_mdev有稍微变大,但是增大得不多。由于RTT是变小,所以RTO也要变小,如果
new_mdev增大很多(比如:new_mdev = 3/4 old_mdev+|err|),就会导致RTO变大,不符合我们的预期
“This is similar to one of Eifel findings.Eifel blocks mdev updates when rtt decreases.
This solution is a bit different:we use finer gain for mdev in this case(alpha *beta).
Like Eifel it also prevents growth of rto,but also it limits too fast rto decreases,happening in pure Eifel.”
2)如果|err|<=1/4 old_mdev,则new_mdev=3/4 old_mdev + |err|
此时:new_mdev <old_mdev
new_mdev变小,会导致RTO变小,符合我们的预期。
当RTT变大时,即err>0时
new_mdev = 3/4 old_mdev + |err|
此时:new_mdev >old_mdev
new_mdev变大,会导致RTO变小,这也符合我们的预期。
mdev_max和rttvar的更新
在每个RTT开始时,mdev_max = rto_min
如果在此RTO内,有更大的mdev,则更新mdev_max。
如果mdev_max >rttvar,则rttvar = mdev_max
否则,本RTT结束后,rttvar -=(rttvar - mdev_max)>>2。
这样一来,就可以通过mdev_max来调节rttvar,间接的调节RTO。
RTO计算实现
不管是方法一还是方法二,最终都调用tcp_valid_rtt_means()来更新RTT和RTO.
RTO = srtt>>8+rttvar。而srtt和rttvar的更新都是在tcp_rtt_estimator()来进行。
rto_min的取值如下:
RTO的设置
函数调用
以上涉及到的函数调用关系如下:
总结
早期的RTT的测量是采用粗粒度的定时器(Coarse grained timer),这会有比较大的误差。
现在由于TCP Timestamp选项的使用,能够更精确的测量RTT,从而计算出更加准确的RTO
TCP使用可靠的传输协议,即意味着必须按序、无差错的传送数据到目的端,那么如果在传输过程中发送的包丢失了该怎么办?TCP的重传机制就是:如果发送方认为发生了丢包现象就重发这些数据包。显然,我们需要一个方法去 猜测是否发生了丢包 。最简单的想法就是,接收方每接收到一个包就向发送者返回一个ACK,表示自己已经收到了这段数据,反过来,如果发送方一段时间内没有收到ACK,就知道 很可能是数据包丢失 了,紧接着就重发该数据包,直到收到ACK为止。
为什么是 猜测 呢? 因为即使是超时了,这个数据包也可能并没有丢,它只是绕了段远程,来的很晚而已。毕竟TCP协议是位于传输层的协议,不可能明确知道数据链路层和物理层发生了什么。但是这并不妨碍我们的超时重传机制,因为接收方会自动忽略收到的重复的包。
下面我们具体讲一讲TCP的重传机制:
这种机制下,每个数据包都有相应的计时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文就会重发该数据包。
超时时间应该设置为多少
我们先来了解一下 RTT (Round-Trip Time 往返时延)
而超时时间是以 RTO(Retransmission Timeout 超时重传时间) 表示。
超时时间不宜设置的过长或过短,否则:
综上可知,RTO设置的值应该略大于RTT的值。
RTO值的计算:
https://blog.csdn.net/JXH_123/article/details/27345151
值得注意的是:每触发一次超时重传,都 会将下一次超时时间间隔设为先前值的两倍 。遇到超时说明网络环境差,不宜频繁发送。
Wireshark 抓包显示:
超时重传存在的问题是:
当一个报文段丢失时,会等待一定的超时时间后才重传,增加了端到端的时延;
当一个报文段丢失时,在其等待超时的过程中,可能会出现这种情况: 其后的报文段已经被接收端接收但却迟迟得不到确认,发送端就也以为丢失了,从而引起不必要的重传,既浪费时间也浪费资源。(例如: 数据包5丢失,数据包6、7、8、9都已到达接收方,这个时候客户端只能等服务端发送ACK,因此对于客户端来说,它完全不知道丢了几个包,可能就悲观的认为:5后面的数据包都丢了,就重传这5个数据包,这就比较浪费了)。
刚刚提到过,基于计时器的重传往往要等待很长时间,而快速重传使用了很巧妙的方法来解决这个问题。
快速重传(Fast Retransmit)机制 不以时间为驱动,而是以数据为驱动重传。
由于TCP采用的是累计确认机制,当接收端收到比期望序号大的报文段时,便会重复发送最近一次确认的报文段的确认号,即 冗余 ACK (Duplicate ACK)。
这样,如果在超时重传定时器溢出之前,接收到连续的三个重复冗余 ACK (第一个ACK是正常的,后三个是冗余的),发送端便知晓哪个报文段在传输过程中丢失了,于是重发该报文段,而不需要等待超时重传定时器溢出,大大提高了效率。
Wireshark 抓包显示:
但是,快速重传仍然没有解决第二个问题:到底该重传多少个包?
改进的方法就是 SACK (Selective Acknowledgment),简单来说就是在快速重传的基础上,返回最近收到的报文段的序列号范围,这样客户端就知道,哪些数据包已经到达服务器了。
看下例子:
存在 SACK 选项时
当500-599报文到达,接收方发送 ACK 200 ,SACK [500,600)
当600-699报文到达,接收方发送 ACK 200 ,SACK [500,700)
当700-799报文到达
当800-899报文到达
当900-999报文到达,接收方累积确认发送 ACK 200 ,SACK [500,1000)
连续收到3个重复ACK,发送方经检查发现200-499的数据丢失了,执行快速重传,待接收方接收到200-499的数据,并返回 ACK 1000时,发送方的所有数据均已确认完毕,移动滑动窗口到1000位置处。
使用 SACK可以告知发送方 收到了哪些数据,发送方收到这些消息后就会知道哪些数据丢失,然后立即重传丢失的部分。
需要注意的是: 只有收到失序的分组时才可能会发送SACK 。
SACK 包括了两个TCP选项,一个选项用于标识是否支持 SACK(SACK_Permitted),在TCP建立连接时发送;另一种选项则包含了具体的 SACK信息。
(1)SACK_Permitted 选项
该选项只允许在TCP连接建立时,有 SYN标志的包中设置,在连接建立阶段,主动发起连接的一方在它的SYN中指定选项。只有在它从另一方的SYN中收到了这个选项之后,SACK机制才会被使能。
(2)SACK 信息选项
SACK 选项参数告诉对方 已经接收到 并缓存的不连续的数据块,发送方可据此信息检查究竟是哪个块丢失,从而发送相应的数据块。
Left Edge:本区块的第一个序号。 Right Edge:本区块的最后序号的下一个序号。
[Left Edge, Right Edge)区间的ACK 序号表示本次确认收到的序号。
问题1:SACK选项最多能包含多少个需重传的块?
由于TCP首部的最大长度为 60 byte,而固定首部占用了 20 byte,对于SACK选项本身占用了2 byte,所以剩下 60-20-2=38 byte。而每个块(包括开始和结束)占用 8 byte,所以最多可标识的块数为 38/8 = 4块,所以 SACK 最多可以包括4个需重传的块。同时由于SACK有些时候会和时间戳(占10字节)一起用,因此,此种情况下最多只有3个SACK。
问题2:SACK选项的使用规则是怎么样的?
SACK 的发送方,即 报文的接收端
第一个块需要指出是哪一个到达的报文触发的 SACK
尽可能多的把所有的块填满
SACK 要报告最近接收的不连续的数据块
SACK 的接收端,即 报文的发送端:
数据没有被确认前,都会保持在滑动窗口内
每个数据包都有一个 SACKed 的标志,对于已经标示的报文,再次接收到时会忽略
如果SACK丢失,超时重传之后,重置所有数据包SACKed 标志
DSACK是在SACK的基础上做了一些 扩展 ,主要用于对收到的 重复报文 进行了处理。
它的主要作用是:告诉发送方有哪些数据被重复接收了。
DSACK同样使用了与SACK一样的报文格式,唯一区别在于: 第一个连续的block指定的是触发DSACK的重复报文的序号空间。如果第一个段的范围被ACK范围所覆盖,那么就是DSACK。或者,第一个段的范围被SACK的第二个段覆盖,那么就是DSACK。
引入DSACK的好处有:
1)可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了;
2)是不是自己的 timeout 设置太小了,导致重传;
3)网络上出现了先发的包后到的情况(又称数据包失序);
4)网络上是不是把我的数据包给复制了;
总之,DSACK的目的是帮助发送方判断,是否发生了包失序、ACK丢失、包重复或伪重传,让TCP可以更好的做网络流量控制。
超时重传机制能解决数据包丢失的问题,但是超时重传机制存在等待时间太长,浪费时间在等待上,降低了传输效率和无法知道需要重传哪些数据包的问题。 快速重传能解决超时重传的等待时间太长的问题,但是对于究竟该重传哪些包的问题仍然不能有效解决。SACK能需要重传哪些数据包的问题,它可以知道哪些包是被确认接收的,客户端能据此判断需要重传的包。DSACK则是作为SACK的一个辅助措施,可以用来判断网络究竟是出现了什么情况,据此做好网络流量控制。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)