再谈TCP-rto时间的计算

TCP 专栏收录该内容
8 篇文章 0 订阅

背景

在做可靠传输的弱网测试时,常常会发现因为一个报文的丢失导致发送端无法发送新数据,比如窗口大小是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的更新可以不再像传统地那样倍增,而是在一定的时许后进行倍增。

-------------
探花原创

  • 0
    点赞
  • 0
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值