你知道如何优化TCP吗?

一、TCP三次握手优化(建立连接)

客户端优化

SYN_SENT状态的优化

客户端作为主动发起连接方,首先它将发送SYN 包,于是客户端的连接就会处于SYN_SENT状态。客户端在等待服务端回复的ACK报文,正常情况下,服务器会几毫秒内返回SYN+ACK ,但如果客户端长时间没有收到 SYN+ACK报文,则会重发SYN包,重发的次数由tcp_syn_retries参数控制,默认是5次。通常,第⼀次超时重传是在1秒后,第⼆次超时重传是在2秒,第三次超时重传是在4秒后,第四次超时重传是在8秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上⼀次的 2 倍。当第五次超时重传后,会继续等待32秒,如果服务端仍然没有回应ACK,客户端就会终止三次握手。 所以,总耗时是 1+2+4+8+16+32=63 秒,大约1分钟左右。以根据网络的稳定性和目标服务器的繁忙程度修改SYN的重传次数,调整客户端的三次握手时间上限。比如内网中通讯时,就可以适当调低重试次数,尽快把错误暴露给应用程序。

服务端优化

SYN_RCV 状态的优化

客户端接收到服务器发来的SYN+ACK报文后,就会回复ACK给服务器,同时客户端连接状态从SYN_SENT转换为ESTABLISHED,表示连接建立成功。服务器端连接成功建立的时间还要再往后,等到服务端收到客户端的 ACK后,服务端的连接状态才变为ESTABLISHED。 如果服务器没有收到 ACK,就会重发SYN+ACK保温,同时⼀直处于SYN_RCV状态。 当网络繁忙、不稳定时,报文丢失就会变严重,此时应该调大重发次数。反之则可以调小重发次数。修改重发次数的方法是,调整tcp_synack_retries参数。
tcp_synack_retries的默认重试次数是5次,与客户端重传SYN类似,它的重传会经历1、2、4、8、16秒,最后一次重传后会继续等待32秒,如果服务端仍然没有收到ACK,才会关闭连接,故共需要等待63秒。 服务器收到ACK 后连接建立成功,此时,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到accept队列,等待进程调用accept函数时把连接取出来。

全队列溢出优化

如果进程不能及时地调用accept函数,就会造成 accept 队列(也称全连接队列)溢出,最终导致建立好的TCP连接被丢弃。
丢弃连接只是Linux的默认行为,我们还可以选择向客户端发送RST复位报文,告诉客户端连接已经建立失败。 打开这一功能需要将 tcp_abort_on_overflow参数设置为1。
tcp_abort_on_overflow共有两个值分别是 0 和 1,其分别表示

  • 0 :如果accept队列满了,那么server扔掉client发过来的ack
  • 1 :如果accept队列满了,server发送一个RST包给client,表示废掉这个握⼿过程和这个连接

通常情况下,应当把tcp_abort_on_overflow设置为0,因为这样更有利于应对突发流量。

TCP Fast Open

TCP Fast Open 的工作方式
在客户端首次建立连接时的过程

  • 客户端发送SYN报文,该报文包含Fast Open选项,且该选项的Cookie为空,这表明客户端请求 Fast Open Cookie
  • 支持TCP Fast Open的服务器生成Cookie,并将其置于SYN+ACK数据包中的Fast Open 选项以发回客户端
  • 客户端收到SYN+ACK后,本地缓存Fast Open 选项中的Cookie。

第⼀次发起HTTP GET请求的时候,还是需要正常的三次握手流程
如果客户端再次向服务器建立连接时的过程

  • 客户端发送SYN报文,该报文包含数据(对于非TFO的普通TCP握手过程,SYN报文中不包含数据)以及此前记录的Cookie
  • 支持TCP Fast Open的服务器会对收到Cookie进行校验
    • 如果Cookie有效,服务器将在SYN+ACK报文中对SYN和数据进行确认,服务器随后将数据递送至相应的应用程序
    • 如果Cookie无效,服务器将丢弃SYN报文中包含的数据,且其随后发出的SYN+ACK报文将只确认SYN的对应序列号
  • 如果服务器接受了SYN报文中的数据,服务器可在握⼿完成之前发送数据,这就减少了握手带来的1个RTT的时间消耗
  • 客户端将发送ACK确认服务器发回的SYN以及数据,但如果客户端在初始的SYN报文中发送的数据没有被确认,则客户端将重新发送数据
  • 此后的 TCP 连接的数据传输过程和非TFO的正常情况⼀致。

