Chinaunix首页 | 论坛 | 博客
  • 博客访问: 612593
  • 博文数量: 30
  • 博客积分: 125
  • 博客等级: 民兵
  • 技术积分: 1871
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-03 11:29
文章分类

全部博文(30)

文章存档

2014年(9)

2013年(21)

分类: LINUX

2014-03-15 16:30:45

作者:henrystark henrystark@126.com
Blog: http://henrystark.blog.chinaunix.net/
日期:20140315
本文可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接。如有错讹,烦请指出。


这篇文章是我在实验过程中用到的方法总结。之所以公开,是因为我的实验方法也借鉴了他人的源码。有必要把这份开源精神传递下去。但是出于保密和个人权利保护需要,并不能说明完整的方法和代码。有需要交流的同行请自行找我的联系方式。

TCP接收端流量控制

TCP发送端有拥塞控制,根据网络状况调节cwnd,算法核心在于“拥塞状态”的控制,避免网络过度负载,选择合适的发送窗口。 TCP接收端有流量控制,目的不在约束发送端的包投递速率,而在给予发送端足够大的通告窗口,同时顾及到本端的接收速率。这样做的必要原因有:(1)糊涂窗口综合症,避免有一个字节空余就通告一个字节的场景,因为这样会严重降低有效负载率。【注 1】(2)高速链路,例如带宽10Gbps【注 2】,普通PC的总线速率只有6Gbps,过快的包投递可能会淹没缓存。这时候流控的作用就得以体现。在普通网络场景中,通告窗口awnd普遍大于cwnd,这是为了把控制权尽量交给发送方,接收方不成为限制因子。
分配合适的接收窗口需要估算对端的cwnd,一般而言,接收窗口大于cwnd的两倍。

1.接收端估算cwnd和RTT的原理

cwnd(拥塞窗口)、awnd(通告窗口)都和RTT(往返延时)关联。要估算cwnd,调节适当的awnd,首先需要估算RTT。RTT的测量大致有两种方法【注 3】:

1.1 使用时间戳。

TCP header有timestamp和echo timestamp,发送端投递数据包、接收端回复ACK都需要携带这些数据。接收方在回复ACK时,会打入时间戳,由发送方回显。接收方在收到有时间戳回显的数据包以后,可以用当前系统时间减去时间戳,即可得出RTT。流程如下:

              timestamp = time_
TCP receiver -----------------------> TCP sender
              ACK packet                  |
                                          |
               echo timestamp = time_     |  
    计算RTT  <----------------------------- 
                Data packet  

    RTT = time_now - time_;  

这种测量RTT的策略相当简单,下面来分析优缺点。
优点:在平稳流量下,策略简单,RTT准确。平稳流量指的是没有丢包的情况,一个数据包1500byte,忽略网卡发送消耗的时间,RTT相当可靠。实验:我设置RTT为12ms,内核打印时间为13ms。 缺点:不支持时间戳选项时不可用。
这种方法在丢包情况下够不够可靠呢?丢包情况下,可能经过了较长的时间,数据包才发送。具体情形可以分为:
(1)ACK丢失。
(2)数据包丢失。
(3)发送方等待较长时间才发送新的数据包。
(4)最严重的,超时。
有丢包时,RTT计算过程如下:

              timestamp = time1
TCP receiver --------------------------------------------------> TCP sender
  |            ACK packet, ack_seq = 9000                               |
  |                                             Data packet, seq = 9000 |
  |                 drop by Switch<---------------------------------    |       
  |                                                     echo ts = time1 |
  | dup ACK, ack_seq = 9000 (新的数据包到达接收端,ACK由数据包驱动)      |   详见【注 4】         
  |-------------------------------------------------------------------> |
  |                                                                     |   
  |            echo timestamp = time2                                   |  
计算RTT  <--------------------------------------------------------------- 
                Data packet retran, seq = 9000  

RTT = time_now - time2;  

