502 和 504 到底是谁的锅
网站访问量一上来就偶尔冒 502 Bad Gateway 或者 504 Gateway Timeout,刷新一下又好了——这几乎是每个用 Nginx + PHP-FPM 架构的站长都经历过的"玄学故障"。多数人的第一反应是重启 PHP-FPM,重启完确实能用一阵,然后高峰期又复发。治标不治本的根本原因,是没有搞清楚这两个错误码的归属:
- 502 Bad Gateway:Nginx 向后端的 PHP-FPM 转发请求,但没有拿到任何有效响应——通常是 FPM 进程池被打满、子进程崩了、或者
listen的 socket 连不上。 - 504 Gateway Timeout:Nginx 连上了 FPM,但 FPM 处理太慢,超过了 Nginx 配置的等待上限,主动断开——问题多半在 PHP 脚本执行时间或数据库慢查询。
一句话区分:502 是"找不到人办事",504 是"人办得太慢"。 搞清楚这点,排查方向就完全不一样了。本文从进程池配置、超时参数、慢日志三个层面,给出可落地的调优方案。
一、先定位:错误日志在哪里、怎么看
没有日志就没有真相。这套架构涉及两个组件的日志,必须都打开。
Nginx 错误日志
tail -f /var/log/nginx/error.log
502 的典型记录:
2026/10/14 10:22:31 [error] 1234#0: *8891 connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream
看到 Resource temporarily unavailable,基本可以断定是 FPM 进程池的 backlog 满了或者进程数不够。
504 的典型记录:
2026/10/14 10:25:02 [error] 1234#0: *9012 upstream timed out (110: Connection timed out) while reading response header from upstream
upstream timed out 说明 Nginx 等超时了,需要去查 PHP 侧为什么慢。
PHP-FPM 慢日志(最重要)
很多站长的 FPM 池配置里根本没开慢日志,导致 504 无从查起。在池配置 /etc/php/8.2/fpm/pool.d/www.conf 里打开:
request_slowlog_timeout = 3s slowlog = /var/log/php-fpm/slow.log request_terminate_timeout = 60s
request_slowlog_timeout 会在脚本执行超过 3 秒时把当时的 PHP 调用栈 dump 到慢日志,request_terminate_timeout 超过 60 秒直接杀掉进程(防止一个僵尸脚本永久占坑)。改完 systemctl reload php8.2-fpm。
慢日志里会明确指到具体文件和行号:
[14-Oct-2026 10:25:01] [pool www] pid 18422 script_filename = /var/www/html/wp-content/plugins/xxx/yyy.php [0x00007f...] sleep() /var/www/.../yyy.php:88 [0x00007f...] curl_exec() /var/www/.../yyy.php:120
看到 curl_exec、sleep、file_get_contents 这种外部调用,就是典型的同步阻塞拖慢整个请求——这才是 504 的真凶,跟 PHP-FPM 配置无关,得优化代码本身。
二、进程池调优:pm 三种模式的取舍
PHP-FPM 有三种进程管理模式,选错模式是 502 频发的主要原因。
1. static(固定进程数)
pm = static pm.max_children = 25
预先创建固定数量的子进程,永远不回收。优点是响应最快、没有进程创建开销;缺点是内存占用固定,即使空闲也占着。适合内存充足、流量稳定的服务器。用 static 时,pm.max_children 是唯一的瓶颈参数,设太大会把内存吃光反而触发 OOM。
2. dynamic(动态伸缩)
pm = dynamic pm.max_children = 50 pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 15
根据负载动态增减子进程。这是最常用的模式,适用于流量有波峰波谷的站点。关键约束是要满足 min_spare ≤ start ≤ max_spare ≤ max_children,否则 FPM 启动会直接报错。
3. ondemand(按需启动)
pm = ondemand pm.max_children = 30 pm.process_idle_timeout = 10s
有请求才 fork 进程,空闲 10 秒就回收。最省内存,但每个新请求有 fork 开销,首次响应会慢一点点。适合低流量、内存紧张的廉价 VPS——如果你只有 1G 内存还跑着一堆服务,ondemand 往往是避免 OOM 的救命选项。
怎么算 max_children?
这是最容易拍脑袋设错的参数。正确算法是:
pm.max_children = 可用内存 / 单个 PHP 进程平均占用内存
先实测单进程占用:
ps -ylC php-fpm --sort:rss | awk '{sum+=$8; n++} END {print "avg RSS KB:", sum/n}'假设单个进程实测 60MB,服务器可用内存 1.5G(预留系统和其他服务),那么 max_children 约等于 1500/60 = 25。千万不要对着总内存算——MySQL、Nginx、系统缓存都要吃内存,把 max_children 算成能吃满全机内存,等于亲手埋下 OOM 的雷。宁可保守一点设 20,也不要激进地设 60。
三、Nginx 侧的超时与缓冲参数
FPM 配好了,Nginx 侧的参数同样关键。在 location ~ \.php$ 或 fastcgi_pass 相关的配置块里:
fastcgi_connect_timeout 60s; fastcgi_send_timeout 60s; fastcgi_read_timeout 60s; fastcgi_buffer_size 32k; fastcgi_buffers 8 32k; fastcgi_busy_buffers_size 64k; fastcgi_temp_file_write_size 64k;
几个要点的取舍:
fastcgi_read_timeout是 Nginx 等后端响应的上限,设太短会导致正常但稍慢的请求被误判 504,设太长会让慢请求堆积占用 worker 连接。建议稍大于业务最长正常耗时,比如后台导出任务需要 30 秒,就设 60 秒。fastcgi_buffer_size和fastcgi_buffers决定 Nginx 能缓存多少响应头/体。接口返回大 JSON 时如果缓冲区不够,Nginx 会把响应写到临时文件,IO 抖动直接拖慢响应。返回体大的站点应该加大 buffer。- 如果 FPM 用 Unix socket(推荐),把
fastcgi_pass unix:/run/php/php8.2-fpm.sock;配好,比 TCP127.0.0.1:9000少一层网络栈开销,高并发下差异明显。
四、backlog:502 里最隐蔽的坑
前面 Nginx 日志里的 Resource temporarily unavailable,很多是 backlog 不够造成的。FPM 池配置和 Nginx 都要调:
; www.conf listen.backlog = 65535
# nginx.conf
events {
worker_connections 4096;
}同时系统层面的 somaxconn 也要放开:
sysctl -w net.core.somaxconn=65535 echo "net.core.somaxconn=65535" >> /etc/sysctl.d/99-web.conf
另外要注意 ulimit:FPM 能开多少文件描述符直接影响并发连接数。在 systemd 单元里设:
[Service] LimitNOFILE=65535
这几个参数是配套的,只调其中一个等于没调——backlog 设大了但 worker_connections 还是默认 1024,一样会在高并发时排队失败。
五、实战排查流程:从告警到根因
把上面的知识点串成一套可复制的排查流程:
- 看 Nginx error.log 区分 502/504:
Resource temporarily unavailable走进程池方向,upstream timed out走慢脚本方向。 - 查 FPM 池状态:开启
pm.status_path = /status,通过 Nginx 暴露后访问,看active processes、max active processes、listen queue三个值。listen queue长期大于 0 说明进程池不够用,max active等于max_children说明一直被打满。 - 看慢日志定位真凶:如果
max active没打满但仍 504,一定是某个脚本执行太慢。慢日志直接指到文件和函数。 - 用 strace 追阻塞:实在找不到原因,对慢进程
strace -p <pid> -T,看时间耗在哪次系统调用上。-T会显示每个调用的耗时,一目了然。 - 回归验证:调整后用
ab或wrk压测。wrk -t4 -c100 -d30s https://你的域名/,观察错误率和 P99 延迟是否改善。
六、连接方式与 socket 的深度取舍
前面提到 FPM 用 Unix socket 比 TCP 更快,但这里还有细节值得展开。Unix socket 走的是内核内存,不经网络协议栈,理论上更快;但如果 Nginx 和 PHP-FPM 跑在同一台机器(绝大多数个人站的情况),两者性能差距在高并发下才明显,低流量时几乎无感。真正决定选哪个的核心是可靠性。
TCP 方式的优点是配置简单、跨机器部署天然支持、连接问题容易用 ss -lntp 直接观察;缺点是每个请求多一层握手。Unix socket 的优点是没有网络开销,缺点是一旦 socket 文件因为重启顺序问题被删掉或权限不对,Nginx 就会持续 502,报 connect() to unix:... failed (2: No such file or directory)。这种故障排查起来比 TCP 更绕。
如果坚持用 socket,务必确认三点:socket 路径两边配置完全一致、FPM 启动的用户和 Nginx 的 worker 用户对目录有权限、重启顺序是 FPM 先于 Nginx。可以给 FPM 单元加依赖:
[Unit] After=network.target Before=nginx.service
对绝大多数单人 VPS,笔者的建议是:流量不大就用 TCP 127.0.0.1:9000 省心,追求极限性能且有能力维护就用 socket。不要为了那一点点性能差距,把自己拖进难以定位的 socket 故障里。
七、一份可直接抄的调优模板
以一台 2 核 4G、跑 WordPress 的 VPS 为例,给出参考配置:
; /etc/php/8.2/fpm/pool.d/www.conf pm = dynamic pm.max_children = 40 pm.start_servers = 8 pm.min_spare_servers = 4 pm.max_spare_servers = 16 pm.max_requests = 300 pm.status_path = /fpm-status request_slowlog_timeout = 3s slowlog = /var/log/php-fpm/slow.log request_terminate_timeout = 60s listen.backlog = 65535
pm.max_requests = 300 是防内存泄漏的关键,让子进程处理 300 个请求后主动退出重建,把可能累积的内存还给系统。代价是轻微的重建开销,换来的是长期稳定的内存曲线,非常划算。
Nginx 侧对应:
fastcgi_read_timeout 60s; fastcgi_buffer_size 32k; fastcgi_buffers 8 32k; fastcgi_busy_buffers_size 64k;
加上系统层的 somaxconn 和 LimitNOFILE,这套组合足以应付绝大多数个人站点的流量峰值。
结语
502 和 504 不是玄学,而是进程池容量、超时设置、脚本性能三者相互作用的结果。真正的调优思路是:先用日志区分错误归属,再用 status 页看实时水位,然后用慢日志定位到具体代码,最后才回头调参数。跳过取证直接改 max_children,只会在一片模糊中反复试错。把这篇文章里的日志开关和 status 页先配好,你以后面对高峰期的告警就能从"重启大法"升级为"精准定位、一次修好"。
八、上线后如何验证调优真的有效
改完参数不验证,等于闭着眼开车。这里给出三个层次验证手段。
第一层:压测看错误率。用 wrk 或 ab 模拟并发,重点看两个数:非 2xx 响应比例和 P99 延迟。调优前先压一次记下基线,调优后再压同样的参数对比。ab -n 5000 -c 100 -k https://你的域名/ 可以看到 Failed requests 从几十降到 0,就是最直接的证据。
第二层:FPM status 看水位。压测过程中并发访问 status 页,观察 max active processes 是否稳定低于 max_children、listen queue 是否始终为 0。如果 max active 一直贴着 max_children 上限,说明并发还是顶到了天花板,要么继续加进程(注意内存),要么上缓存减少单请求耗时。
第三层:慢日志看趋势。连续观察几天慢日志的条数和分布。理想情况是调整后慢日志明显减少;如果照样频繁出现 curl_exec 阻塞,说明瓶颈在代码的外部调用而非进程配置,得考虑加超时、异步化或结果缓存。
另外建议把 pm.status_path 配合 Nginx 只对本地或内网网段开放,避免被外部探测:
location = /fpm-status {
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}这样既能在服务器本地随时查水位,又不会把内部状态信息暴露给搜索引擎和扫描器。