连 TCP 这几个参数都不懂,回去等通知吧!(三)

03 TCP 传输数据的性能提升

在前面介绍的是三次握手和四次挥手的优化策略,接下来主要介绍的是 TCP 传输数据时的优化策略。

TCP 连接是由内核维护的,内核会为每个连接建立内存缓冲区:

  • 如果连接的内存配置过小,就无法充分使用网络带宽,TCP 传输效率就会降低;

  • 如果连接的内存配置过大,很容易把服务器资源耗尽,这样就会导致新连接无法建立;

因此,我们必须理解 Linux 下 TCP 内存的用途,才能正确地配置内存大小。

滑动窗口是如何影响传输速度的?

TCP 会保证每一个报文都能够抵达对方,它的机制是这样:报文发出去后,必须接收到对方返回的确认报文 ACK,如果迟迟未收到,就会超时重发该报文,直到收到对方的 ACK 为止。

所以,TCP 报文发出去后,并不会立马从内存中删除,因为重传时还需要用到它。

由于 TCP 是内核维护的,所以报文存放在内核缓冲区。如果连接非常多,我们可以通过 free 命令观察到 buff/cache 内存是会增大。

如果 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。这个模式就有点像我和你面对面聊天,你一句我一句,但这种方式的缺点是效率比较低的。

连 TCP 这几个参数都不懂,回去等通知吧!(三)

所以,这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低

要解决这一问题不难,并行批量发送报文,再批量确认报文即刻。

连 TCP 这几个参数都不懂,回去等通知吧!(三)

然而,这引出了另一个问题,发送方可以随心所欲的发送报文吗?当然这不现实,我们还得考虑接收方的处理能力。

当接收方硬件不如发送方,或者系统繁忙、资源紧张时,是无法瞬间处理这么多报文的。于是,这些报文只能被丢掉,使得网络效率非常低。

为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是滑动窗口的由来。

接收方根据它的缓冲区,可以计算出后续能够接收多少字节的报文,这个数字叫做接收窗口。当内核接收到报文时,必须用缓冲区存放它们,这样剩余缓冲区空间变小,接收窗口也就变小了;当进程调用 read 函数后,数据被读入了用户空间,内核缓冲区就被清空,这意味着主机可以接收更多的报文,接收窗口就会变大。

因此,接收窗口并不是恒定不变的,接收方会把当前可接收的大小放在 TCP 报文头部中的窗口字段,这样就可以起到窗口大小通知的作用。

发送方的窗口等价于接收方的窗口吗?如果不考虑拥塞控制,发送方的窗口大小「约等于」接收方的窗口大小,因为窗口通知报文在网络传输是存在时延的,所以是约等于的关系。

连 TCP 这几个参数都不懂,回去等通知吧!(三)

从上图中可以看到,窗口字段只有 2 个字节,因此它最多能表达 65535 字节大小的窗口,也就是 64KB 大小。

这个窗口大小最大值,在当今高速网络下,很明显是不够用的。所以后续有了扩充窗口的方法:在 TCP 选项字段定义了窗口扩大因子,用于扩大TCP通告窗口,使 TCP 的窗口大小从 2 个字节(16 位) 扩大为 30 位,所以此时窗口的最大值可以达到 1GB(2^30)。

Linux 中打开这一功能,需要把 tcp_window_scaling 配置设为 1(默认打开):

连 TCP 这几个参数都不懂,回去等通知吧!(三)

要使用窗口扩大选项,通讯双方必须在各自的 SYN 报文中发送这个选项:

  • 主动建立连接的一方在 SYN 报文中发送这个选项;

  • 而被动建立连接的一方只有在收到带窗口扩大选项的 SYN 报文之后才能发送这个选项。

这样看来,只要进程能及时地调用 read 函数读取数据,并且接收缓冲区配置得足够大,那么接收窗口就可以无限地放大,发送方也就无限地提升发送速度。

这是不可能的,因为网络的传输能力是有限的,当发送方依据发送窗口,发送超过网络处理能力的报文时,路由器会直接丢弃这些报文。因此,缓冲区的内存并不是越大越好。

如果确定最大传输速度?

在前面我们知道了 TCP 的传输速度,受制于发送窗口与接收窗口,以及网络设备传输能力。其中,窗口大小由内核缓冲区大小决定。如果缓冲区与网络传输能力匹配,那么缓冲区的利用率就达到了最大化。

问题来了,如何计算网络的传输能力呢?

相信大家都知道网络是有「带宽」限制的,带宽描述的是网络传输能力,它与内核缓冲区的计量单位不同:

  • 带宽是单位时间内的流量,表达是「速度」,比如常见的带宽 100 MB/s;

  • 缓冲区单位是字节,当网络速度乘以时间才能得到字节数;

这里需要说一个概念,就是带宽时延积,它决定网络中飞行报文的大小,它的计算方式:

连 TCP 这几个参数都不懂,回去等通知吧!(三)

比如最大带宽是 100 MB/s,网络时延(RTT)是 10ms 时,意味着客户端到服务端的网络一共可以存放 100MB/s * 0.01s = 1MB 的字节。

这个 1MB 是带宽和时延的乘积,所以它就叫「带宽时延积」(缩写为 BDP,Bandwidth Delay Product)。同时,这 1MB 也表示「飞行中」的 TCP 报文大小,它们就在网络线路、路由器等网络设备上。如果飞行报文超过了 1 MB,就会导致网络过载,容易丢包。

