调整TCP缓冲区
TCP 连接是由内核维护的,内核为每个连接建立的内存缓冲区,为网络传输服务,也要充当进程与网络间的缓冲桥梁。如果连接的内存配置过小,就无法充分使用网络带宽,TCP 传输速度就会很慢;如果连接的内存配置过大,那么服务器内存会很快用尽,新连接就无法建立成功。本文就对Linux TCP缓冲区的机制及调整方法进行分析。
滑动窗口是如何影响传输速度的?
我们知道TCP提供了可靠的传输,主要的机制就是在报文发出后,必须收到接收方返回的 ACK 确认报文,如果在RTO内还没收到,就会重新发送这个报文。由此可知,TCP 报文发出去后,并不能立刻从内存中删除,因为重发时还需要用到它。报文存放在内核缓冲区中,这也是高并发下 buff/cache 内存增加的原因。
网络传输接收方的处理能力有限,把它的处理能力告诉发送方, 使其限制发送速度,这即是滑动窗口。接收方根据它的缓冲区,可以计算出后续能够接收多少字节的报文,在接收到报文内核缓冲区变小;当进程调用read将数据都读入用户空间后,内核缓冲区被清空。由此,接收窗口可能时刻变化,因而TCP头部设计了窗口字段,来起到通知发送方的作用。
TCP报文头部设计了2个字节的窗口字段,因此它最多只能表达2^16即65535字节大小的窗口,(窗口可以为 0,此时叫做窗口关闭),这在 RTT 为 10ms 的网络中也只能到达 6MB/s 的最大速度,在当今的高速网络中显然并不够用。RFC1323中定义了扩充窗口的方法,但 Linux 中打开这一功能,需要把 tcp_window_scaling 配置设为 1,此时窗口的最大值可以达到 1GB(2^30)
net.ipv4.tcp_window_scaling = 1
当然,尽管接收缓冲区配置的足够大,接收窗口无限放大,发送方也不可能无限的提升发送速度,因为网络的传输能力是有限的,当发送方依据发送窗口,发送超过网络处理能力的报文时,路由器会直接丢弃这些报文。因此,缓冲区的内存并不是越大越好。
怎样调整缓冲区去适配滑动窗口?
滑动窗口定义了飞行报文的最大字节数,当它超过带宽时延积时,就会发生丢包。而当它小于带宽时延积时,就无法让 TCP 的传输速度达到网络允许的最大值。因此滑动窗口的设计必须参考带宽时延积。
在socket网络编程中,通过设置 socket 的 SO_SNDBUF 属性,就可以设定缓冲区的大小。那是不是将缓冲区一直设置为最大就可以了?然而,不是每一个请求都能够达到最大传输速度,比如请求的体积太小时,在慢启动的影响下,未达到最大速度时请求就处理完了。再比如网络本身也会有波动,未必可以一直保持最大速度。因此,时刻让缓冲区保持最大,太过浪费内存了。
Linux 提供了的缓冲区动态调节功能可以解决上述问题。其中,可以设置缓冲区的调节范围。对于发送缓冲区,它的范围通过tcp_wmem 配置:
net.ipv4.tcp_wmem = 4096 16384 4194304
其中三个数值分别为动态范围的【下限,初始默认值,上限】,发送缓冲区自动调节的依据是待发送的数据,且发送缓冲区的调节功能是自动开启的。
而接收缓冲区可以依据空闲系统内存的数量来调节接收窗口。如果系统的空闲内存很多,就可以把缓冲区增大一些,这样传给对方的接收窗口也会变大,因而对方的发送速度就会通过增加飞行报文来提升。
发送缓冲区的调节功能是自动开启的,而接收缓冲区则需要配置 tcp_moderate_rcvbuf 为1 来开启调节功能:
net.ipv4.tcp_moderate_rcvbuf = 1
而接收缓冲区通过tcp_mem判断空闲内存的多少。
net.ipv4.tcp_mem = 88560 118080 177120
tcp_mem 的 3 个值,是 Linux 判断系统内存是否紧张的依据。当 TCP 内存小于第 1 个值时,不需要进行自动调节;在第 1 和第 2 个值之间时,内核开始调节接收缓冲区的大小;大于第 3 个值时,内核不再为 TCP 分配新内存,此时新连接是无法建立的。
在高并发服务器中,为了兼顾网速与大量的并发连接,我们应当保证缓冲区的动态调整上限达到带宽时延积,而下限保持默认的 4K 不变即可。而对于内存紧张的服务而言,可以调低默认值作为提高并发的手段。
同时,如果这是网络 IO 型服务器,那么,**调大 tcp_mem 的上限可以让 TCP 连接使用更多的系统内存,这有利于提升并发能力。**需要注意的是,tcp_wmem 和 tcp_rmem 的单位是字节,而 tcp_mem 的单位是页面大小。而且,千万不要在 socket 上直接设置SO_SNDBUF 或者 SO_RCVBUF,这样会关闭缓冲区的动态调整功能。