TCP TIME_WAIT状态优化
一般来讲,在高并发的场景中,出现TIME_WAIT连接是正常现象,一旦四次握手连接关闭之后,这些连接也就随之被系统回收了
但是在实际高并发场景中,很有可能会出现这样的极端情况——大量的TIME_WAIT连接
TIME_WAIT状态连接过多的危害
- TIME_WAIT 状态下,TCP连接占用的本地端口将一直无法释放
- 如果TIME_WAIT连接把所有可用端口都占完了(TCP端口数量上限是65535)而且还未被系统回收,就会出现无法向服务端创建新的socket连接的情况,此时系统几乎停转,任何链接都不能建立:
address already in use : connect
异常
相关原理
在遇到一个问题时,我们不但要看到其现象,更要看到问题产生背后的原理是什么,这样不但解决了问题,还能够拓展自己的知识面
什么是TIME_WAIT连接?
一般来讲,客户端(client)与服务端(server)之间的某个进程要进行通信时,在运输层层面来讲先要通过三次握手来建立TCP连接
TCP三次握手
- 第一次握手:客户端发送一个SYN包给服务端,然后进入到SYN_ SENT状态
- 第二次握手:处在监听状态的服务端收到客户端的SYN包后进行回应:发送一个ACK包给客户端,同时发送一个SYN包给客户端, 然后进入到SYN_ RCVD状态
- 第三次握手:客户端在收到服务端的SYN包后发送一个ACK包进行确认, 然后进入到ESTABLISHED (连接成功状态)。服务端在收到ACK包后也进入ESTABLISHED (连接成功状态)
通信结束后,需要关闭连接,这时候就要通过TCP的四次挥手来进行关闭连接了
TCP四次挥手
- 第一次挥手:客户端先发送一个FIN包给服务端,然后进入到FIN _WAIT1 (终止等待1)状态
- 第二次挥手:服务端收到FIN包之后对其进行回应:发送一个ACK包给客户端, 然后进入到close__ wait (关闭等待)状态。这时候服务端处于半关闭状态。
- 第三次挥手:同时服务端也请求关闭连接,发送一个FIN包给客户端, 然后进入LAST__ ACK (最后确认)状态
- 第四次挥手:客户端在收到服务端发送的ACK包之后进入到FIN__ WAIT2 (终止等待2)状态,对服务端发来的FIN包进行回应:发送一个ACK包给服务端, 然后进入到TIME__WAIT (时间等待)状态,等待2MSL (最长报文段寿命)后进入关闭状态,服务端在收到客户端发来的ACK包之后立即进入关闭状态
从TCP四次挥手的过程我们可以看到,主动关闭连接的一端(注意这里是说主动关闭连接的一端,即 client 和 server 都可以是主动关闭连接的一端)在收到对方的FIN包请求之后,发送ACK包进行响应,这时候会处在TIME_WAIT状态
为什么要有TIME_WAIT状态?
有很多同学可能不理解为什么会有TIME_WAIT这个状态,而且在这个状态下还要先等待2MSL(报文最大生存时间)后才真正关闭连接
首先,TIME_WAIT状态使得TCP全双工连接的终止更加可靠
我们知道,网络的本质是不可靠的,四次挥手关闭TCP连接的过程中,最后一个ACK包是由主动关闭连接一端发出的(这里我们假设是 client 进行主动关闭连接)。
而这个ACK有可能在路上丢失,使得处在LAST_ACK状态的一端(server端)接收不到,如果接收不到,server 就会超时重传 FIN 请求
所以 client 需要处在TIME_WAIT状态并等待2MSL时间来处理 server 重传的 FIN 请求,来使得 server 能够正常关闭
其次,TIME_WAIT状态的存在可以处理延迟到达的报文
网络的本质是不可靠的,也就意味着TCP报文有可能会延迟到达,TIME_WAIT状态时,两端的端口不能使用,要等到2MSL时间结束后才可以继续使用,并且在等待2MSL时间的过程中,任何迟到的报文都将被丢弃
这样就可以避免延迟到达的TCP报文被误认为是新TCP连接的数据,并且使得这些延迟报文在网络上消失
如何查看TIME_WAIT连接?
以我本地虚拟机(CentOS7)为例:
查看状态为TIME_WAIT的TCP连接
$ netstat -tan |grep TIME_WAIT
统计TCP各种状态的连接数
$ netstat -n | awk '/^tcp/ {++S[$NF]} END {for(i in S) print i, S[i]}'
如何优化
前面我们讲过,出现一定数量的TIME_WAIT连接是正常现象,但是在线上生产环境面对高并发场景时可能会出现极端的情况——大量的TIME_WAIT连接
大量的TIME_WAIT连接会占用系统本地端口,导致不能再创建新的TCP连接
那么我们要怎么进行优化呢?
大量的TIME_WAIT连接存在,其本质原因是什么?
1.大量的短连接存在
在HTTP/1.0协议中默认使用短连接。
也就是说,浏览器和服务器每进行一次HTTP操作,就会建立一次连接,任务结束后就会断开连接,而断开连接这个请求是由server去发起的,主动关闭连接请求一端才会有TIME_WAIT状态连接
2.HTTP请求头里connection值被设置为close
如果HTTP请求中,connection的值被设置成close,那就相当于告诉server:server执行完HTTP请求后去主动关闭连接
优化
客户端层面
我们可以在客户端将HTTP请求头里connection的值设置为:keep-alive。将短连接改成长连接
长连接比短连接从根本上减少了server去主动关闭连接的次数,减少了TIME_WAIT状态连接的产生
(在利用nginx做反向代理时,如果要设置成长连接,则需要设置成:1.从client到nginx的连接是长连接。2.从nginx到server的连接是长连接)
服务器层面
我们可以通过修改服务器的系统内核参数来进行优化
1.允许将TIME_WAIT状态的socket重新用于新的TCP连接
这样的好处就是如果出现大量TIME_WAIT状态的连接,也能够将这些连接占用的端口重新用于新的TCP连接
$ vim /etc/sysctl.confnet.ipv4.tcp_tw_reuse = 1 #默认为0,表示关闭
2.快速回收TIME_WAIT状态的socket
$ vim /etc/sysctl.confnet.ipv4.tcp_tw_recycle = 1#默认为0,表示关闭
3.将MSL值缩减
linux中MSL的值默认为60s,我们可以通过缩减MSL值来使得主动关闭连接一端由TIME_WAIT状态到关闭状态的时间减少
但是这样做会导致延迟报文无法清除以及主动关闭连接一端不能收到重传来的FIN请求,也会影响很多基于TCP的应用的连接复用和调优
所以在实际生产环境中,需要谨慎操作
#查看默认的MSL值
$cat /proc/sys/net/ipv4/tcp_fin_timeout
#修改
$echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout
或者
$ vim /etc/sysctl.conf
net.ipv4.tcp_fin_timeout = 30