共计 3586 个字符,预计需要花费 9 分钟才能阅读完成。
Last-Modified: 2019 年 7 月 10 日 21:58:43
项目生产环境出现大量 TIME_WAIT(数千个), 需要一一排查
先上总结:
- nginx 未开启 keep-alive 导致大量主动断开的 tcp 连接
- nginx 与 fastcgi(php-fpm) 的连接默认是短连接, 此时必然出现 TIME_WAIT
状态确认
统计 TIME_WAIT 连接的本地地址
netstat -an | grep TIME_WAIT | awk '{print $4}' | sort | uniq -c | sort -n -k1
# ... 前面很少的略过
# 2 127.0.0.1:56420
# 442 192.168.1.213:8080
# 453 127.0.0.1:9000
分析:
- 8080 端口是 nginx 对外端口
- 9000 端口是 php-fpm 的端口
8080 对外 web 端口
经过确认, nginx 的配置文件中存在一行
# 不启用 keep-alive
keepalive_timeout 0;
尝试抓取 tcp 包
tcpdump tcp -i any -nn port 8080 | grep "我的 ip"
# 其中某一次连接的输出如下
# 20:52:54.647907 IP 客户端.6470 > 服务端.8080: Flags [S], seq 2369523978, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
# 20:52:54.647912 IP 服务端.8080 > 客户端.6470: Flags [S.], seq 1109598671, ack 2369523979, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
# 20:52:54.670302 IP 客户端.6470 > 服务端.8080: Flags [.], ack 1, win 256, length 0
# 20:52:54.680784 IP 客户端.6470 > 服务端.8080: Flags [P.], seq 1:301, ack 1, win 256, length 300
# 20:52:54.680789 IP 服务端.8080 > 客户端.6470: Flags [.], ack 301, win 123, length 0
# 20:52:54.702935 IP 服务端.8080 > 客户端.6470: Flags [P.], seq 1:544, ack 301, win 123, length 543
# 20:52:54.702941 IP 服务端.8080 > 客户端.6470: Flags [F.], seq 544, ack 301, win 123, length 0
# 20:52:54.726494 IP 客户端.6470 > 服务端.8080: Flags [.], ack 545, win 254, length 0
# 20:52:54.726499 IP 客户端.6470 > 服务端.8080: Flags [F.], seq 301, ack 545, win 254, length 0
# 20:52:54.726501 IP 服务端.8080 > 客户端.6470: Flags [.], ack 302, win 123, length 0
上述具体的 ip 已经被我批量替换了, 不方便暴露服务器 ip
分析:
- 可以看到 4 次挥手的开始是由服务端主动发起的 (记住 TIME_WAIT 只会出现在主动断开连接的一方)
- 个人理解是, nginx 在配置 ” 不启用 keep-alive” 时, 会在 http 请求结束时主动断开连接.
- 尝试开启 http 的 keep-alive
修改 nginx 配置
keepalive_timeout 65;
reload nginx
nginx -s reload
再次抓包
tcpdump tcp -i any -nn port 8080 | grep "我的 ip"
# 21:09:10.044918 IP 客户端.8217 > 服务端.8080: Flags [S], seq 1499308169, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
# 21:09:10.044927 IP 服务端.8080 > 客户端.8217: Flags [S.], seq 2960381462, ack 1499308170, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
# 21:09:10.070694 IP 客户端.8217 > 服务端.8080: Flags [.], ack 1, win 256, length 0
# 21:09:10.077437 IP 客户端.8217 > 服务端.8080: Flags [P.], seq 1:302, ack 1, win 256, length 301
# 21:09:10.077443 IP 服务端.8080 > 客户端.8217: Flags [.], ack 302, win 123, length 0
# 21:09:10.198117 IP 服务端.8080 > 客户端.8217: Flags [P.], seq 1:671, ack 302, win 123, length 670
# 21:09:10.222957 IP 客户端.8217 > 服务端.8080: Flags [F.], seq 302, ack 671, win 254, length 0
# 21:09:10.222980 IP 服务端.8080 > 客户端.8217: Flags [F.], seq 671, ack 303, win 123, length 0
# 21:09:10.247678 IP 客户端.8217 > 服务端.8080: Flags [.], ack 672, win 254, length 0
注意看上面很有意思的地方:
- tcp 的挥手只有 3 次 , 而非正常的 4 次. 个人理解是, 服务端在收到 FIN 时, 已经确认自己不会再发送数据, 因此就将 FIN 与 ACK 一同合并发送
- 此时是客户端主动断开 tcp 连接, 因此服务端不会出现 TIME_WAIT
再次查看连接状态
netstat -an | grep TIME_WAIT | awk '{print $4}' | sort | uniq -c | sort -n -k1
# ... 忽略上面
# 1 127.0.0.1:60602
# 1 127.0.0.1:60604
# 344 127.0.0.1:9000
此时发现已经没有处于 TIME_WAIT 的连接了.
9000 fast-cgi 端口
经过网上查找资料, 整理:
- nginx 与 fast-cgi 的默认连接是短连接, 每次连接都需要经过一次完整的 tcp 连接与断开
当前 nginx 配置
upstream phpserver{server 127.0.0.1:9000 weight=1;}
修改 nginx 配置使其与 fastcgi 的连接使用长连接
upstream phpserver{
server 127.0.0.1:9000 weight=1;
keepalive 100
}
fastcgi_keep_conn on;
说明:
- upstream 中的
keepalive
指定 nginx 每个 worker 与 fastcgi 的最大长连接数, 当长连接不够用时, 此时新建立的连接会在请求结束后断开 (由于此时指定了 HTTP1.1, fastcgi 不会主动断开连接, 因此 nginx 这边会出现大量 TIME_WAIT, 需谨慎 ( 未验证) - 由于 php-fpm 设置了最大进程数为 100, 因此此处的
keepalive
数量指定 100 (未测试)
此处题外话, 如果 nginx 是作为反向代理, 则需增加如下配置:
# 将 http 版本由 1.0 修改为 1.1
proxy_http_version 1.1;
# 清除 "Connection" 头部
proxy_set_header Connection "";
- 配置
proxy_pass
将请求转发给后端 -
这里, 理解一下
proxy_pass
与fastcgi_pass
区别客户端 --http--> 前端负载均衡 Nginx --proxy_pass--> 业务服务器 Nginx --fastcgi_pass--> 业务服务器 php-fpm
再次确认 tcp 连接情况
netstat -antp | grep :9000 | awk '{print $(NF-1)}' | sort | uniq -c
# 6 ESTABLISHED
# 1 LISTEN
ok, 问题解决.
另一种解决方法:
若 nginx 与 fast-cgi 在同一台服务器上, 则使用 unix 域 会更为高效, 同时避免了 TIME_WAIT 的问题.
题外
经过上面优化后, TIME_WAIT 数量从上千个大幅下降到几十个, 此时发现 TIME_WAIT 中的存在大量的 127.0.0.1:6379, 6379 是 redis 服务的默认端口 ….
赶紧改业务代码去, 将 $redis->connect(...)
改成 $redis->pconnect(...)
说明:
-
pconnect
表示 php-fpm 与 redis 建立 tcp 连接后, 在本次 http 请求结束后仍维持该连接, 下次新的请求进来时可以复用该连接, 从而复用了 tcp 连接.
正文完