背景
在做可靠传输的弱网测试时,常常会发现因为一个报文的丢失导致发送端无法发送新数据,比如窗口大小是10,1-9已经被收到,但是0号报文一直丢包,而重传的间隔越来越大,导致有一段时间没有任何报文的收发,直到下次0号报文的重传。这里的原因就在于rto的时间被估值太大,导致重传间隔太久,下面一段话摘自linux内核代码,
/* The following amusing code comes from Jacobson's
* article in SIGCOMM '88. Note that rtt and mdev
* are scaled versions of rtt and mean deviation.
* This is designed to be as fast as possible
* m stands for "measurement".
*
* On a 1990 paper the rto value is changed to:
* RTO = rtt + 4 * mdev
*
* Funny. This algorithm seems to be very broken.
* These formulae increase RTO, when it should be decreased, increase
* too slowly, when it should be increased quickly, decrease too quickly
* etc. I guess in BSD RTO takes ONE value, so that it is absolutely
* does not matter how to _calculate_ it. Seems, it was trap
* that VJ failed to avoid. 8)
*/
可以看出内核本身也认为这个算法已经有些老土了,应该减小rto的时候却在增大,应该快速增加rto的时候却很慢,但人们似乎并不在乎rto的这点偏差。
计算方式
计算需要的参数,可能取自下面三个链路往返时间
- 本次收到的包中携带的ack确认的数据,都是从未被重传过的数据包,计算得到的seq_rtt_us,最小序号的,可能一次确认多个数据包
- 从未被重传过的数据包使用选择ack计算得到的sack_rtt_us,最小序号的,可能一次确认多个数据包
- 对syn报文或者数据包有确认的ack报文中的携带的回显时间戳计算得到的delta
上面三个时间的优先级为seq_rtt_us > sack_rtt_us > delta。
为什么不能使用被重传过的数据包计算得到的时间戳
假设数据包A在时刻0s被发送了,到了超时时间1s时重传了,那么该报文中记录的发送时间就被更新成了1s,但是对端只是处理较慢,是对0s发送的数据包的确认,但是本地计算时使用的是1s进行的差值计算,那么就导致rtt小了1s。而前面我们提到过,内核目前的这个算法对减小敏感,对增大不敏感,所以不能使用这个rtt。
首次计算
下面m就是上面三个rtt中的一个mrtt_us,
m = mrtt_us
srtt = m << 3
tp->mdev_us = m << 1
tp->rttvar_us = max(tp->mdev_us, tcp_rto_min_us(sk))
tp->mdev_max_us = tp->rttvar_us
tp->rtt_seq = tp->snd_nxt
tp->srtt_us = max(1U, srtt)
其中mdev_us是rtt的偏差值,初始值是两倍的测量值。
rttvar_us是平滑后的mdev_max_us,至少为200ms,因为tcp有延迟ack。
mdev_max_us是最近一次的最大mdev_us
srtt_us 是平滑之后的rtt的8倍
在首次计算中,上面的这些特性体现的都不够明显,但至少初始化值符合要求的。
非首次计算
m -= (srtt >> 3); /* m is now error in rtt est */
srtt += m; /* rtt = 7/8 rtt + 1/8 new */
srtt = srtt + m - srtt>>3
srtt是8倍的rtt值,其中新值占到1倍,旧值占7倍。
if (m < 0) {
m = -m; /* m is now abs(error) */
m -= (tp->mdev_us >> 2); /* similar update on mdev */
/* 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.
*/
if (m > 0)
m >>= 3;
}
m值的含义是刚刚计算得到的rtt值和旧rtt值之间的差距,而减少和增大所做计算不同。
- 负数要先变成正数,这就是上面说的应该减小的时候却在增大
- 两者都需要减去偏差的四分之一,正值情况没贴
- 负数的话如果经过计算还是大于0 的话需要再除以8,保证对减小不敏感
tp->mdev_us += m; /* mdev = 3/4 mdev + 1/4 new */
更新mdev_us,以正数为例的话,mdev_us = mdev_us + m - mdev_us>>2,还记得前面初始化时,偏差是rtt的两倍,那么也就是说新值占0.5倍,旧值占1.5倍。
if (tp->mdev_us > tp->mdev_max_us) {
tp->mdev_max_us = tp->mdev_us;
if (tp->mdev_max_us > tp->rttvar_us)
tp->rttvar_us = tp->mdev_max_us;
}
如果上面计算得到的偏差值大于最大偏差值,那么更新最大偏差值,如果最大偏差值大于平滑的最大偏差值,则更新平滑的最大偏差值。
if (after(tp->snd_una, tp->rtt_seq)) {
if (tp->mdev_max_us < tp->rttvar_us)
tp->rttvar_us -= (tp->rttvar_us - tp->mdev_max_us) >> 2;
tp->rtt_seq = tp->snd_nxt;
tp->mdev_max_us = tcp_rto_min_us(sk);
}
接收窗口的左边沿已经超过了上次估计rtt时的右边沿,则做以下计算:
- 如果最近的最大偏差小于平滑的偏差值,则偏差值减去两者差值的四分之一,也就是降低平滑的偏差值
- 边沿更新
- 最大偏差值更新为200ms。
算法总结
算法维护了几个比较重要的值。
- 持续更新的rtt值,旧值占7倍,新值占1倍
- 持续更新的偏差值,旧值占1.5倍,新值占0.5倍,注意这里是偏差,如果rtt稳定,那么这个值将等于0.
- 本轮测量期间出现过的最大偏差和平滑偏差,而平滑偏差在一轮计算结束后可能需要缩小。
- 在测量期间,平滑偏差在小于最大偏差值得到更新
- 在测量结束的时候,最大偏差小于平滑偏差,也就是说上一步一直都没得到执行,那么平滑偏差降低,更新方式和偏差更新方式一样
- 最大偏差的最小值是200ms,平滑偏差初始值为200ms,在大于200ms时得到更新,所以不管怎么计算,平滑偏差最小值是延迟ack的大小200ms
rto的设置
static inline u32 __tcp_set_rto(const struct tcp_sock *tp)
{
return usecs_to_jiffies((tp->srtt_us >> 3) + tp->rttvar_us);
}
持续平滑的rtt值加上偏差值,即200ms加rtt,不过rtt的估计中一般包含了200ms的延迟。
rto的更新
if (sk->sk_state == TCP_ESTABLISHED &&
(tp->thin_lto || net->ipv4.sysctl_tcp_thin_linear_timeouts) &&
tcp_stream_is_thin(tp) &&
icsk->icsk_retransmits <= TCP_THIN_LINEAR_RETRIES) {
icsk->icsk_backoff = 0;
icsk->icsk_rto = min(__tcp_set_rto(tp), TCP_RTO_MAX);
} else {
/* Use normal (exponential) backoff */
icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
}
tcp会根据系统配置,决定是否倍增rto值,不过sysctl_tcp_thin_linear_timeouts默认是0,也可以针对单个套接字进行设置。在6次重传后才开始倍增。
总结
rto时间为rtt➕偏差值,偏差值可以理解为rtt的两倍。rtt的样本有严格的条件,必须是首次发送被确认的数据,或者报文中带了回显时间戳。
rto的更新可以不再像传统地那样倍增,而是在一定的时许后进行倍增。
-------------
探花原创