# http - TIME_WAIT,CLOSE_WAIT异常

# 遇到的问题

request.js 在启用 proxy 来爬取数据的时候,进程的 active handle 一直在增加。

# 排查问题

  1. 使用 netstat 命令查看当前 tcp 链接情况
netstat -na | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

#LAST_ACK 4
#LISTEN 16
#CLOSE_WAIT 92 // 很多!!!
#ESTABLISHED 18
#FIN_WAIT1 3
#FIN_WAIT2 3
#TIME_WAIT 98 // 很多!!!
#SYN_SENT 14

1
2
3
4
5
6
7
8
9
10
11

常用的三个状态是:ESTABLISHED 表示正在通信,TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关闭。

CLOSED 表示socket连接没被使用。
LISTENING 表示正在监听进入的连接。
SYN_SENT 表示正在试着建立连接。
SYN_RECEIVED 进行连接初始同步。
ESTABLISHED 表示连接已被建立。
CLOSE_WAIT 表示远程计算机关闭连接,正在等待socket连接的关闭。
FIN_WAIT_1 表示socket连接关闭,正在关闭连接。
CLOSING 先关闭本地socket连接,然后关闭远程socket连接,最后等待确认信息。
LAST_ACK 远程计算机关闭后,等待确认信号。
FIN_WAIT_2 socket连接关闭后,等待来自远程计算机的关闭信号。
TIME_WAIT 连接关闭后,等待远程计算机关闭重发。

可以看到,有很多的连接是 TIME_WAITCLOSE_WAIT,并且存在很久,导致 pm2 看到内存占用越来越大,active handle 也越来越多。

# 解决流程

1. TIME_WAIT

这种情况比较常见,一些爬虫服务器或者WEB服务器(如果网管在安装的时候没有做内核参数优化的话)上经常会遇到这个问题。

TIME_WAIT是主动关闭连接的一方保持的状态,对于爬虫服务器来说他本身就是“客户端”,在完成一个爬取任务之后,他就会发起主动关闭连接,从而进入TIME_WAIT的状态,然后在保持这个状态2MSL(max segment lifetime)时间之后,彻底关闭回收资源。为什么要这么做?明明就已经主动关闭连接了为啥还要保持资源一段时间呢?这个是TCP/IP的设计者规定的,主要出于以下两个方面的考虑:

a. 防止上一次连接中的包,迷路后重新出现,影响新连接(经过2MSL,上一次连接中所有的重复包都会消失) b. 可靠的关闭TCP连接。在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。另外这么设计TIME_WAIT 会定时的回收资源,并不会占用很大资源的,除非短时间内接受大量请求或者受到攻击。

解决思路很简单,就是让服务器能够快速回收和重用那些TIME_WAIT的资源。

修改 /etc/sysctl.conf 文件配置,没有这个文件就新建一个:

net.ipv4.tcp_keepalive_time = 200
net.ipv4.tcp_keepalive_probes = 3
net.ipv4.tcp_keepalive_intvl = 15
1
2
3

修改之后执行 /sbin/sysctl -p 使参数生效。

2. CLOSE_WAIT

TIME_WAIT状态可以通过优化服务器参数得到解决,因为发生TIME_WAIT的情况是服务器自己可控的,要么就是对方连接的异常,要么就是自己没有迅速回收资源,总之不是由于自己程序错误导致的。

但是CLOSE_WAIT就不一样了,从上面的图可以看出来,如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后服务器程序自己没有进一步发出ack信号。换句话说,就是在对方连接关闭之后,程序里没有检测到,或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直被程序占着。

所以,最后检查代码,是代码层面的问题

考虑到本次问题一个很明晰的特点就是,使用了代理服务器去请求 https 的请求。所以就重点关注了 request 库 socket,https,proxy 相关的问题,最后找到了一个 issue (opens new window)。其实一开始就看到了issue,但是不知道为什么没有注意???蒸腾了1,2天之后再次看到它 - - 😢

p.s Error: socket hang up (opens new window) 看到一个配置,请求的 header 里面,默认是 keepAlive:true,这个时候,可能保持很多socket请求。所有可以配置

const HttpsAgent = require('agentkeepalive').HttpsAgent;

const agent = new HttpsAgent({
    freeSocketTimeout: 4000
});

1
2
3
4
5
6

# 解决方案

最终解决方案,

  1. 针对 TIME_WAIT 修改服务器配置
  2. 针对 CLOSE_WAIT 请求的时候设置 headers 的 Connection: 'close''
const rp = require('request-promise');

rp({
  url: 'https://www.example.com',
  proxy: 'http://1.1.1.1:8888',
  headers:{
    Connection: 'close'
  }
})

1
2
3
4
5
6
7
8
9
10

# 补充

一个小插曲,针对 TIME_WAIT 其实都没有去进行服务器配置的修改,但是早上突然发现还有大量 LAST_ACK 的状态。度娘了一下,发现好像被针对了 - -。遂进行了一番学习和配置。

# 当keepalive打开的情况下,TCP发送keepalive消息的频率。(默认值是7200(2小时),服务器建议设为1800)
net.ipv4.tcp_keepalive_time = 200
# 在近端丢弃TCP连接之前﹐要进行多少次重试。默认值是7个﹐在大负载服务器上建议调整为3)
net.ipv4.tcp_keepalive_probes = 3
# 探测消息发送的频率,乘以tcp_keepalive_probes就得到对于从开始探测以来没有响应的连接杀除的时间。(默认值为75秒,推荐设为15秒)
net.ipv4.tcp_keepalive_intvl = 15
1
2
3
4
5
6

最后还配置了 ip 限制,针对发送请求的几个 ip 段。

# 参考

上次更新: 5/6/2022, 4:44:11 PM