[转载] tcp数据重传时间细节探秘及数据中心优化

原文: http://weibo.com/p/1001603821691477346388

 

在数据中心网络内,机器之间数据传输的往返时间(rtt)一般在10ms以内,为此调内部服务的超时时间一般会设置成50ms、200ms、500ms等,如果在传输过程中出现丢包,这样的服务超时时间,tcp层有机会发现并重传一次数据么?如果设置成200ms以内,答案是没有机会,原因是linux系统下第一次重传时间等于传输的往返时间上至少加上200ms的预测偏差值,即如果rtt值是7ms,第一次重传超时时间至少是207ms,这样如果对某个接口的超时时间设置成200ms以内, 即便是rtt时间很小,仍然无法容忍一次丢包,因为在tcp发现丢包之前,该接口已经超时了。

 

本文针对linux系统tcp数据包第一次重传时间的计算进行探究,结果会让人大吃一惊。提出的优化方法,理论上能够降低内部服务调用时延和出错量。

 

tcp发送数据包后,会设置一个定时器,到期后如果还没有收到对方的回复(ack),就会重传数据包。从发出数据包到第一次重传之间的间隔时间称为retransmission timeout(RTO),rto由数据包的往返时间(rtt)加上rtt的预测偏差(波动值)计算出来。

 

即 rto = srtt + rttvar,其中srtt是rtt的平滑值,而rttvar是波动值,代表可能的预测偏差。

 

接下来我们做一个试验。

 

先ping一下www.weibo.com,看一下数据包的往返时间,如下:

[xiaohong@localhost ~]$ ping www.weibo.com

PING www.weibo.com (123.125.104.197) 56(84) bytes of data.

64 bytes from 123.125.104.197: icmp_seq=1 ttl=55 time=3.65 ms

64 bytes from 123.125.104.197: icmp_seq=2 ttl=55 time=3.38 ms

64 bytes from 123.125.104.197: icmp_seq=3 ttl=55 time=4.34 ms

64 bytes from 123.125.104.197: icmp_seq=4 ttl=55 time=7.82 ms

 

再看一下tcp对到www.weibo.com的rtt相关数据,下面的命令是针对centos7(如果是以下的版本,运行的命令是ip route list tab cache)如下:

[xiaohong@localhost ~]$ sudo ip tcp_metrics

123.125.104.197 age 22.255sec rtt 7375us rttvar 7250us cwnd 10

 

由上面看出,平滑后的rtt值约为7ms,rttvar约为7ms,那按理说rto值应该是14ms左右,也就是等14ms后,如果没有收到对方的响应,就会重传数据。实际的情况会是这样么?

 

在一个命令窗口里,运行下面的命令:

[xiaohong@localhost ~]$ nc www.weibo.com 80

GET / HTTP/1.1

Host: www.weibo.com

Connection: 

 

同时再开一个命令行窗口里,运行下面的命令:

[xiaohong@localhost iproute2-3.19.0]$ ss -eipn '( dport = :www )'

tcp   ESTAB      0      0              10.209.80.111:56486       123.125.104.197:80     users:(("nc",1713,3)) uid:1000 ino:14243 sk:ffff88002c992d00 <->

ts sack cubic wscale:0,7 rto:207 rtt:7.375/7.25 mss:1448 cwnd:10 send 15.7Mbps rcv_space:14600

 

从上面的结果可以看出,实际的rto值是207ms,相当于rtt值加上200ms,为什么呢?

 

下面从内核tcp源代码中分析原因。

 

设置超时时间的函数是tcp_set_rto,在net/ipv4/tcp_input.c中,如下:

 

static inline void tcp_set_rto(struct sock *sk)

{

        const struct tcp_sock *tp = tcp_sk(sk);

 

        inet_csk(sk)->icsk_rto = __tcp_set_rto(tp);

 

        tcp_bound_rto(sk);

}

 

可以看出,重传的定时值isck_rto实际上是调用 __tcp_set_rto,接着看它的源码,这个在文件include/tcp/net/tcp.h中,如下:

 

static inline u32 __tcp_set_rto(const struct tcp_sock *tp)

{        

        return (tp->srtt >> 3) + tp->rttvar;

}        

 

为了避免浮点数运算,rtt乘以8保存在socket数据结构中,从代码可以确认:

 

icsk_rto  = srtt + rttvar

 

 

而计算和影响srtt和rttvar的函数是tcp_rtt_estimator,在文件net/ipv4/tcp_input.c中,代码如下:

 

static void tcp_rtt_estimator(struct sock *sk, const __u32 mrtt)