所以,之后发起HTTP GET请求的时候,可以绕过三次握⼿,这就减少了握手带来的1个RTT的时间消耗。 开启了TFO功能,cookie的值是存放到TCP option字段里。
客户端在请求并存储了 Fast Open Cookie 之后,可以不断重复TCP Fast Open 直到服务器认为Cookie无效(通常为过期)。
在Linux 系统中,可以通过设置tcp_fastopn内核参数,来打开 Fast Open 功能

  • 0 关闭
  • 1 作为客户端使⽤ Fast Open 功能
  • 2 作为服务端使⽤ Fast Open 功能
  • 3 无论作为客户端还是服务器,都可以使用Fast Open功能

TCP Fast Open功能需要客户端和服务端同时支持,才有效果。

二、TCP四次挥手性能提升(断开连接)

客户端和服务端双方都可以主动断开连接,通常先关闭连接的一方称为主动方,后关闭连接的一方称为被动方。
这里⼀点需要注意是:主动关闭连接的,才有 TIME_WAIT状态。

关闭连接的方式

  • RST报文:如果进程异常退出了,内核就会发送RST报文来关闭,它可以不走四次挥⼿流程,是⼀个暴力关闭连接的方式。
  • FIN报文: 安全关闭连接的方式必须通过四次挥手,它由进程调用close和 shutdown函数发起FIN报文(shutdown参数须传入SHUT_WR(关闭写)或者 SHUT_RDWR(关闭读写)才会发送FIN)。

close函数与shutdown函数的区别

  • close函数意味着完全断开连接,完全断开不仅指无法传输数据,而且也不能发送数据。使用close函数关闭连接是不优雅的。
  • shutdown函数,它可以控制只关闭一个方向的连接,优雅关闭连接的方式。

主动方的优化

FIN_WAIT1状态的优化

主动方发送FIN报文后,连接就处于FIN_WAIT1状态,正常情况下,如果能及时收到被动方的ACK,则会很快变为 FIN_WAIT2状态。 但是当迟迟收不到对方返回的ACK时,连接就会⼀直处于FIN_WAIT1状态。此时,内核会定时重发FIN报文, 其中重发次数由tcp_orphan_retries参数控制默认值是 0。
实际上当为0时,特指8次。
如果FIN_WAIT1状态连接很多,我们就需要考虑降低tcp_orphan_retries的值,当重传次数超过tcp_orphan_retries时,连接就会直接关闭掉。
当进程调用了close函数关闭连接,此时连接就会是孤儿连接,因为它无法再发送和接收数据。Linux 系统为了防止孤儿连接过多,导致系统资源长时间被占用,就提供了tcp_max_orphans参数。如果孤儿连接数大于它,新增的孤儿连接将不再走四次挥手,而是直接发送RST复位报文强制关闭。

FIN_WAIT2状态的优化

当主动方收到ACK报文后,会处于FIN_WAIT2状态,就表示主动方的发送通道已经关闭,接下来将等待对对方发送FIN报文,关闭对方的发送通道。 这时,如果连接是用shutdown函数关闭的,连接可以⼀直处于FIN_WAIT2状态,因为它可能还可以发送或接收数据。但对于close函数关闭的孤儿连接,由于无法再发送和接收数据,所以这个状态不可以持续太久,而 tcp_fin_timeout控制了这个状态下连接的持续时长,默认值是60秒
它意味着对于孤儿连接(调用close关闭的连接),如果在 60 秒后还没有收到FIN报文,连接就会直接关闭。 这个60秒不是随便决定的,它与 TIME_WAIT 状态持续的时间是相同的。

TIME_WAIT状态的优化

