上个月月底,我负责的一个客户官网突然开始抽风。
不是什么高并发场面,就是一个普通的企业展示站,日均PV撑死两千多。用户反馈说有时候点个菜单按钮,要转圈四五秒才出来。一开始我怀疑是后端接口慢,查了一圈数据库慢查询,优化了几个SQL,确实好了一点,但该卡的时候还是卡。
最离谱的一次,我自己在Chrome里连按F5刷新了五次,第三次直接给我返回了个504 Gateway Timeout。我当场血压就上来了。
排查过程其实挺常规的。先看服务器负载,top一敲,CPU占用才12%,内存还剩3个G,磁盘IO也正常。这明显不是资源瓶颈。nginx的error.log翻出来,发现一堆这样的记录:
2024/01/15 14:23:11 [crit] 12345#0: *6789 connect() to 127.0.0.1:9000 failed (99: Cannot assign requested address) while connecting to upstream
这个错误我见过。Cannot assign requested address,字面意思就是“没有可用端口了”。Nginx作为反向代理,每收到一个请求就要跟后端的PHP-FPM建一个TCP连接。如果并发上来了,系统临时端口用完了,新的连接就建立不了,直接报错。
但是我盯着这个错误看了半天,总觉得哪里不对劲。两千多的日活,怎么就能把端口耗尽了?
然后我查了一下netstat。
netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’
结果出来一看,TIME_WAIT状态的连接堆了八千多个。
问题找到了。Nginx跟PHP-FPM通信的时候,每个请求都用了一个新连接,请求处理完了,连接进入TIME_WAIT,等着系统回收。但是这个回收是有延迟的,默认60秒。如果短时间内请求密集,TIME_WAIT堆到几万个,临时端口池子就见了底。
解决方案其实不复杂,但有个细节坑了我一下午。
最直接的办法是启用长连接。Nginx从1.3.9版本开始就支持upstream的keepalive了,我用的1.18.0,完全没问题。
配置改法是这样的。在http块里,找到那个upstream配置,加上keepalive参数:
upstream php_backend {
server 127.0.0.1:9000;
keepalive 32;
}
然后在server块里,转发请求的地方,加上proxy_http_version和proxy_set_header:
location ~ \.php$ {
proxy_pass http://php_backend;
proxy_http_version 1.1;
proxy_set_header Connection “”;
}
这两行缺一不可。proxy_http_version 1.1是必须的,因为HTTP/1.0不支持keep-alive。proxy_set_header Connection “”是为了清掉默认的Connection头,让Nginx自己管理长连接。
改完之后nginx -t检查语法,然后reload。我盯着监控面板,TIME_WAIT数量肉眼可见地往下掉,从八千多掉到了几百。再F5暴力刷新,页面秒开,再也没有504。
但是,我高兴得太早了。
第二天客户反馈说,后台管理页面偶尔会出现“数据加载失败”的提示。我查了一下日志,发现是某些长时间运行的PHP脚本——比如导出Excel报表的那种——会随机断开连接。错误是“upstream prematurely closed connection while reading response header”。
这个问题坑了我一下午。长连接模式下,如果后端PHP-FPM进程处理完请求后自己断开了连接,而Nginx还复用着这个已经断开的连接,就会报这个错。根本原因是PHP-FPM的pm.max_requests设置。默认情况下,一个PHP-FPM子进程处理完500个请求就会自动退出,释放内存。但如果用了长连接,Nginx不知道这个子进程要退了,还把请求发过去,就撞枪口上了。
解决方案是让PHP-FPM在关闭连接之前,先通知Nginx。这个要靠keepalive_timeout和keepalive_requests来配合。
我在upstream块里加了两行:
keepalive_timeout 60s;
keepalive_requests 1000;
keepalive_timeout告诉Nginx,一个空闲的长连接最多保持60秒,超时就关掉。keepalive_requests限制一个连接最多处理1000个请求,到了就主动断掉,重新建。
这两个参数的意义在于:让连接的断开变得可控,而不是让PHP-FPM那边突然掐断。Nginx自己掌握主动权,就不会出现“我还以为连着结果对面早跑了”的情况。
改完之后,我又蹲了两天监控。TIME_WAIT稳定在300以下,504和premature close错误全部消失。客户再也没反馈过卡顿。
这次优化的收获其实就两点。
第一,长连接不是银弹。遇到TIME_WAIT堆积,上keepalive确实立竿见影,但一定要配套处理连接回收的逻辑。不然后端进程一重启,长连接就会变成“僵尸管道”。
第二,调优之前一定要搞清楚瓶颈到底是什么。我一开始怀疑后端慢,优化SQL,方向错了。真正的根源是TCP连接层面的端口耗尽。如果没有netstat那一顿查,光靠猜,可能还在那里调PHP的max_execution_time。
另外说个题外话。Nginx的keepalive参数值设多少合适,网上说法不一。有人设8,有人设128。我试了一圈,32这个值在我们这个场景下表现最好。太大反而会增加空闲连接的管理开销。具体多少合适,建议压测的时候用wrk或者ab跑一下,看TIME_WAIT和active connections的曲线,哪个平就用哪个。
📎 延伸阅读
看完这篇,如果你想:
- 直接拿工具 → 回复”13“,我把跨境获客工具包发给你
- 系统学习 → 点击菜单”AI训练营”,从0开始跑通AI变现