关于time_wait & close_wait

/ 转载 / 没有评论 / 439浏览

关于慎用

net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_recycle = 1

摘自: http://www.kakuiwong.cn/article/190


time_wait 状态的tcp连接,是产生在主动发起关闭的一端~

1.客户端通过短连接访问服务端,Connection: close,服务端响应数据后立刻关闭会话,time_wait 产生在服务端;

2.客户端通过长连接访问服务端,Connection: keep-alive,当请求到次数上线或到了会话保持超时时间,则服务端主动关闭会话,time_wait 产生在服务端;

解决上述 time_wait 状态大量存在,导致新连接创建失败的问题,一般解决办法:

  • 客户端,HTTP 请求的头部,connection 设置为 keep-alive,保持存活一段时间:现在的浏览器,一般都这么进行了
  • 服务器端
    • 允许 time_wait 状态的 socket 被重用
    • 缩减 time_wait 时间,设置为 1 MSL(即,2 mins)
    • tomcat中,默认支持长连接,设置maxKeepAliveRequests="1" 参数为关闭长连接等; 
统计TCP状态:netstat -n | awk '/^tcp/ {++y[$NF]} END {for(w in y) print w, y[w]}'


TIME_WAIT是TCP连接关闭过程中的一个状态,具体是这么形成的:

1 主动关闭端A:发FIN,进入FIN-WAIT-1状态,并等待......
2 被动关闭端P:收到FIN后必须立即发ACK,进入CLOSE_WAIT状态,并等待......
3 主动关闭端A:收到ACK后进入FIN-WAIT-2状态,并等待......
4 被动关闭端P:发FIN,进入LAST_ACK状态,并等待......
5 主动关闭端A:收到FIN后必须立即发ACK,进入TIME_WAIT状态,等待2MSL后结束Socket
6 被动关闭端P:收到ACK后结束Socket

因此,TIME_WAIT状态是出现在主动发起连接关闭的一点,和是谁发起的连接无关,可以是client端,也可以是server端。
而从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是 2*MSL(RFC793定义了MSL为2分钟,Linux设置成了30s)

为什么需要TIME_WAIT?
主要有两个原因:
1)为了确保两端能完全关闭连接。
假设A服务器是主动关闭连接方,B服务器是被动方。如果没有TIME_WAIT状态,A服务器发出最后一个ACK就进入关闭状态,如果这个ACK对端没有收到,对端就不能完成关闭。对端没有收到ACK,会重发FIN,此时连接关闭,这个FIN也得不到ACK,而有TIME_WAIT,则会重发这个ACK,确保对端能正常关闭连接。
2)为了确保后续的连接不会收到“脏数据”
刚才提到主动端进入TIME_WAIT后,等待2MSL后CLOSE,这里的MSL是指(maximum segment lifetime,我们内核一般是30s,2MSL就是1分钟),网络上数据包最大的生命周期。这是为了使网络上由于重传出现的old duplicate segment都消失后,才能创建参数(四元组,源IP/PORT,目标IP/PORT)相同的连接,如果等待时间不够长,又创建好了一样的连接,再收到old duplicate segment,数据就错乱了。

TIME_WAIT 会导致什么问题
1) 新建连接失败
TIME_WAIT到CLOSED,需要2MSL=60s的时间。这个时间非常长。每个连接在业务结束之后,需要60s的时间才能完全释放。如果业务上采用的是短连接的方式,会导致非常多的TIME_WAIT状态的连接,会占用一些资源,主要是本地端口资源。
一台服务器的本地可用端口是有限的,也就几万个端口,由这个参数控制:
sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 61000
当服务器存在非常多的TIME_WAIT连接,将本地端口都占用了,就不能主动发起新的连接去连其他服务器了。
这里需要注意,是主动发起连接,又是主动发起关闭的一方才会遇到这个问题。
如果是server端主动关闭client端建立的连接产生了大量的TIME_WAIT连接,这是不会出现这个问题的。除非是其中涉及到的某个客户端的TIME_WAIT连接都有好几万个了。

2)TIME_WAIT条目超出限制
这个限制,是由一个内核参数控制的:
sysctl net.ipv4.tcp_max_tw_buckets
net.ipv4.tcp_max_tw_buckets = 5000
超出了这个限制会报一条INFO级别的内核日志,然后继续关闭掉连接。并没有什么特别大的影响,只是增加了刚才提到的收到脏数据的风险而已。
另外的风险就是,关闭掉TIME_WAIT连接后,刚刚发出的ACK如果对端没有收到,重发FIN包出来时,不能正确回复ACK,只是回复一个RST包,导致对端程序报错,说connection reset。
因此net.ipv4.tcp_max_tw_buckets这个参数是建议不要改小的,改小会带来风险,没有什么收益,只是表面上通过netstat看到的TIME_WAIT少了些而已,有啥用呢?
并且,建议是当遇到条目不够,增加这个值,仅仅是浪费一点点内存而已。

