凌晨被叫醒,打开监控:Nginx 大面积 502。你重启、回滚、拜佛,最后发现只是 PHP-FPM 的 pm.max_children 被打满了。502/504 的可怕之处在于原因藏在 Nginx 身后——它只是个传话的,真正的凶手在后端。本文用故障树把 502/504 的所有常见分支逐层拆开,每个分支附真实错误日志片段、根因和解法,让你下次 30 秒定位,而不是 30 分钟瞎猜。
一、先分清:502 和 504 是两类完全不同的病
| 状态码 | 含义 | 一句话根因 | 典型日志关键词 |
|---|
| 502 Bad Gateway | Nginx 联系不上后端,或后端的回答没法理解 | 连接层就失败了 | connect() failed / prematurely closed / too big header |
| 504 Gateway Timeout | Nginx 联系上了后端,但后端超时没回话 | 后端处理太慢 | upstream timed out |
这个区分决定了排查方向:502 查"连接"(进程在不在、端口通不通、权限够不够),504 查"速度"(慢查询、死锁、资源耗尽)。用 504 的思路去修 502,修到天亮也没用。
二、故障树总览
拿到故障先别急着动手,对着这棵树走一遍:
用户报障:5xx
│
├─ 看 Nginx error.log 的第一行关键词
│
├─ "connect() failed (111: Connection refused)"
│ └─ 502-A:后端进程挂了 / 端口不对 / 监听地址错(§三)
│
├─ "connect() to unix:...sock failed (13: Permission denied)"
│ └─ 502-B:PHP-FPM socket 权限问题(§四)
│
├─ "upstream prematurely closed connection"
│ └─ 502-C:后端收下了连接但主动掐断(§五)
│
├─ "upstream sent too big header"
│ └─ 502-D:响应头超过 Nginx 缓冲区(§六)
│
├─ "no live upstreams"
│ └─ 502-E:upstream 里所有节点都被标记下线(§七)
│
└─ "upstream timed out (110: Connection timed out)"
└─ 504:后端太慢 / 超时参数太短(§八)
第一动作永远是同一句话:
tail -100 /var/log/nginx/error.log
error.log 里 while connecting to upstream 或 while reading response header from upstream 这半句,就是故障树的分叉口。
三、502-A:Connection refused —— 后端根本没在听
日志现场:
2026/10/19 03:12:05 [error] 1234#1234: *567 connect() failed (111: Connection refused)
while connecting to upstream, client: 10.0.0.88, server: api.example.com,
upstream: "http://127.0.0.1:8080/login", host: "api.example.com"
根因(按概率排序):
后端进程挂了。Java OOM、PHP-FPM 崩溃、Node 进程退出——最常见,没有之一;
端口写错。后端实际监听 8081,proxy_pass 写的 8080;
监听地址错。后端 bind 在 127.0.0.1,Nginx 却通过内网 IP 访问(或反之)。
30 秒定位法:
# ① 端口到底谁在听
ss -lntp | grep-E'8080|8081'
# ② 直接绕过 Nginx 打后端
curl-v http://127.0.0.1:8080/login
# ③ 后端进程还活着吗
ps aux | grep-E'java|php-fpm|node' | grep-vgrep
解法:后端挂了就按后端的方式排查(看应用日志、OOM killer 记录 dmesg | grep -i kill);端口/地址错了改 proxy_pass。根治靠进程守护:systemd 的 Restart=always、supervisor、或 K8s 的 livenessProbe,别靠人肉重启。
四、502-B:Permission denied —— PHP-FPM 的 socket 权限
日志现场:
2026/10/19 03:15:22 [crit] 1234#1234: *890 connect() to unix:/run/php-fpm/www.sock
failed (13: Permission denied) while connecting to upstream,
upstream: "fastcgi://unix:/run/php-fpm/www.sock:"
根因:Nginx 的 worker 以 nginx(或 www-data)用户运行,而 PHP-FPM 的 socket 文件属主和权限不允许它访问。常见于:PHP-FPM 重装后配置重置、有人手改了 socket 目录权限。
解法,改 PHP-FPM 的 pool 配置(如 /etc/php-fpm.d/www.conf):
listen = /run/php-fpm/www.sock
listen.owner = nginx ; socket 属主 = Nginx 运行用户
listen.group = nginx
listen.mode = 0660
重启 PHP-FPM 后验证:
ls-l /run/php-fpm/www.sock
# srw-rw---- 1 nginx nginx ... www.sock
sudo-u nginx curl--unix-socket /run/php-fpm/www.sock http://localhost/ # 以 nginx 身份试连
顺带一个孪生坑:日志是 (2: No such file or directory) 而不是 Permission denied——socket 文件不存在,说明 PHP-FPM 根本没启动,或 listen 路径和 Nginx 的 fastcgi_pass 不一致。
五、502-C:upstream prematurely closed connection —— 后端主动掐线
日志现场:
2026/10/19 03:21:40 [error] 1234#1234: *1024 upstream prematurely closed connection
while reading response header from upstream, upstream: "http://127.0.0.1:8080/api/order"
翻译:TCP 连接建上了,Nginx 也把请求发过去了,后端处理到一半把连接断了。这是后端"内伤"的信号,常见根因:
后端进程崩溃:处理这个请求时 panic / OOM / 段错误,连接随进程死亡被内核关闭;
后端自己的超时更短:比如 Node 默认 server.timeout = 120s(老版本),请求处理超过后主动断开——Nginx 看到的就是"prematurely closed";
PHP-FPM 的 request_terminate_timeout 到期:PHP 脚本跑太久被 FPM 杀掉;
请求体过大被后端拒收:Tomcat 的 maxPostSize、Express 的 body-parser limit。
定位:这种情况 Nginx 日志只能告诉你"被掐了",凶手名在后端日志里:
# 按故障时间点翻后端日志
journalctl -u php-fpm --since"03:20"--until"03:23"
grep"03:21" /var/log/php-fpm/www-error.log
dmesg | grep-i oom # 是不是被 OOM killer 干掉的
解法对应:崩溃就修代码/扩内存;超时不对齐就统一链路超时(见 §八);PHP 长任务调大 request_terminate_timeout。
六、502-D:upstream sent too big header —— 缓冲区装不下
日志现场:
2026/10/19 03:30:11 [error] 1234#1234: *2048 upstream sent too big header
while reading response header from upstream
根因:后端返回的响应头超过了 Nginx 缓冲区。几乎都是巨型 Cookie(塞了 JWT + 用户画像 + 埋点信息)或后端框架输出超长自定义头。FastCGI 和 HTTP 代理各有一套缓冲参数,报错哪个就调哪个:
# HTTP 代理(proxy_pass)
proxy_buffer_size 16k;
proxy_buffers8 16k;
proxy_busy_buffers_size 32k;
# FastCGI(PHP-FPM)
fastcgi_buffer_size 16k;
fastcgi_buffers8 16k;
fastcgi_busy_buffers_size 32k;
注意:调大缓冲区是止血,治本要查后端为什么输出这么大的头——把 session 数据塞进 Cookie 是常见坏味道,header 超过 8K 基本就是设计问题。
七、502-E:no live upstreams —— 全员被"标记下线"
日志现场:
2026/10/19 03:40:02 [error] 1234#1234: *4096 no live upstreams
while connecting to upstream
根因:upstream 里有多个节点,Nginx 默认的健康策略(max_fails=1 fail_timeout=10s)把所有节点都在过去 10 秒内标记为失败——哪怕它们只是偶发抖动了一下。于是新请求连试都不试,直接 502。
解法:放宽熔断参数 + 给重试留后路:
upstreambackend {
server 10.0.0.21:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.22:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.23:8080 backup; # 兜底节点,全员挂时才启用
}
location / {
proxy_passhttp://backend;
proxy_next_upstreamerror timeouthttp_502 http_503; # 允许换节点重试
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 5s; # 重试总预算,防雪崩
}注意 proxy_next_upstream 默认不含 504(怕重试导致后端更崩),也别对非幂等的 POST 请求乱开重试——重复下单的锅比 502 大得多。
八、504:upstream timed out —— 联系上了,但后端太慢
日志现场:
2026/10/19 04:02:17 [error] 1234#1234: *8192 upstream timed out (110: Connection timed out)
while reading response header from upstream, upstream: "http://127.0.0.1:8080/report/export"
根因两条线:
① 后端真的慢。 报表导出、没加索引的慢查询、循环里调外部接口——请求处理时间超过了 Nginx 的耐心。证据在后端:慢查询日志、APM 链路追踪。
② Nginx 超时参数太短。 默认 proxy_read_timeout 60s,正常业务偶尔要跑 2 分钟的接口就会被冤杀。
解法,先把超时参数家族认全(很多人只知道一个):
# HTTP 代理场景
proxy_connect_timeout 10s; # 连上后端的超时(连不上走这里,别设长)
proxy_send_timeout 60s; # 把请求发给后端的超时
proxy_read_timeout 120s; # 等后端响应的超时(504 的命门)
# FastCGI 场景
fastcgi_connect_timeout 10s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 120s; # 对应 PHP-FPM
但调大超时是最后的手段。正确的优先级:
能异步就异步:报表导出改成"提交任务→轮询/推送结果",别让用户干等 HTTP;
能优化就优化:慢查询加索引、N+1 改批量——后端 200ms 能搞定的,不该让 Nginx 等 120s;
必须长的才调长:个别接口单独 location 里放宽 proxy_read_timeout,别在全局调大——全局长超时会让慢请求堆积,耗尽 worker 连接数,把局部问题拖成全站雪崩。
整条链路的超时要对齐:Nginx 120s,但前端的 CDN 或 LB 只有 60s,那 61 秒的请求照样死在半路。从客户端到应用,逐跳核对。
九、30 秒定位流程
把前面的故障树压缩成一张可执行的排查动线,贴在工位上:
第 1 步(5 秒):tail error.log,抓关键词
│
第 2 步(10 秒):关键词对号入座
├─ refused → ss -lntp 查端口 → 起后端/改配置
├─ denied / sock → ls -l 查权限 → 改 listen.owner
├─ prematurely → 翻后端日志/dmesg → 崩溃或超时不匹配
├─ too big header → 调 proxy/fastcgi buffer,查巨型 Cookie
├─ no live → 查各节点健康 → 调 max_fails/backup
└─ timed out → 后端慢日志 → 异步化/调 proxy_read_timeout
│
第 3 步(15 秒):curl 绕过 Nginx 直打后端验证
curl -v http://后端:端口/路径 # 通 → 问题在 Nginx 层
# 不通 → 问题在后端层
十、检查清单
故障修复后,用这份清单做复盘,防复发:
error.log 里提取了带时间戳的日志片段并归档(复盘证据)
后端进程有自动拉起机制(systemd Restart / supervisor / livenessProbe)
PHP-FPM socket 属主与 Nginx 运行用户一致(0660)
链路超时已对齐:CDN → LB → Nginx → 后端,逐跳核对
proxy/fastcgi buffer 按业务最大响应头留足余量
upstream 多节点配置了合理的 max_fails + backup 兜底
长任务接口已异步化或单独 location 放宽超时
proxy_next_upstream 未对非幂等请求开放重试
502/504 监控告警接的是比例(如 5xx > 1%)而非绝对值
十一、总结
502/504 排查的本质是读懂 Nginx 的"转述":它是网关不是凶手,error.log 里 while ... upstream 的每一个关键词都在指认真正的案发现场。记住两条主线——502 查连接、504 查速度,再配合故障树逐层排除,绝大多数故障 30 秒内就能锁定方向。
最后一条建议:把这份故障树存进团队的运维 Wiki,第一次用它是查故障,第十次用它就是给新人讲课的教材。如果这篇帮你少熬了一个夜,欢迎转发给同样在凌晨三点盯过 error.log 的朋友。
阅读原文:原文链接
该文章在 2026/11/13 13:06:11 编辑过