内核中拥塞窗口初始值对http性能的影响分析 博客分类: kernel Google数据结构Blog算法Cache
程序员文章站
2024-03-22 14:08:46
...
这个是google的人提出的概念,那就是对tcp的拥塞窗口的初始值进行增大可以显著的提高http的性能,这个主要是针对tcp的slow start(我前面的blog有介绍)的.
下面是相关的paper和ppt:
paper: http://code.google.com/speed/articles/tcp_initcwnd_paper.pdf
ppt:http://www.ietf.org/proceedings/10mar/slides/iccrg-4.pdf
先来看什么叫做拥塞窗口:
拥塞窗口也就是tcp的发送端所估计的如果没有包的丢失(发生拥塞)时的窗口的大小,他主要是用于tcp的拥塞控制算法,以及发送滑动窗口。
当前内核默认实现是拥塞窗口默认为2个段(也就是两个mss的大小),也就是slow start开始的初始窗口大小只是2个段,这里google 的paper通过一些数据来正明最合适的大小为10(也就是15k),不过这里我认为这里的初始段的大小还是应该根据自己网站的平均页面大小来进行设置。
当前的的http,大部分都是短链接,因此很多情况下slow start还没有结束(进入拥塞避免状态),可是连接已经断开了,并且根据google的paper,网页的平均大小是300多k,而每次都是slow start,这个时候其实slow start会导致性能的下降,所以说这种情况下,我们其实可以加速slow start,而在内核中我们就可以通过设置initcwnd来进行控制。由于拥塞窗口的扩大,slow start将不会起作用,因为就算发送的数据包没有ack,可是由于我们的拥塞窗口本身就比较大,因此还是能够发送比较多的数据而不需要等待ack到来之后来增大拥塞窗口.
更具体的就要去看我上面给出的ppt和paper了。
接下来我们来看源码如何实现的。
首先要知道initcwnd必须通过iproute来设置,而且版本必须大于等于 2.6.34。
通过ip route的源码我们能够看到它是通过netlink 与内核进行通讯,从而达到修改这个值,而对应的数组位置就是RTAX_INITCWND。
这里ip route的源码就不详细分析了,主要我们来看内核的代码。
我们要知道dst_entry这个结构,这个结构我们不详细的分析,他有一个很重要的域我们这里会用到,就是metrics,这个域是一个数组,主要是保存了很多和对端通信时所需要的数值,比如initcwnd,比如max_mtu等等。这里我们只需要关注initcwnd.
下面就是它所保存的数值类型:
可以看到我们如果需要取得某个值的话,只需要从数组里面取得对应位置的数值就可以了。
下面就是读取方法:
拥塞窗口的初始化是在当tcp连接建立之后进行的,我们来看相关代码,这里是当状态处于TCP_SYN_RECV,然后接收到ack之后,进行初始化拥塞窗口。
相关代码在tcp_rcv_state_process中,来看代码片断:
然后来看tcp_init_metrics的代码,这里我们只需要看最重要的一部分,也就是初始化snd_cwnd.
然后就是最重要的一个函数,初始化snd_cwnd,这个函数主要就是调用dst_metric得到对应的INITCWND的值(如果dst存在),然后选择snd_cwnd_clamp与cwnd的最小值,这里snd_cwnd_clamp表示snd_cwnd的最大值,因此我们设置的值不能大于这个值。
一开始我们就知道ip route initcwnd设置的就是 RTAX_INITCWND对应的值,因此这里就改变了拥塞窗口的值。
ok,然后我们来看拥塞窗口变大后,内核发送数据段时如何被影响。
内核4层发送数据是通过tcp_write_xmit函数进行的,我们来看这个的代码片断,这个函数的详细分析我前面的blog 有分析,有需要的可以看我前面的blog。
下面就是我们最关心的代码片断。
紧接着就是tcp_cwnd_test,这个函数里面用到了几个数据包的概念,比如in flight,这些我前面的blog都有分析,可以去看我前面的blog。
它通过拥塞窗口来计算还可惜发送几个段的数据包。
通过上面的代码我们可以看到如果cwnd也就是拥塞默认窗口变大之后,则我们每次可以多发送一些数据段。
这里要注意的是就算initcwnd增大,slow start也是不可避免的,只不过现在拥塞窗口变大导致发送的时候能够多发送数据段。而也只有当发送的都ack之后拥塞窗口才会变大,而有可能等不到全部的ack,连接已经断开。
在2.6.33之后还多出来了一个RTAX_INITRWND,这个值主要是针对默认的接收窗口进行设置。
下面是相关的paper和ppt:
paper: http://code.google.com/speed/articles/tcp_initcwnd_paper.pdf
ppt:http://www.ietf.org/proceedings/10mar/slides/iccrg-4.pdf
先来看什么叫做拥塞窗口:
拥塞窗口也就是tcp的发送端所估计的如果没有包的丢失(发生拥塞)时的窗口的大小,他主要是用于tcp的拥塞控制算法,以及发送滑动窗口。
当前内核默认实现是拥塞窗口默认为2个段(也就是两个mss的大小),也就是slow start开始的初始窗口大小只是2个段,这里google 的paper通过一些数据来正明最合适的大小为10(也就是15k),不过这里我认为这里的初始段的大小还是应该根据自己网站的平均页面大小来进行设置。
当前的的http,大部分都是短链接,因此很多情况下slow start还没有结束(进入拥塞避免状态),可是连接已经断开了,并且根据google的paper,网页的平均大小是300多k,而每次都是slow start,这个时候其实slow start会导致性能的下降,所以说这种情况下,我们其实可以加速slow start,而在内核中我们就可以通过设置initcwnd来进行控制。由于拥塞窗口的扩大,slow start将不会起作用,因为就算发送的数据包没有ack,可是由于我们的拥塞窗口本身就比较大,因此还是能够发送比较多的数据而不需要等待ack到来之后来增大拥塞窗口.
更具体的就要去看我上面给出的ppt和paper了。
接下来我们来看源码如何实现的。
首先要知道initcwnd必须通过iproute来设置,而且版本必须大于等于 2.6.34。
通过ip route的源码我们能够看到它是通过netlink 与内核进行通讯,从而达到修改这个值,而对应的数组位置就是RTAX_INITCWND。
这里ip route的源码就不详细分析了,主要我们来看内核的代码。
我们要知道dst_entry这个结构,这个结构我们不详细的分析,他有一个很重要的域我们这里会用到,就是metrics,这个域是一个数组,主要是保存了很多和对端通信时所需要的数值,比如initcwnd,比如max_mtu等等。这里我们只需要关注initcwnd.
下面就是它所保存的数值类型:
enum { RTAX_UNSPEC, #define RTAX_UNSPEC RTAX_UNSPEC RTAX_LOCK, #define RTAX_LOCK RTAX_LOCK RTAX_MTU, #define RTAX_MTU RTAX_MTU RTAX_WINDOW, #define RTAX_WINDOW RTAX_WINDOW RTAX_RTT, #define RTAX_RTT RTAX_RTT RTAX_RTTVAR, #define RTAX_RTTVAR RTAX_RTTVAR RTAX_SSTHRESH, #define RTAX_SSTHRESH RTAX_SSTHRESH RTAX_CWND, #define RTAX_CWND RTAX_CWND RTAX_ADVMSS, #define RTAX_ADVMSS RTAX_ADVMSS RTAX_REORDERING, #define RTAX_REORDERING RTAX_REORDERING RTAX_HOPLIMIT, #define RTAX_HOPLIMIT RTAX_HOPLIMIT RTAX_INITCWND, #define RTAX_INITCWND RTAX_INITCWND RTAX_FEATURES, #define RTAX_FEATURES RTAX_FEATURES RTAX_RTO_MIN, #define RTAX_RTO_MIN RTAX_RTO_MIN RTAX_INITRWND, #define RTAX_INITRWND RTAX_INITRWND __RTAX_MAX };
可以看到我们如果需要取得某个值的话,只需要从数组里面取得对应位置的数值就可以了。
下面就是读取方法:
dst_entry static inline u32 dst_metric(const struct dst_entry *dst, int metric) { //根据传递进来的位置,返回对应的数值。 return dst->metrics[metric-1]; }
拥塞窗口的初始化是在当tcp连接建立之后进行的,我们来看相关代码,这里是当状态处于TCP_SYN_RECV,然后接收到ack之后,进行初始化拥塞窗口。
相关代码在tcp_rcv_state_process中,来看代码片断:
if (th->ack) { int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH) > 0; switch (sk->sk_state) { case TCP_SYN_RECV: if (acceptable) { .............................................. //初始化对应序列号 tp->snd_una = TCP_SKB_CB(skb)->ack_seq; tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale; ........................................... //这个函数里面将会初始化上面所介绍的那些类型metrics,包括initcwnd tcp_init_metrics(sk); tcp_init_congestion_control(sk); ............................................................
然后来看tcp_init_metrics的代码,这里我们只需要看最重要的一部分,也就是初始化snd_cwnd.
static void tcp_init_metrics(struct sock *sk) { ....................................... cwnd: //初始化snd_cwnd tp->snd_cwnd = tcp_init_cwnd(tp, dst); tp->snd_cwnd_stamp = tcp_time_stamp; return; .......................... }
然后就是最重要的一个函数,初始化snd_cwnd,这个函数主要就是调用dst_metric得到对应的INITCWND的值(如果dst存在),然后选择snd_cwnd_clamp与cwnd的最小值,这里snd_cwnd_clamp表示snd_cwnd的最大值,因此我们设置的值不能大于这个值。
__u32 tcp_init_cwnd(struct tcp_sock *tp, struct dst_entry *dst) { //取得对应类型的值 __u32 cwnd = (dst ? dst_metric(dst, RTAX_INITCWND) : 0); if (!cwnd) { if (tp->mss_cache > 1460) cwnd = 2; else cwnd = (tp->mss_cache > 1095) ? 3 : 4; } //返回两个之间的相对较小的值。 return min_t(__u32, cwnd, tp->snd_cwnd_clamp); }
一开始我们就知道ip route initcwnd设置的就是 RTAX_INITCWND对应的值,因此这里就改变了拥塞窗口的值。
ok,然后我们来看拥塞窗口变大后,内核发送数据段时如何被影响。
内核4层发送数据是通过tcp_write_xmit函数进行的,我们来看这个的代码片断,这个函数的详细分析我前面的blog 有分析,有需要的可以看我前面的blog。
下面就是我们最关心的代码片断。
while ((skb = tcp_send_head(sk))) { unsigned int limit; tso_segs = tcp_init_tso_segs(sk, skb, mss_now); BUG_ON(!tso_segs); //这个函数将会返回还允许发送几个数据包,也就是说这个函数就是配合慢开始的。 cwnd_quota = tcp_cwnd_test(tp, skb); //如果没有则跳出循环,也就是不再发送数据。 if (!cwnd_quota) break;
紧接着就是tcp_cwnd_test,这个函数里面用到了几个数据包的概念,比如in flight,这些我前面的blog都有分析,可以去看我前面的blog。
它通过拥塞窗口来计算还可惜发送几个段的数据包。
static inline unsigned int tcp_cwnd_test(struct tcp_sock *tp, struct sk_buff *skb) { u32 in_flight, cwnd; /* Don't be strict about the congestion window for the final FIN. */ if ((TCP_SKB_CB(skb)->flags & TCPCB_FLAG_FIN) && tcp_skb_pcount(skb) == 1) return 1; //计算发送还没有确认的数据包 in_flight = tcp_packets_in_flight(tp); //取得发送拥塞窗口 cwnd = tp->snd_cwnd; //如果发送还没确认的数据包小于拥塞窗口,则说明我们还能发送数据 if (in_flight < cwnd) //返回还能发送的数据段个数,默认的cwnd变大则将会返回更大的可以发送的数据段。 return (cwnd - in_flight); //否则返回0 return 0; }
通过上面的代码我们可以看到如果cwnd也就是拥塞默认窗口变大之后,则我们每次可以多发送一些数据段。
这里要注意的是就算initcwnd增大,slow start也是不可避免的,只不过现在拥塞窗口变大导致发送的时候能够多发送数据段。而也只有当发送的都ack之后拥塞窗口才会变大,而有可能等不到全部的ack,连接已经断开。
在2.6.33之后还多出来了一个RTAX_INITRWND,这个值主要是针对默认的接收窗口进行设置。