{

        struct tcp_sock *tp = tcp_sk(sk);

        long m = mrtt; /* RTT */

 

        /*      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)

         */

        if (m == 0)

                m = 1;

        if (tp->srtt != 0) {

                m -= (tp->srtt >> 3);   /* m is now error in rtt est */

                tp->srtt += m;          /* rtt = 7/8 rtt + 1/8 new */

                if (m < 0) {

                        m = -m;         /* m is now abs(error) */

                        m -= (tp->mdev >> 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;

                } else {

                        m -= (tp->mdev >> 2);   /* similar update on mdev */

                }

                tp->mdev += m;          /* mdev = 3/4 mdev + 1/4 new */

                if (tp->mdev > tp->mdev_max) {

                        tp->mdev_max = tp->mdev;

                        if (tp->mdev_max > tp->rttvar)

                                tp->rttvar = tp->mdev_max;

                }

                if (after(tp->snd_una, tp->rtt_seq)) {

                        if (tp->mdev_max < tp->rttvar)

                                tp->rttvar -= (tp->rttvar - tp->mdev_max) >> 2;

                        tp->rtt_seq = tp->snd_nxt;

                        tp->mdev_max = tcp_rto_min(sk);

                }

        } else {

                /* no previous measure. */

                tp->srtt = m << 3;      /* take the measured time to be rtt */

                tp->mdev = m << 1;      /* make sure rto = 3*rtt */

                tp->mdev_max = tp->rttvar = max(tp->mdev, tcp_rto_min(sk));

                tp->rtt_seq = tp->snd_nxt;

        }

}

 

从上面的代码可以看出,srtt  = 7/8 old srtt + 1/8 new rtt,这个跟RFC一致,没有啥可以说的。

 

获得第一个往返时间数据时(一般是建立连接完成时,对于客户端就是发出sync请求,收到服务端的回应时,而对于服务器端就是发出syc+ack后,收到客户端的ack时)的计算分析如下:

 

        } else {

                /* no previous measure. */

                /* 以前没有rtt的数据,这是收到第一个rtt的样本数据的代码逻辑 */

 

                                      /* m是本次的rtt值,乘以8保存到 srtt中 */

                tp->srtt = m << 3;      /* take the measured time to be rtt */

 

                /* rtt的初始偏差值mdev是 2倍rtt值 */

                tp->mdev = m << 1;      /* make sure rto = 3*rtt */

 

                /* 设置rttvar和rtt偏差的最大值mdev_max这两者的初始值  */

                                       /*   2倍的rtt值,tcp_rto_min之间,那个大,就选那个 */

                tp->mdev_max = tp->rttvar = max(tp->mdev, tcp_rto_min(sk));

                tp->rtt_seq = tp->snd_nxt;

        }

 

再看tcp_rto_min的代码,在文件include/net/tcp.h中:

 

static inline u32 tcp_rto_min(struct sock *sk)

{               

        struct dst_entry *dst = __sk_dst_get(sk);

        u32 rto_min = TCP_RTO_MIN; /* 200ms */

                

        if (dst && dst_metric_locked(dst, RTAX_RTO_MIN))

                rto_min = dst_metric_rtt(dst, RTAX_RTO_MIN);

        return rto_min; 

}                               

 

结合起来看,如果第一个数据包往返时间在100ms以内,rtt预测初始的偏差值就固定为200ms,当数据包往返时间超过100ms,rtt预测偏差的初始值是2倍的rtt值,也就是说rttvar最小值是200ms。

 

接着分析计算和影响srtt和rttvar的函数是tcp_rtt_estimator的代码:

 

                if (tp->mdev > tp->mdev_max) {

                        /* 跟踪rtt的偏差,记录偏差最大值mdev_max */

                        tp->mdev_max = tp->mdev;

                        if (tp->mdev_max > tp->rttvar) /* 偏差最大值大于 rttvar时,rttvar跟着变大 */

                                tp->rttvar = tp->mdev_max;

                }

                if (after(tp->snd_una, tp->rtt_seq)) {

                        /* 偏差最大值小于 rttvar时,rttvar也会相应减少*/

                        if (tp->mdev_max < tp->rttvar)

                                tp->rttvar -= (tp->rttvar - tp->mdev_max) >> 2;

                        tp->rtt_seq = tp->snd_nxt;

 

                        /* 每个发送周期结束,重置mdev_max为tcp_rto_min */

                        tp->mdev_max = tcp_rto_min(sk);

                }

 

也就是说,rtt预测偏差值rttvar会跟着实际的rtt预测偏差值变化,如果波动变大,则跟着变大,反之,如果波动变小,也会跟着变小。但因为每个发送周期内,偏差的最大值会重置为tcp_rto_min,所以,rtt预测偏差值rttvar不会小于200ms。

 

那这200ms的限制,有啥简单的方法调整么?继续看tcp_rto_min的代码,前面也贴过,如下:

 

 

static inline u32 tcp_rto_min(struct sock *sk)

{               

        struct dst_entry *dst = __sk_dst_get(sk);

        u32 rto_min = TCP_RTO_MIN; /* 200ms */

                

        if (dst && dst_metric_locked(dst, RTAX_RTO_MIN))

                rto_min = dst_metric_rtt(dst, RTAX_RTO_MIN);

        return rto_min; 

 

从上面的代码可以看出,如果对应的目标的路由表项中设置了rto_min值,则以设置的值为准。这可以通过netlink机制来修改,具体可以通过ip route命令,增加rto_min选项来完成。

 

 

分析完源代码,接着试验一下。

 

运行下面的命令修改成20ms:

 

sudo ip route add 123.125.104.197/32 via 10.209.83.254 rto_min 20

 

看以下修改后的结果:

 

[xiaohong@localhost ~]$ ip route list

default via 10.209.83.254 dev enp0s3  proto static  metric 1024 

10.209.80.0/22 dev enp0s3  proto kernel  scope link  src 10.209.80.111 

123.125.104.197 via 10.209.83.254 dev enp0s3  rto_min lock 20ms

 

清除以下路由表的缓存,这样可以立即查看效果:

 

sudo ip tcp_metrics flush

 

再测试访问weibo.com:

 

[xiaohong@localhost ~]$ nc www.weibo.com 80

GET / 

 

在另外的终端中确认一下结果:

 

[xiaohong@localhost iproute2-3.19.0]$ ss -eipn '( dport = :www )'

tcp   ESTAB      0      0              10.209.80.111:56487       123.125.104.197:80     users:(("nc",1786,3)) uid:1000 ino:14606 sk:ffff88002c992d00 <->

ts sack cubic wscale:0,7 rto:22 rtt:2/1 mss:1448 cwnd:10 send 57.9Mbps rcv_space:14600

 

可以看出,本次的rtt值是2ms,rto为22ms,即已经生效。

 

欢迎一起讨论,拍砖也可以。呵呵。

转载于:https://www.cnblogs.com/zhengran/p/4404268.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值