去年双十一凌晨,我负责的电商平台突然大面积报502和504。用户反馈页面白屏,客服电话被打爆。我登录服务器,Nginx日志里全是upstream timed out和connect() failed。当时第一反应是PHP-FPM挂了,但重启后几分钟又复现。折腾了3小时,最后发现是系统内核参数net.ipv4.tcp_tw_reuse没开,加上MySQL连接池耗尽。这篇文章把排查过程拆开讲,每个步骤都有代码和压测数据。
502 Bad Gateway:Nginx作为反向代理,向上游(PHP-FPM、Node.js等)发请求,但上游没响应或返回无效响应。504 Gateway Timeout:Nginx等上游响应超时,超过proxy_read_timeout设置的时间。
我那次故障,502和504交替出现。502是因为PHP-FPM进程池满了,新请求连不上;504是因为PHP执行时间过长,超过Nginx的超时阈值。
方案A:凭经验重启服务。这是新手做法,治标不治本。我见过有人每5分钟写个cron重启PHP-FPM,结果业务高峰期照样崩。
方案B:系统化排查。从Nginx日志开始,逐层检查PHP-FPM状态、MySQL慢查询、系统资源、内核参数。我推荐方案B,虽然耗时但能根除问题。
日志是上帝视角。先定位错误类型和频率。
# 实时查看502/504错误tail -f /var/log/nginx/error.log | grep -E '502|504'# 统计过去1小时错误数grep -c '502' /var/log/nginx/access.loggrep -c '504' /var/log/nginx/access.log我那次看到connect() to unix:/var/run/php-fpm.sock failed (11: Resource temporarily unavailable),说明PHP-FPM的socket连接数满了。
开启PHP-FPM状态页,实时监控进程池。
# 在PHP-FPM配置中启用状态页# /etc/php/8.3/fpm/pool.d/www.confpm.status_path = /status# 然后通过Nginx访问curl http://127.0.0.1/status?json{ "pool": "www", "process manager": "dynamic", "start time": 1699000000, "start since": 3600, "accepted conn": 100000, "listen queue": 500, "max listen queue": 1000, "listen queue len": 128, "idle processes": 0, "active processes": 50, "total processes": 50, "max active processes": 50, "max children reached": 10, "slow requests": 0}关键指标:listen queue(等待队列长度)和max children reached(达到最大进程数次数)。我那次max children reached是10,说明进程池不够用。
PHP执行慢,通常是因为数据库查询慢。
-- 开启慢查询日志SET GLOBAL slow_query_log = 'ON';SET GLOBAL long_query_time = 2;-- 查看慢查询SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;我找到一条SQL:SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 10,没加索引,执行了3秒。加上索引后降到0.01秒。
用top、free、iostat看CPU、内存、磁盘IO。
# CPU和内存top -bn1 | head -20# 磁盘IOiostat -x 1 5# 网络连接数ss -s我那次CPU正常,但ss -s显示TIME_WAIT连接数超过3万,导致新连接被拒绝。
TIME_WAIT过多,需要调整TCP参数。
# /etc/sysctl.confnet.ipv4.tcp_tw_reuse = 1net.ipv4.tcp_tw_recycle = 0 # 注意:NAT环境下不要开net.ipv4.tcp_fin_timeout = 30net.ipv4.tcp_max_tw_buckets = 20000# 生效sysctl -p同时调整Nginx和PHP-FPM配置:
# /etc/nginx/nginx.confworker_processes auto;events { worker_connections 10240; multi_accept on; use epoll;}http { keepalive_timeout 65; proxy_read_timeout 60; proxy_send_timeout 60; fastcgi_read_timeout 60;}# /etc/php/8.3/fpm/pool.d/www.confpm = dynamicpm.max_children = 100pm.start_servers = 20pm.min_spare_servers = 10pm.max_spare_servers = 30pm.max_requests = 1000request_terminate_timeout = 60优化前后对比(压测工具:wrk,并发1000,持续5分钟):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 502错误数 | 1200 | 0 |
| 504错误数 | 800 | 0 |
| 平均响应时间 | 3.2秒 | 0.8秒 |
| P99响应时间 | 8.5秒 | 1.2秒 |
| PHP-FPM进程数 | 50(满) | 30(空闲10) |
| TIME_WAIT连接数 | 35000 | 500 |
数据来自线上环境:PHP 8.3.6、Nginx 1.24.0、MySQL 8.0.35、Linux 5.15.0。
坑1:net.ipv4.tcp_tw_recycle在NAT环境下会导致连接失败。我开过这个参数,结果部分用户(尤其移动网络)无法访问。后来改成tcp_tw_reuse + 调整tcp_fin_timeout。
坑2:PHP-FPM的pm.max_children不是越大越好。我设成200,结果内存爆了(每个进程约50MB,200个就是10GB)。要根据服务器内存计算:max_children = 总内存 / 每个进程平均内存。
坑3:Nginx的worker_connections设太大,但系统ulimit -n没改,导致Nginx报too many open files。记得同步修改/etc/security/limits.conf:
# /etc/security/limits.conf* soft nofile 65535* hard nofile 65535坑4:MySQL慢查询日志默认不记录索引缺失的查询。加上log_queries_not_using_indexes = 1才能抓到。
坑5:压测时别用ab,它不支持HTTP/1.1 keep-alive。用wrk或hey,更接近真实场景。
502/504排查,核心是看日志、查状态、调参数。别盲目重启,别乱改配置。按我给的流程走一遍,90%的问题能定位到根因。如果还不行,检查防火墙、DNS、上游服务(如Redis、Memcached)是否正常。
专注技术分享与实战