上面列举的四种情况,能造成RTT测量偏大的只有后两种。超时情况下,发送端没有收到ACK,这时重发数据包的时间戳会造成RTT突发增大。除了超时,还有没有发送方等待较长时间才投递数据包的情况呢?也确实有,为了提升效率,发送端通常在数据负荷不够的情况下,会等待一段时间。实际上,关于丢包和有delay情况下的RTT测量,rfc1072: TCP Extensions for Long-Delay Paths 4.2节【引用 1】 早有讨论。rfc1072主要论述了发送端测量RTT的方式,接收端使用时间戳测量时,原理和发送端是一样的。当然,任何事情都少不了例外,我在实验过程中,确实看到接收端用时间戳测量RTT有偏大的情况,而且不止一次【注 5】。
源码,位于linux-3.2.18/net/ipv4/tcp_input.c:

static inline void tcp_rcv_rtt_measure(struct tcp_sock *tp)
{
        if (tp->rcv_rtt_est.time == 0)  //第一次接收到数据,三次握手之后,才开始传送数据的阶段
            goto new_measure;
        if (before(tp->rcv_nxt, tp->rcv_rtt_est.seq))   //这里是判断数据量是否够多,如果有awnd这么多,就可以更新RTT,其中rcv_nxt是接收窗口的边界,表示期望接收的下一个数据包
            return;
        tcp_rcv_rtt_update(tp, jiffies - tp->rcv_rtt_est.time, 1);  //更新RTT,还是用jiffes

new_measure:
        tp->rcv_rtt_est.seq = tp->rcv_nxt + tp->rcv_wnd;        
        tp->rcv_rtt_est.time = tcp_time_stamp;
}   

1.2 不使用时间戳

这种方法原理也很简单,判断接收端是否已经收到了一个接收窗口的数据。把这个时间间隔作为一个RTT。然而,这种方法有明显的缺陷,就是RTT偏大,发送端cwnd通常小于接收端awnd。并且这种方法在丢包时,RTT测量也会偏大,因为丢包时,收取一个满窗的数据可能要等很久。

static inline void tcp_rcv_rtt_measure_ts(struct sock *sk,
                  const struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);
    if (tp->rx_opt.rcv_tsecr &&         //一系列选项判断,是否支持时间戳,数据包是否大于MSS
        (TCP_SKB_CB(skb)->end_seq -
         TCP_SKB_CB(skb)->seq >= inet_csk(sk)->icsk_ack.rcv_mss))
        tcp_rcv_rtt_update(tp, tcp_time_stamp - tp->rx_opt.rcv_tsecr, 0);
}  

统计RTT之后,就可以估算发送端拥塞窗口。第二种方法估算cwnd显然偏大。第一种方法比较靠谱。

2.接收缓存的动态调节

2.1 更新RTT

这些源码很容易懂,无关紧要,注意看函数上面的注释,这里写出了方法的起源:DRS【引 2】。

/* Receiver "autotuning" code.
 *
 * The algorithm for RTT estimation w/o timestamps is based on
 * Dynamic Right-Sizing (DRS) by Wu Feng and Mike Fisk of LANL.
 * <
 *
 * More detail on this code can be found at
 * <
 * though this reference is out of date.  A new paper
 * is pending.
 */
static void tcp_rcv_rtt_update(struct tcp_sock *tp, u32 sample, int win_dep)
{
    u32 new_sample = tp->rcv_rtt_est.rtt;
    long m = sample;
    //注意,Linux内核为了避免浮点运算,RTT采样都是按8倍存储的。
    //至于为什么是8倍,涉及到排队论的延时采样分析。至于为什么不用浮点数,自行google。
    //所以看到">>3" "<<3"这样的源码不要感到奇怪,只是放大和缩小而已。  
    if (m == 0)
        m = 1;

    if (new_sample != 0) {
        /* If we sample in larger samples in the non-timestamp
         * case, we could grossly overestimate the RTT especially
         * with chatty applications or bulk transfer apps which
         * are stalled on filesystem I/O.
         *
         * Also, since we are only going for a minimum in the
         * non-timestamp case, we do not smooth things out
         * else with timestamps disabled convergence takes too
         * long.
         */
        if (!win_dep) {
            m -= (new_sample >> 3); 
            new_sample += m;
        } else {
            m <<= 3;
            if (m < new_sample)
                new_sample = m;
        }
    } else {
        /* No previous measure. */
        new_sample = m << 3;
    }

    if (tp->rcv_rtt_est.rtt != new_sample)
        tp->rcv_rtt_est.rtt = new_sample;
}

