Linux下BICTCP实现上burst control机制分析

问题现象

在测试部发现的问题:测试SqlPlus的数据库查询速度的时候,发现经过我们连接代理后速度比不过代理时候慢一倍,在关闭我们的功能后1分钟能够完成查询,但是启用后就2分钟才能完成。

当时对SqlPlus的数据流还有TCP的抓包进行分析,该查询器查询数据的特点是客户端发出21个字节的数据包,然后服务器回应9356字节的数据,然后再由客户端发21字节的数据,再回应9356字节,如此重复直到查询完成。在没过代理时发现,9356字节的数据都是一瞬间送到客户端的,从而每次交互就是一个RTT的时间,而被代理后抓包发现这9356个字节都是分两次送出的,并不是一次性的全部发出,每次发出了6个包之后就开始等待对端的ACK,直到收到ACK后才发送剩下的字节,这样每一次交互就相当于耗费了两个RTT的时间,从而总时间多出了一倍。

摸索过程

在分析了SqlPlus的数据流后,写了一个简单的测试程序,因为SqlPlus拿来测试太麻烦了,每次都要登录等操作,极其繁琐。该测试程序工作方式:客户端发送m字节,然后服务端收到后回应n字节数据,如此反复,取m=21,n=9356用来模拟SqlPlus的数据情况,得到的现象是一样的,走代理后时间增加了一倍,后来继续加大该比例,当取到m=21,n=50000时发现过代理慢了2倍,从抓包分析是中间等了2次ACK。

那么是我们的代理程序引起的还是BICTCP的原因呢?

TCP的实现上算法有很多,但是影响发送性能,并且需要等对端ACK的只有2个值,拥塞窗口和对端的流控窗口。利用TCP提供的TCP_INFO套接字选项,把这些值全部取出来看,窗口是远超过6的,流控窗口方面,因为之前修改过TCP的缓冲区,所以这次把接收缓冲区修改为很大,但是问题依旧,利用netstat可以看到,TCP的Send-Q基本上都是几十K,说明大部分情况是有数据可以发送的,后来干脆把该测试程序直接在Linux上跑,也不经过代理,结果也发现比Windows下跑还是慢了1倍,从而可以肯定是与我们的业务程序代码是没有关系的,罪魁祸首肯定是Linux的TCP协议栈。

既然牵涉到协议栈,首先最大嫌疑的就是BICTCP,于是将/proc/sys/net/ipv4/bictcp=0,再跑,居然还是一样,看来又怀疑错对象了。

在应用层能做的事情就这么多了,还不能解决问题,那就要深入到内核里面去看代码了,是否有遗漏的东西,强调下要记得目标方向,内核里面的TCP代码相对比较复杂,机制非常多,很容易看走眼。

TCP在发包之前调用了一个函数来检测是否可以发送数据包:


代码
 1  static  __inline__  int  tcp_snd_wait( struct  tcp_opt  * tp,  struct  sk_buff  * skb,
 2                     unsigned cur_mss,  int  nonagle)
 3  {
 4       if  ((tcp_packets_in_flight(tp)  >=  tp -> snd_cwnd)  &&
 5           ! (TCP_SKB_CB(skb) -> flags  &  TCPCB_FLAG_FIN))
 6           return  WC_SNDLIM_CWND;
 7 
 8           if  (sysctl_bictcp_moderation  &&  tp -> bictcp_max_packets_in_flight){  /*  burst moderation */
 9          __u32 cap;
10           if  (tp -> ca_state  ==  TCP_CA_Recovery)
11              cap  =  tp -> bictcp_max_packets_in_flight;                         /*  in recovery  */
12           else
13              cap  =  tp -> bictcp_max_packets_in_flight  +  tcp_max_burst(tp)  +  (tp -> snd_cwnd >> 7 );     /*  in other states */
14 
15                   if  ((tcp_packets_in_flight(tp)  >=  cap)  &&
16                       ! (TCP_SKB_CB(skb) -> flags  &  TCPCB_FLAG_FIN))
17                           return  WC_SNDLIM_CWND;
18          }
19 
20       if  (after(TCP_SKB_CB(skb) -> end_seq, tp -> snd_una  +  tp -> snd_wnd))
21           return  WC_SNDLIM_RWIN;
22       if  ( ! (nonagle  ==   1   ||  tp -> urg_mode  ||
23             ! tcp_nagle_check(tp, skb, cur_mss, nonagle)))
24           return  WC_SNDLIM_SENDER;
25       return  WC_SNDLIM_NONE;
26  #if  0
27       /*  Don't be strict about the congestion window for the
28       * final FIN frame.  -DaveM
29        */
30       return  ((nonagle == 1   ||  tp -> urg_mode
31            ||   ! tcp_nagle_check(tp, skb, cur_mss, nonagle))  &&
32          ((tcp_packets_in_flight(tp)  <  tp -> snd_cwnd)  ||
33           (TCP_SKB_CB(skb) -> flags  &  TCPCB_FLAG_FIN))  &&
34           ! after(TCP_SKB_CB(skb) -> end_seq, tp -> snd_una  +  tp -> snd_wnd));
35  #endif
36  }


在外层的判断中只有该函数返回是WC_SNDLIM_NONE才可以发送数据包。该函数中4个if语句分别做了4个条件的检测:

1. 在网络中的包个数不能超过tp->snd_cwnd,但是FIN包不算在此范围内,这就是拥塞窗口的控制。

2. 在网络中包的个数不能超过一个cap值,该cap就是突发的上限值。

3. 不能超过对端的流控窗口。

4. 检测是否需要启用nagle算法进行粘包处理,紧急数据除外。