如何解决time_wait?
1)最佳方案是应用改造长连接,但是一般不太适用
2)修改系统回收参数
设置以下参数
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_recycle = 1
设置该参数会带来什么问题?
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_recycle = 1
如果这两个参数同时开启,会校验源ip过来的包携带的timestamp是否递增,如果不是递增的话,则会导致三次握手建联不成功,具体表现为抓包的时候看到syn发出,server端不响应syn ack
通俗一些来讲就是,一个局域网有多个客户端访问您,如果有客户端的时间比别的客户端时间慢,就会建联不成功
治标不治本的方式:
放大端口范围
sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 61000
放大time_wait的buckets
sysctl net.ipv4.tcp_max_tw_buckets
net.ipv4.tcp_max_tw_buckets = 180000

----------update 2020-02-22 关于tw_bucket之争------------
关于net.ipv4.tcp_max_tw_buckets到底要不要放大,目前云上ecs多数是设置了5000,我个人浅见觉得小了



简单来说 net.ipv4.tcp_max_tw_buckets的作用 是为了“优雅”的关闭连接

1,完整的关闭连接
2,避免有数据包重复
如果tw满了会怎样
TCP: time wait bucket table overflow
新内核
tw_bucket满了的话,会影响established状态的连接在finack的时候,直接进入closed状态
老内核
tw_bucket满了的话,会将tw_bucket里面的time_wait按照一定的规则(如LRU),将一批time_Wait直接进入closed状态 ,然后established状态发送finack后进入time_wait

tw的开销是什么?
1,特别少量的内存
2,占用本地端口
tw放大的好与坏?
1,放大的话需要更多的内存开销,但是几乎可以忽略不计
2,占用更多的本地端口,需要适当的放大本地端口范围,端口范围经过简单的测试,建议设置为tw的1.5倍
net.ipv4.ip_local_port_range
3,netstat 大量的扫描socket的时候(ss不会扫描,但是ss在slab内存特别高的时候,也有可能会引起抖动),极端情况下可能会引起性能抖动
4,tw放大,local_port_range放大,还可以配置复用以及快速回收等参数
5,使用快速回收可能会导致snat时间戳递增校验问题,不递增的话syn不响应

特殊场景的时候(本机会发起大量短链接的时候)
1, nginx结合php-fpm需要本地起端口,
2,nginx反代如(java ,容器等)
如下图所示,
tcp_tw_reuse参数需要结合net.ipv4.tcp_timestamps = 1 一起来用
即 服务器即做客户端,也做server端的时候
tcp_tw_reuse参数用来设置是否可以在新的连接中重用TIME_WAIT状态的套接字。注意,重用的是TIME_WAIT套接字占用的端口号,而不是TIME_WAIT套接字的内存等。这个参数对客户端有意义,在主动发起连接的时候会在调用的inet_hash_connect()中会检查是否可以重用TIME_WAIT状态的套接字。如果你在服务器段设置这个参数的话,则没有什么作用,因为服务器端ESTABLISHED状态的套接字和监听套接字的本地IP、端口号是相同的,没有重用的概念。但并不是说服务器端就没有TIME_WAIT状态套接字。
因此 该类场景最终建议是

net.ipv4.tcp_tw_recycle = 0 关掉快速回收
net.ipv4.tcp_tw_reuse = 1   开启tw状态的端口复用(客户端角色)
net.ipv4.tcp_timestamps = 1 复用需要timestamp校验为1 
net.ipv4.tcp_max_tw_buckets = 30000 放大bucket
net.ipv4.ip_local_port_range = 15000 65000 放大本地端口范围

内存开销测试

# ss -s
Total: 15254 (kernel 15288)
TCP:   15169 (estab 5, closed 15158, orphaned 0, synrecv 0, timewait 3/0), ports 0
Transport Total     IP        IPv6
*         15288     -         -        
RAW       0         0         0        
UDP       5         4         1        
TCP       11        11        0        
INET      16        15        1        
FRAG      0         0         0        
15000个socket消耗30多m内存
![image](https://yqfile.alicdn.com/b13fbcec765e806e839aba099a3369e0b7be7319.png)

----------万恶的分割线-----------
关于 close_wait



如上所示,CLOSE_WAIT的状态是 服务器端/客户端程序收到外部过来的FIN之后,响应了ACK包,之后就进入了 CLOSE_WAIT 状态。一般来说,如果一切正常,稍后服务器端/客户端程序 需要发出 FIN 包,进而迁移到 LAST_ACK 状态,收到对端过来的ACK后,完成TCP连接关闭的整个过程。

注:不管是服务器还是客户端,只要是被动接收第一个FIN的那一方才会进入CLOSE_WAIT状态