TIME_WAIT的作用
  • 防止旧连接的数据包:TIME_WAIT 的一个作用是防止收到历史数据,从而导致数据错乱的问题。TCP就设计出了这么一个机制,经过2MSL这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包⼀定都是新建立连接所产生的。
  • 保证连接正确关闭:TIME_WAIT的另外⼀个作用是等待足够的时间以确保最后的ACK能让被动关闭方接收,从而帮助其关闭。
2MSL的原因

这与孤儿连接FIN_WAIT2状态默认保留60秒的原理是⼀样的,因为这两个状态都需要保持2MSL 时长。MSL全称是 Maximum Segment Lifetime,它定义了一个报文在网络中的最长生存时间(报文每经过⼀次路由器的转发,IP 头部的TTL字段就会减 1,减到0时报文就被丢弃,这就限制了报文的最长存活时间)。2MSL其实是相当于至少允许报文丢失⼀次。比如,若ACK在⼀个 MSL内丢失,这样被动方发的FIN会在第2个MSL内到达,TIME_WAIT状态的连接可以应对。 为什么不是4或者8MSL 的时长呢?你可以想象⼀个丢包率达到百分之⼀的糟糕网络,连续两次丢包的概率只有万分之⼀,这个概率实在是太小了,忽略它比解决它更具性价比。

TIME_WAIT过多导致的问题
  • 客户端端口限制:如果客户端TIME_WAIT过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接。
  • 服务端受系统资源限制:由于一个四元组表示TCP连接,理论上服务端可以建立很多连接,服务端确实只监听⼀个端口,但是会把连接扔给处理线程,所以理论上监听的端口可以继续监听。但是线程池处理不了那么 多⼀直不断的连接了。所以当服务端出现大量TIME_WAIT时,系统资源被占满时,会导致处理不过来新的连接。
不经历TIME_WAIT直接关闭

另外,Linux 提供了tcp_max_tw_buckets参数,当TIME_WAIT的连接数量超过该参数时,新关闭的连接就不再经历TIME_WAIT而直接关闭。
当服务器的并发连接增多时,相应地,同时处于TIME_WAIT状态的连接数量也会变多,此时就应当调大tcp_max_tw_buckets参数,减少不同连接间数据错乱的概率。tcp_max_tw_buckets也不是越⼤越好,毕竟内存和端口都是有限的。

连接复用

有⼀种方式可以在建立新连接时,复用处于TIME_WAIT状态的连接,那就是打开tcp_tw_reuse参数。但是需要注意,该参数是只用于客户端(建立连接的发起方),因为是在调用connect() 时起作用的,而对于服务端(被动连接方)是没有用的。
tcp_tw_reuse 从协议角度理解是安全可控的,可以复用处于TIME_WAIT 的端口为新的连接所用。
协议理解的安全角度

  • 只适用于连接发起方,也就是C/S模型中的客户端。
  • 对应的TIME_WAIT状态的连接创建时间超过1秒才可以被复用(这1秒主要是花费在SYN包重传)

需要打开对TCP时间戳的支持

  • 2MSL问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃
  • 可以防止序列号绕回,也是因为重复的数据包会由于时间戳过期被自然丢弃
  • 时间戳是在TCP的选项字段里定义的,开启了时间戳功能,在TCP报文传输的时候会带上发送报文的时间戳

⽼版本的Linux 还提供了tcp_tw_recycle参数

  • Linux会加快客户端和服务端TIME_WAIT状态的时间,也就是它会使得 TIME_WAIT状态会小于60 秒,很容易导致数据错乱
  • Linux会丢弃所有来自远端时间戳小于上次记录的时间戳(由同⼀个远端发送的)的任何数据包。

所以,不建议设置为1,在 Linux 4.12版本后,Linux 内核直接取消了这⼀参数,建议关闭它。

被动方的优化