上述4个条件,第一个和第三个条件经过分析发现不是这里引发的了,第四个条件的话,我们默认是使用了NODELAY选项,即关闭了nagle算法的,所以条件也不能成立。那么只能是第二个条件了,在函数中加了一句调试语句,把4个条件的结果打印出来看下,重编内核:

    

printk(KERN_INFO  "inflight[%u],snd_cwnd[%u:%u],snd_wnd[%u],maxburst[%u]

  cap[%u],after[%d]\n",

   tcp_packets_in_flight(tp), tp->snd_cwnd, tp->snd_cwnd_cnt, tp->snd_wnd,

   tp->bictcp_max_packets_in_flight,

   tp->bictcp_max_packets_in_flight + tcp_max_burst(tp) + (tp->snd_cwnd>>7),

       after(TCP_SKB_CB(skb)->end_seq, tp->snd_una + tp->snd_wnd));


看下其输出:

<6>inflight[0],snd_cwnd[12:0],snd_wnd[65792],maxburst[0] cap[3],after[0]

<6>inflight[1],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[2],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[3],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[4],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[5],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[6],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[7],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[8],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[9],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[0],snd_cwnd[12:0],snd_wnd[65792],maxburst[0] cap[3],after[0]

<6>inflight[1],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[2],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

<6>inflight[3],snd_cwnd[12:0],snd_wnd[65792],maxburst[3] cap[6],after[0]

从这里的结果可以看到,果然是被cap这个值限制了数据的发送,从而必须得等到ACK。

      再看一下在m=21,n=50000时候的输出:

<6>inflight[0],snd_cwnd[21:0],snd_wnd[99968],maxburst[0] cap[3],after[0]

<6>inflight[1],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[2],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[3],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[4],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[5],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[6],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[7],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[8],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[9],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[10],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[11],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[12],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[13],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[14],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[15],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[16],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[17],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[18],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[19],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[20],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[20],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[20],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[20],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[20],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[20],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[20],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[20],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[20],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[20],snd_cwnd[21:0],snd_wnd[99968],maxburst[3] cap[6],after[0]

<6>inflight[10],snd_cwnd[21:0],snd_wnd[99968],maxburst[0] cap[3],after[0]

<6>inflight[11],snd_cwnd[21:0],snd_wnd[99968],maxburst[13] cap[16],after[0]

<6>inflight[12],snd_cwnd[21:0],snd_wnd[99968],maxburst[13] cap[16],after[0]

<6>inflight[13],snd_cwnd[21:0],snd_wnd[99968],maxburst[13] cap[16],after[0]

至此问题原因已经找到。

Burst control算法分析

对比下BICTCP的补丁代码,第二个if语句的代码是补丁中加进去的,burst control的目地就是防止TCP一次性发出太多的包,导致网络上突发性流量很大,可以看到cap值是慢慢往上增长的,也就是说慢慢的就可以越发越多了,但是tp->bictcp_max_packets_in_flight在哪里赋值的呢?


代码
int  tcp_rcv_established( struct  sock  * sk,  struct  sk_buff  * skb,   struct  tcphdr  * th, unsigned len)
{
    
struct  tcp_opt  * tp  =   & (sk -> tp_pinfo.af_tcp);

    tp
-> bictcp_max_packets_in_flight  =   0 ;     /*  reset burst moderation count   */
        ……
}

int  tcp_transmit_skb( struct  sock  * sk,  struct  sk_buff  * skb)
{
    ……
    
if  (tp -> bictcp_max_packets_in_flight  ==   0 )
        tp
-> bictcp_max_packets_in_flight  =   tcp_packets_in_flight(tp) + tcp_max_burst(tp);
     ……
}


也就是说在establish状态每收到任何一个包,bictcp_max_packets_in_flight就被置0了,然后每次发送一个包的时候再重新赋值为“in_flight”的数据包个数加上突发个数,其中tcp_max_burst(tp)是等于3的,那么在交互的过程当中,一旦收到对端的全部确认后,显然bictcp_max_packets_in_flight就为0了,那么下次再发送数据包的时候,bictcp_max_packets_in_flight就等于3了,当数据越发越多的时候,那么"in_flight"的数据也变大,所以bictcp_max_packets_in_flight也随之增大,但是当一次交互完成后,"in_flight"就为0了,也就相当于每一次交互都需要重新启动这个过程,因而就导致了之前看到的交互都变慢了的现象。

该控制机制的好处在于可以防止突然的大量数据:如果有一个包乱序或者丢失,后面接着就来了一大把数据,从而造成这个包一旦到达后,可以连续确认非常多的数据,如果没有此机制的话,那么便可以一次性的发出一大片数据,而使用了该方式后,则一次能发送的数据少的多,可以很大程度上的减少超时和包丢失率。

关于burst control的讨论可以在此看到:http://oss.sgi.com/archives/netdev/2004-07/msg00988.html,同时有数据分析,对比了burst control机制的影响:

http://www4.ncsu.edu/~rhee/export/bitcp/tiny_release/experiments/BIC-600-75-7500-1-0-0-noburst/index.htm

http://www4.ncsu.edu/~rhee/export/bitcp/tiny_release/experiments/BIC-600-75-7500-1-0-0/index.htm

建议解决方案

    最简单的方式就是把/proc/sys/net/ipv4/bictcp_moderation置0,通过模拟测试可以确认该方法可以达到和不过代理时一样快的速度。

    因为在2.6版本的内核代码里面找不到该限制了,在2.6的内核的发包函数里面只有对拥塞窗口、流控窗口和nagle算法的检查,没有burst的检查,在BIC的实现里面也没有该条件,可能某种原因把它去掉了,但是没有找到其说明,又或者用其他什么算法给替换了,暂时没有找到。

你可能感兴趣的:(linux)