由于发送缓冲区大小决定了发送窗口的上限,而发送窗口又决定了「已发送未确认」的飞行报文的上限。因此,发送缓冲区不能超过「带宽时延积」。

发送缓冲区与带宽时延积的关系:

  • 如果发送缓冲区「超过」带宽时延积,超出的部分就没办法有效的网络传输,同时导致网络过载,容易丢包;

  • 如果发送缓冲区「小于」带宽时延积,就不能很好的发挥出网络的传输效率。

所以,发送缓冲区的大小最好是往带宽时延积靠近。

怎样调整缓冲区大小?

在 Linux 中发送缓冲区和接收缓冲都是可以用参数调节的。设置完后,Linux 会根据你设置的缓冲区进行动态调节

调节发送缓冲区范围

先来看看发送缓冲区,它的范围通过 tcp_wmem 参数配置;

在 Linux 中发送缓冲区和接收缓冲都是可以用参数调节的。设置完后,Linux 会根据你设置的缓冲区进行动态调节

连 TCP 这几个参数都不懂,回去等通知吧!(三)

上面三个数字单位都是字节,它们分别表示:

  • 第一个数值是动态范围的最小值,4096 byte = 4K;

  • 第二个数值是初始默认值,87380 byte ≈ 86K;

  • 第三个数值是动态范围的最大值,4194304 byte = 4096K(4M);

发送缓冲区是自行调节的,当发送方发送的数据被确认后,并且没有新的数据要发送,就会把发送缓冲区的内存释放掉。

调节接收缓冲区范围

而接收缓冲区的调整就比较复杂一些,先来看看设置接收缓冲区范围的 tcp_rmem 参数:

连 TCP 这几个参数都不懂,回去等通知吧!(三)

上面三个数字单位都是字节,它们分别表示:

  • 第一个数值是动态范围的最小值,表示即使在内存压力下也可以保证的最小接收缓冲区大小,4096 byte = 4K;

  • 第二个数值是初始默认值,87380 byte ≈ 86K;

  • 第三个数值是动态范围的最大值,6291456 byte = 6144K(6M);

接收缓冲区可以根据系统空闲内存的大小来调节接收窗口:

  • 如果系统的空闲内存很多,就可以自动把缓冲区增大一些,这样传给对方的接收窗口也会变大,因而提升发送方发送的传输数据数量;

  • 反正,如果系统的内存很紧张,就会减少缓冲区,这虽然会降低传输效率,可以保证更多的并发连接正常工作;

发送缓冲区的调节功能是自动开启的,而接收缓冲区则需要配置 tcp_moderate_rcvbuf 为 1 来开启调节功能

连 TCP 这几个参数都不懂,回去等通知吧!(三)

调节 TCP 内存范围

接收缓冲区调节时,怎么知道当前内存是否紧张或充分呢?这是通过 tcp_mem 配置完成的:

连 TCP 这几个参数都不懂,回去等通知吧!(三)

上面三个数字单位不是字节,而是「页面大小」,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,这样会关闭缓冲区的动态调整功能。

小结

本节针对 TCP 优化数据传输的方式,做了一些介绍。

连 TCP 这几个参数都不懂,回去等通知吧!(三)

TCP 可靠性是通过 ACK 确认报文实现的,又依赖滑动窗口提升了发送速度也兼顾了接收方的处理能力。

可是,默认的滑动窗口最大值只有 64 KB,不满足当今的高速网络的要求,要想要想提升发送速度必须提升滑动窗口的上限,在 Linux 下是通过设置 tcp_window_scaling为 1 做到的,此时最大值可高达 1GB。

滑动窗口定义了网络中飞行报文的最大字节数,当它超过带宽时延积时,网络过载,就会发生丢包。而当它小于带宽时延积时,就无法充分利用网络带宽。因此,滑动窗口的设置,必须参考带宽时延积。

内核缓冲区决定了滑动窗口的上限,缓冲区可分为:发送缓冲区 tcp_wmem 和接收缓冲区 tcp_rmem。

Linux 会对缓冲区动态调节,我们应该把缓冲区的上限设置为带宽时延积。发送缓冲区的调节功能是自动打开的,而接收缓冲区需要把 tcp_moderate_rcvbuf 设置为 1 来开启。其中,调节的依据是 TCP 内存范围 tcp_mem。

但需要注意的是,如果程序中的 socket 设置 SO_SNDBUF 和 SO_RCVBUF,则会关闭缓冲区的动态整功能,所以不建议在程序设置它俩,而是交给内核自动调整比较好。

有效配置这些参数后,既能够最大程度地保持并发性,也能让资源充裕时连接传输速度达到最大值。

摘自

小林coding 的 面试官:换人!他连 TCP 这几个参数都不懂

自我感言

认真看完的话 真的会有很多不同的收获,重新认知三次握手跟四次挥手,不在理解字面意思,而明白了为啥系统会去这么做的缘由

本作品采用《CC 协议》,转载必须注明作者和本文链接
快乐就是解决一个又一个的问题!
CrazyZard
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 1
CrazyZard

再次申明一下,就是因为太长了,所以我自己把文章分了三段,里面夹杂着我自己的笔记,所以我不打算每段都写转载自哪里,看到这的人都是坚持看完的,能发现尾部的转载,该公众号对于网络的理解很深,推荐大家有空去看下!!!

转载只是为了让大家了解更多的知识,无意抄袭

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!