当被动方收到FIN报文时,内核会自动回复ACK,同时连接处于 CLOSE_WAIT状态,顾名思义,它表示等待应用进程调用close函数关闭连接。内核没有权利替代进程去关闭连接,因为如果主动方是通过shutdown关闭连接,那么它就是想在半关闭连接上接收数据或发送数据。因此,Linux 并没有限制CLOSE_WAIT状态的持续时间。处于CLOSE_WAIT状态时,调用了close函数,内核就会发出FIN报文关闭发送通道,同时连接进入 LAST_ACK状态,等待主动方返回ACK来确认连接关闭。 如果迟迟收不到这这个ACK,内核就会重发FIN报文,重发次数仍然由tcp_orphan_retries参数控制,这与主动方重发发FIN报文的优化策略一致。 还有⼀点我们需要注意的,如果被动方迅速调用close函数,那么被动方的ACK和FIN有可能在⼀个报文中发送,这样看起来,四次挥手会变成三次挥⼿,这只是⼀种特殊情况,不用在意。

双方同时关闭连接

由于TCP是双全工的协议,所以是会出现两方同时关闭连接的现象,也就是同时发送了FIN报文。 此时,上面介绍的优化策略仍然适用。两方发送FIN 报文时,都认为自己是主动方,所以都进入了FIN_WAIT1状 态,FIN 报文的重发次数仍由tcp_orphan_retries参数控制。接下来,双方在等待ACK报⽂的过程中,都等来了FIN报文。这是⼀种新情况,所以连接会进入⼀种叫做 CLOSING的新状态,它替代了FIN_WAIT2状态。接着,双方内核回复ACK 确认对方发送通道的关闭后,进入TIME_WAIT状态,等待2MSL的时间后,连接自动关闭。
在这里插入图片描述

三、TCP传输数据的性能提升(传输过程)

内存缓冲区

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

  • 如果连接的内存配置过小,就无法充分使用网络带宽,TCP传输效率就会降低
  • 如果连接的内存配置过大,很容易把服务器资源耗尽,这样就会导致新连接无法建立

滑动窗口

TCP会保证每⼀个报文都能够抵达对方,它的机制是这样:报文发出去后,必须接收到对方返回的确认报文ACK,如果迟迟未收到,就会超时重发该报文,直到收到对方的ACK为止。 所以,TCP报文发出去后,并不会立马从内存中删除,因为重传时还需要用到它。窗口字段只有2个字节,因此它最多能表达65535字节大小的窗口,也就是64KB大小。 这个窗口大小最大值,在当今高速网络下,很明显是不够用的。所以后续有了扩充窗口的方法:在TCP 选项字段定义了窗口扩大因子,用于扩大TCP通告窗口,其值大小是2^14,这样就使TCP的窗口大小从16位扩大为30位(2^16 * 2^14 = 2^30),所以此时窗口的最大值可以达到1GB。

最大传输速度

TCP的传输速度受制于发送窗口与接收窗口,以及网络设备传输能力。窗口大小由内核缓冲区大小决定。如果缓冲区与网络传输能力匹配,那么缓冲区的利用率就达到了最大化。由于发送缓冲区大小决定了发送窗口的上限,而发送窗口又决定了已发送未确认的飞行报文的上限。因此,发送缓冲区不能超过带宽时延积。

调整缓冲区大小

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

调节发送缓冲区范围

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

  • 第⼀个数值是动态范围的最小值,4096 byte = 4K
  • 第⼆个数值是初始默认值,87380 byte ≈ 86K
  • 第三个数值是动态范围的最大值,4194304 byte = 4096K(4M)

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

调节接收缓冲区范围

接收缓冲区可以根据系统空闲内存的大小来调节接收窗口
发送缓冲区的调节功能是自动开启的,而接收缓冲区则需要配置 tcp_moderate_rcvbuf为1来开启调节功能
在高并发服务器中,为了兼顾网速与⼤量的并发连接,我们应当保证缓冲区的动态调整的最大值达到带宽时延积, 而最小值保持默认的4K不变即可。而对于内存紧张的服务而言,调低默认值是提高并发的有效手段。 同时,如果这是网络IO型服务器,那么,调大tcp_mem的上限可以让TCP连接使用更多的系统内存,这有利于提升并发能力。需要注意的是,tcp_wmem和 tcp_rmem的单位是字节,而tcp_mem 的单位是页面大小。而且, 千万不要在socket上直接设置SO_SNDBUF或者SO_RCVBUF,这样会关闭缓冲区的动态调整功能。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值