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在发包之前调用了一个函数来检测是否可以发送数据包:
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在哪里赋值的呢?
{
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/index.htm
建议解决方案
最简单的方式就是把/proc/sys/net/ipv4/bictcp_moderation置0,通过模拟测试可以确认该方法可以达到和不过代理时一样快的速度。
因为在2.6版本的内核代码里面找不到该限制了,在2.6的内核的发包函数里面只有对拥塞窗口、流控窗口和nagle算法的检查,没有burst的检查,在BIC的实现里面也没有该条件,可能某种原因把它去掉了,但是没有找到其说明,又或者用其他什么算法给替换了,暂时没有找到。