TCP拥塞控制慢启动窗口设置===》另外一篇文章

来源:互联网 发布:贵阳市发改委数据铁笼 编辑:程序博客网 时间:2024/06/05 09:37

拥塞控制及慢启动

通塞控制:Congestion Control
简单的说,就是TCP传输过程中,为了避免一下子将网络冲爆,引入的机制。而慢启动,顾名思义,一开始慢慢传,发现没有问题,再增加传输速度。而一旦发现传输有超时,协议会认为网络拥堵,又降低传输速度。
起始的传输速度,就是由初始拥塞窗口,initial congestion window,简称initcwnd参数控制的。
alikernel 2.6.32的内核,initcwnd的初始化,在net/ipv4/tcp_input.c:

__u32 tcp_init_cwnd(struct tcp_sock *tp, struct dst_entry *dst){        __u32 cwnd = (dst ? dst_metric(dst, RTAX_INITCWND) : 0);  //可以被调整        if (!cwnd)                cwnd = TCP_INIT_CWND; //默认的值        return min_t(__u32, cwnd, tp->snd_cwnd_clamp);}

在include/net/tcp.h定义了TCP_INIT_CWND这个值为10:

/* TCP initial congestion window */#define TCP_INIT_CWND           10

这里的10代表一开始可以传输10*MSS的数据。
我们的MSS=1460,因此初始可以传输14600字节,大约15KB的数据。

设置方式

ip route命令支持调整指定路由的initcwnd:

#ip route change 10.0.0.0/8 via 10.83.251.247 dev bond0 initcwnd 30#ip route list 10.0.0.0/810.0.0.0/8 via 10.83.251.247 dev bond0  initcwnd 30

它是通过netlink的接口,调整了上述dst_entry里边的RTAX_INITCWND,覆盖了默认的值。

场景

客户端访问server端,每次需要返回的数据大约20-30KB,而默认的initcwnd,初始只能一次性传输15KB左右,这就涉及到要分两次传输,多一个round trip的时间。
用户希望能够提升访问速度,让server端能够一次将所有数据传输过来,而不是分两次。因此,就涉及到调整初始拥塞窗口的调整。

问题

发现,调整了server/clinet端的initcwnd后,传输速度并没有改善。

curl测试

通过curl测试的方式,可以看到:

curl -w %{time_starttransfer}:%{time_total}:%{size_download}"\n" 'http://url/'0.287:0.417:30601

time_starttransfer:从开始到第一个字节开始传输的时间
time_total:整个操作持续的时间
size_download:传输的字节数


这里看到total时间比开始传输的时间大不少,应该是分两次传输的。


而对于小包,结果应该是这样的:这里传输4574B<10*MSS,在慢启动初始阶段就传完了,total时间和开始传输的时间基本是一致的。

curl -w %{time_starttransfer}:%{time_total}:%{size_download}"\n" 'http://url/'0.288::0.288::4574

抓包分析

并且,通过抓包也能够证明,initcwnd依旧是10,如图:
10.137.18.42为client端
10.98.60.74为server端
server端建立好连接并收到HTTP GET请求之后,开始发送数据:
在17:49:17.22xxxxx这个时间点,连续发送了1514+4410+2491+5858=10*MSS
然后在下个时间点17:49:17.35xxxxx,单独发送了1514+9531=8*MSS
screenshot

问题分析

还是看这个抓包文件,发现,clinet和server建立好连接以后,它的Window为14720=10*MSS,因此发送端还是只能发送10*MSS的包过来,因此,这个设置在这里没有生效。
screenshot

2.6.32内核window初始化

2.6.32版本的alikernel中,将这个receive window设置成TCP_DEFAULT_INIT_RCVWND,这个值定义为10,不可更改,因此改大了initcwnd,还是不生效。
net/ipv4/tcp_output.c

void tcp_select_initial_window(int __space, __u32 mss,                   __u32 *rcv_wnd, __u32 *window_clamp,                   int wscale_ok, __u8 *rcv_wscale){    unsigned int space = (__space < 0 ? 0 : __space);........    /* Set initial window to a value enough for senders starting with     * initial congestion window of TCP_DEFAULT_INIT_RCVWND. Place     * a limit on the initial window when mss is larger than 1460.     */    if (mss > (1 << *rcv_wscale)) {        int init_cwnd = TCP_DEFAULT_INIT_RCVWND;        if (mss > 1460)            init_cwnd =            max_t(u32, (1460 * TCP_DEFAULT_INIT_RCVWND) / mss, 2);        *rcv_wnd = min(*rcv_wnd, init_cwnd * mss);    }........}

3.10内核window初始化

在3.10版本的alikernel中,receive window的初始化方式已经做了改变:

void tcp_select_initial_window(int __space, __u32 mss,                   __u32 *rcv_wnd, __u32 *window_clamp,                   int wscale_ok, __u8 *rcv_wscale,                   __u32 init_rcv_wnd){    unsigned int space = (__space < 0 ? 0 : __space);........    if (mss > (1 << *rcv_wscale)) {        if (!init_rcv_wnd) /* Use default unless specified otherwise */            init_rcv_wnd = tcp_default_init_rwnd(mss);        *rcv_wnd = min(*rcv_wnd, init_rcv_wnd * mss);    }........}

这个函数传入了一个参数,init_rcv_wnd,如果没有设置,则通过tcp_default_init_rwnd函数拿到默认值,为两倍的TCP_INIT_CWND*2=20。可以看到默认的window做了优化,有10*MSS改成了20*MSS

u32 tcp_default_init_rwnd(u32 mss){        /* Initial receive window should be twice of TCP_INIT_CWND to         * enable proper sending of new unsent data during fast recovery         * (RFC 3517, Section 4, NextSeg() rule (2)). Further place a         * limit when mss is larger than 1460.         */        u32 init_rwnd = TCP_INIT_CWND * 2;        if (mss > 1460)                init_rwnd = max((1460 * init_rwnd) / mss, 2U);        return init_rwnd;}

init_rcv_wnd的来源从调用端可以看到,是从dst的RTAX_INITRWND拿到的:

        tcp_select_initial_window(tcp_full_space(sk),                                  tp->advmss - (tp->rx_opt.ts_recent_stamp ? tp->tcp_header_len - sizeof(struct tcphdr) : 0),                                  &tp->rcv_wnd,                                  &tp->window_clamp,                                  sysctl_tcp_window_scaling,                                  &rcv_wscale,                                  dst_metric(dst, RTAX_INITRWND));



之前我们知道,这是通过ip route设置的,因此可以猜测是新版的内核中,支持通过ip route修改initrwnd。查阅ip route的man page,确实多了一个initrwnd参数的设置:
screenshot

3.10内核测试

server端initcwnd设置为30
client端内核为3.10,分两步测试:
1、不做任何修改
2、将initrwnd修改为30,ip route change 10.0.0.0/8 via 10.83.251.247 dev bond0 initrwnd 30

client端默认

抓包分析

截图如下:建立连接时,client的window为29200=20*MSS=20*1460
分两次,将大于20*MSS的数据发出去。
screenshot

curl的结果:

time_first:286 ms
time_total:414 ms
total_byte:30333 bytes

client端initrnwd改为30

抓包分析

截图如下:建立连接时,client的window已经变成43200=30*MSS=30*1460,并且在21:31:36.97xxxx时间点,一下子将所有的数据包都发出去了,总数大于20*MSS
screenshot

curl的结果:

time_first:285 ms
time_total:285 ms
total_byte:30452 bytes


从结果中,可以看到,3.10版本内核的client,可以通过设置initrwnd,真正实现提升慢启动的速度。并且,对于短连接应用,响应速度提升明显。(小于15KB的包,没有变化)

initcwnd对长连接的影响

我们知道,拥塞窗口会涨,并且涨的速度还挺快:
每当收到一个ACK,cwnd++; 线性上升
每当过了一个RTT,cwnd = cwnd*2; 指数上升
当然,有最大限制。


理论上,对于长连接,跑了一段时间后,cwnd肯定会涨到很高。但是当连接空闲一段时间后,又会重新回到慢启动过程,如下,tcp_cwnd_restart函数,重置cwnd。

static void tcp_event_data_sent(struct tcp_sock *tp,                                struct sk_buff *skb, struct sock *sk){        struct inet_connection_sock *icsk = inet_csk(sk);        const u32 now = tcp_time_stamp;        if (sysctl_tcp_slow_start_after_idle &&            (!tp->packets_out && (s32)(now - tp->lsndtime) > icsk->icsk_rto))                tcp_cwnd_restart(sk, __sk_dst_get(sk));  ......}

当tcp_slow_start_after_idle这个内核参数开启了(默认开启),并且一段时间没有包传输,则会重新进入慢启动,这段时间为 icsk->icsk_rto=inet_csk(sk)->icsk_rto,等于TCP_TIMEOUT_INIT,这个常量在include/net/tcp.h中定义为3s(2.6.32内核为3s,3.10内核为1s)

#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ)) /* RFC 1122 initial RTO value   */

因次,要让长连接不会重新进入慢启动,可以关闭tcp_slow_start_after_idle:

#sysctl -w net.ipv4.tcp_slow_start_after_idle=0