2.2 缓存调节

这才是通告窗口的取值来源,缓存要适当调节,适应发送速率的需要。把缓存通告出去,就是通告窗口了。

/*
 * This function should be called every time data is copied to user space.
 * It calculates the appropriate TCP receive buffer space.
 */
void tcp_rcv_space_adjust(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    int time;
    int space;

    if (tp->rcvq_space.time == 0)
        goto new_measure;
    //只有time大于一个RTT,才调节缓存。调节周期就是一个RTT   
    time = tcp_time_stamp - tp->rcvq_space.time;
    if (time < (tp->rcv_rtt_est.rtt >> 3) || tp->rcv_rtt_est.rtt == 0)
        return;
    //接收缓存应该大于一个RTT内接收数据的两倍 
    space = 2 * (tp->copied_seq - tp->rcvq_space.seq);

    space = max(tp->rcvq_space.space, space);

    if (tp->rcvq_space.space != space) {
        int rcvmem;

        tp->rcvq_space.space = space;

        if (sysctl_tcp_moderate_rcvbuf &&
            !(sk->sk_userlocks & SOCK_RCVBUF_LOCK)) {
            int new_clamp = space;

            /* Receive space grows, normalize in order to
             * take into account packet headers and sk_buff
             * structure overhead.
             */
            space /= tp->advmss;        //这里是对space做处理,变成MSS的整数倍
            if (!space)
                space = 1;
            rcvmem = SKB_TRUESIZE(tp->advmss + MAX_TCP_HEADER); //一个SKB的size  
            while (tcp_win_from_space(rcvmem) < tp->advmss)
                rcvmem += 128;
            space *= rcvmem;
            space = min(space, sysctl_tcp_rmem[2]);
            if (space > sk->sk_rcvbuf) {
                sk->sk_rcvbuf = space;

                /* Make the window clamp follow along.  */
                tp->window_clamp = new_clamp;   //更新窗口增长上限
            }
        }
    }

new_measure:
    tp->rcvq_space.seq = tp->copied_seq;
    tp->rcvq_space.time = tcp_time_stamp;
}

注解:
【1】糊涂窗口综合症,为了避免一字节通告窗口的奇葩现象(有效负载过低),需要对TCP做出改进。
TCPSillyWindowSyndromeandChangesTotheSlidingWindow.htm
window_syndrome
【2】数据中心网络带宽极高,10Gbps已经捉襟见肘。
【3】RTT测量方法和问题。 Zhang [Zhang86], Jain [Jain86] and Karn [Karn87]。
【4】ACK由数据包驱动,数据包由ACK驱动,TCP是一个控制闭环系统。由此可推出的结论是:接收端如果没有收到三个以上的数据包,是无法触发重复ACK,知会发送端重传的。这种情况只有等到RTO才能重传。这也就是TCP窗口尾丢包问题,简称TLP。Google和Taobao hritian都对此做了改进。
【5】准确来说,流量过小的情况,用时间戳测量都是不准的,这时候应该用第二种的方法测量。我在实验中看到RTT有突发2-3倍以上的情况,说明时间戳方法确实有缺陷。

引用:
【1】
【2】Fisk M, Feng W. Dynamic right-sizing in TCP[J]. lanl. gov/la-pubs/00796247. pdf, 2001: 2.

阅读(11753) | 评论(0) | 转发(7) |
给主人留下些什么吧!~~