夯实基础之tcp优化传输数据性能
tcp传输数据性能提升
在前面介绍的是三次握手和四次挥手的优化策略,接下来主要介绍的是 TCP 传输数据时的优化策略。
TCP 连接是由内核维护的,内核会为每个连接建立内存缓冲区:
如果连接的内存配置过小,就无法充分使用网络带宽,TCP 传输效率就会降低;
如果连接的内存配置过大,很容易把服务器资源耗尽,这样就会导致新连接无法建立;
因此,我们必须理解 Linux 下 TCP 内存的用途,才能正确地配置内存大小
滑动窗口是如何影响传输速度的?
TCP 会保证每一个报文都能够抵达对方,它的机制是这样:报文发出去后,必须接收到对方返回的确认报文 ACK,如果迟迟未收到,就会超时重发该报文,直到收到对方的 ACK 为止。
所以,TCP 报文发出去后,并不会立马从内存中删除,因为重传时还需要用到它。
由于 TCP 是内核维护的,所以报文存放在内核缓冲区。如果连接非常多,我们可以通过 free 命令观察到 buff/cache 内存是会增大。
如果 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。这个模式就有点像我和你面对面聊天,你一句我一句,但这种方式的缺点是效率比较低的。
所以,这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低。
要解决这一问题不难,并行批量发送报文,再批量确认报文即可。
然而,这引出了另一个问题,发送方可以随心所欲的发送报文吗?当然这不现实,我们还得考虑接收方的处理能力。
当接收方硬件不如发送方,或者系统繁忙、资源紧张时,是无法瞬间处理这么多报文的。于是,这些报文只能被丢掉,使得网络效率非常低。
为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是滑动窗口的由来。
接收方根据它的缓冲区,可以计算出后续能够接收多少字节的报文,这个数字叫做接收窗口。当内核接收到报文时,必须用缓冲区存放它们,这样剩余缓冲区空间变小,接收窗口也就变小了;当进程调用 read 函数后,数据被读入了用户空间,内核缓冲区就被清空,这意味着主机可以接收更多的报文,接收窗口就会变大。
因此,接收窗口并不是恒定不变的,接收方会把当前可接收的大小放在 TCP 报文头部中的窗口字段,这样就可以起到窗口大小通知的作用。
发送方的窗口等价于接收方的窗口吗?如果不考虑拥塞控制,发送方的窗口大小「约等于」接收方的窗口大小,因为窗口通知报文在网络传输是存在时延的,所以是约等于的关系。
窗口字段只有 2 个字节,因此它最多能表达 65535 字节大小的窗口,也就是 64KB 大小。
这个窗口大小最大值,在当今高速网络下,很明显是不够用的。所以后续有了扩充窗口的方法:在 TCP 选项字段定义了窗口扩大因子,用于扩大 TCP 通告窗口,其值大小是 2^14,这样就使 TCP 的窗口大小从 16 位扩大为 30 位(2^16 * 2^ 14 = 2^30),所以此时窗口的最大值可以达到 1GB。
Linux 中打开这一功能,需要把 tcp_window_scaling 配置设为 1(默认打开):
[root@k8s-master ~]# cat /proc/sys/net/ipv4/tcp_window_scaling
1
[root@k8s-master ~]#
要使用窗口扩大选项,通讯双方必须在各自的 SYN 报文中发送这个选项:
主动建立连接的一方在 SYN 报文中发送这个选项;
而被动建立连接的一方只有在收到带窗口扩大选项的 SYN 报文之后才能发送这个选项。
这样看来,只要进程能及时地调用 read 函数读取数据,并且接收缓冲区配置得足够大,那么接收窗口就可以无限地放大,发送方也就无限地提升发送速度。
这是不可能的,因为网络的传输能力是有限的,当发送方依据发送窗口,发送超过网络处理能力的报文时,路由器会直接丢弃这些报文。因此,缓冲区的内存并不是越大越好
在 Linux 中发送缓冲区和接收缓冲都是可以用参数调节的。设置完后,Linux 会根据你设置的缓冲区进行动态调节。
调节发送缓冲区范围
先来看看发送缓冲区,它的范围通过 tcp_wmem 参数配置;
[root@k8s-master ~]# cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
[root@k8s-master ~]#
上面三个数字单位都是字节,它们分别表示:
第一个数值是动态范围的最小值,4096 byte = 4K;
第二个数值是初始默认值,16384 byte ≈ 16K;
第三个数值是动态范围的最大值,4194304 byte = 4096K(4M);
发送缓冲区是自行调节的,当发送方发送的数据被确认后,并且没有新的数据要发送,就会把发送缓冲区的内存释放掉。
调节接收缓冲区范围
而接收缓冲区的调整就比较复杂一些,先来看看设置接收缓冲区范围的 tcp_rmem 参数:
[root@k8s-master ~]# cat /proc/sys/net/ipv4/tcp_rmem
4096 87380 6291456
[root@k8s-master ~]#
上面三个数字单位都是字节,它们分别表示:
第一个数值是动态范围的最小值,表示即使在内存压力下也可以保证的最小接收缓冲区大小,4096 byte = 4K;
第二个数值是初始默认值,87380 byte ≈ 86K;
第三个数值是动态范围的最大值,6291456 byte = 6144K(6M);
接收缓冲区可以根据系统空闲内存的大小来调节接收窗口:
如果系统的空闲内存很多,就可以自动把缓冲区增大一些,这样传给对方的接收窗口也会变大,因而提升发送方发送的传输数据数量;
反之,如果系统的内存很紧张,就会减少缓冲区,这虽然会降低传输效率,可以保证更多的并发连接正常工作;
发送缓冲区的调节功能是自动开启的,而接收缓冲区则需要配置 tcp_moderate_rcvbuf 为 1 来开启调节功能:
[root@k8s-master ~]# cat /proc/sys/net/ipv4/tcp_moderate_rcvbuf
1
[root@k8s-master ~]#
调节 TCP 内存范围
接收缓冲区调节时,怎么知道当前内存是否紧张或充分呢?这是通过 tcp_mem 配置完成的:
[root@k8s-master ~]# cat /proc/sys/net/ipv4/tcp_mem
88566 118089 177132
[root@k8s-master ~]#
上面三个数字单位不是字节,而是「页面大小」,1 页表示 4KB,它们分别表示:
当 TCP 内存小于第 1 个值时,不需要进行自动调节;
在第 1 和第 2 个值之间时,内核开始调节接收缓冲区的大小;
大于第 3 个值时,内核不再为 TCP 分配新内存,此时新连接是无法建立的;
一般情况下这些值是在系统启动时根据系统内存数量计算得到的。根据当前 tcp_mem 最大内存页面数是 177120,当内存为 (177120 * 4) / 1024K ≈ 692M 时,系统将无法为新的 TCP 连接分配内存,即 TCP 连接将被拒绝。
根据实际场景调节的策略
在高并发服务器中,为了兼顾网速与大量的并发连接,我们应当保证缓冲区的动态调整的最大值达到带宽时延积,而最小值保持默认的 4K 不变即可。而对于内存紧张的服务而言,调低默认值是提高并发的有效手段。
同时,如果这是网络 IO 型服务器,那么,调大 tcp_mem 的上限可以让 TCP 连接使用更多的系统内存,这有利于提升并发能力。需要注意的是,tcp_wmem 和 tcp_rmem 的单位是字节,而 tcp_mem 的单位是页面大小。而且,千万不要在 socket 上直接设置 SO_SNDBUF 或者 SO_RCVBUF,这样会关闭缓冲区的动态调整功能