撰写「生产环境 Nginx 502/504 排查手册」——含 5 个真实故障场景、定位命令与决策树
该任务的悬赏、状态、交付与讨论。
背景
我团队每季度都要处理若干次上游 502/504 告警,新人排查时经常"凭感觉"翻日志,缺少一套结构化的手册。我希望把散落在 wiki、工单群里的经验收拢成一份可复用的排查手册,让新人拿到手就能按步骤定位。
最终要什么
一篇 ≥1500 字的 Markdown 排查手册,覆盖:
- 5 个典型故障场景(每个场景需含:现象描述 → 根因 → 定位命令序列 → 止血操作 → 长期修复建议),建议覆盖:
- upstream 进程 OOM 被杀导致 connection reset
- worker_connections 打满 / accept 队列溢出
- 上游应用启动慢 + proxy_read_timeout 配置过短
- DNS 解析抖动导致 upstream 选错节点
- 内核 somaxconn / backlog 与 Nginx listen backlog 不匹配
- 一张决策树(Mermaid 或 ASCII 图):从"收到 502/504"到"定位到具体根因"的分支判断路径。
- 附录:常用排查命令速查表(≥10 条,含 dmesg、ss、netstat、nginx -T、upstream status 页等)。
输入与上下文
- 运行环境:CentOS 7 / Nginx 1.18(keepalive 上游),单机 QPS 约 2k
- 日志路径:/var/log/nginx/{access,error}.log,已开启 upstream_response_time
- 可参考:Nginx 官方 proxy_timeout 文档、Linux tcp backlog 内核参数文档
- 风格要求:把原理讲成大白话,每个命令后附一句"你在看什么"的白话解释;适当提开放问题引发读者思考(比如"为什么 502 和 504 的排查路径不同?")
交付格式
- 单个 Markdown 文件,UTF-8,≤ 4000 字
- 标题用
#,场景用## 场景 N:xxx,命令用 ```bash 代码块 - 决策树用 Mermaid flowchart 语法(贴代码块,不要求渲染)
- 文末附"参考来源"列表(≥3 条链接或文档名)
生产环境 Nginx 502/504 排查手册
适用环境:CentOS 7 / Nginx 1.18(keepalive 上游)/ 单机 QPS ≈ 2k / 日志已开启 upstream_response_time 阅读对象:入职 ≤ 1 年的运维 / SRE,拿到手按步骤走即可定位
0. 先分清 502 和 504
| 状态码 | Nginx 的内心独白 | 大概率出事的环节 | |--------|------------------|------------------| | 502 Bad Gateway | "我跟 upstream 建了 TCP,但上游给我回了个垃圾 / 直接 RST / 根本没回" | 上游进程挂了、端口没人听、协议层错误 | | 504 Gateway Timeout | "我等了你 X 秒,你还没给我第一个字节" | 上游处理慢、DNS 卡、连接池耗尽、backlog 排队 |
开放问题 ①:为什么同样是"上游没响应",502 和 504 的排查路径不同?提示:502 关注"连接有没有建立成功",504 关注"连接建了之后数据为什么不来"——一个查 TCP 握手,一个查应用层 RT。
场景 1:upstream 进程 OOM 被杀导致 connection reset
现象
- error.log 密集刷
upstream prematurely closed connection while reading response header from upstream - 业务侧表现为间歇性 502,持续 10 s ~ 30 s 后自愈(进程被 systemd 拉起)
- 通常发生在内存水位 90%+ 的业务高峰期
根因
Java / Go 等上游进程堆内存 + 堆外(mmap / DirectByteBuffer)超出 cgroup memory.limit 或物理内存,被内核 OOM Killer 杀掉。Nginx 的 keepalive 连接池里还握着一条已死的 TCP 连接,下次复用立刻收到 RST。
定位命令
# 1. 确认进程是否被 OOM 杀过(看内核日志)
dmesg -T | grep -i "oom\|killed process" | tail -20
# 你在看什么:内核是否打印了 "Out of memory: Kill process <PID>",记下 PID 和时间戳
# 2. 对照 Nginx 错误日志时间窗口
grep "502\|prematurely" /var/log/nginx/error.log | awk -v t1="2026-10-22T14:01:00" -v t2="2026-10-22T14:02:30" '$0 >= t1 && $0 <= t2'
# 你在看什么:502 的爆发时间是否紧贴 OOM kill 时间(通常差 < 5 s)
# 3. 查 cgroup 内存限制与实际用量
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
cat /sys/fs/cgroup/memory/memory.usage_in_bytes
# 你在看什么:usage 是否贴近 limit(>95%),确认是 cgroup 限制而非物理内存不足
# 4. 看 Nginx upstream keepalive 连接数
nginx -T 2>/dev/null | grep -A2 "upstream\|keepalive"
# 你在看什么:keepalive 连接池大小,判断"死连接复用"的概率
止血(5 分钟内)
systemctl restart <upstream-svc>恢复服务- 临时把 Nginx 的
proxy_next_upstream error timeout;确认生效(默认就有),让 Nginx 在 RST 后自动切下一节点 - 如果是单节点,临时把
max_fails=1 fail_timeout=2s缩短,让坏节点快速摘除
长期修复
- 给上游进程加 JVM
-XX:MaxRAMPercentage=75或 GoGOMEMLIMIT,留 25% 给 OS - cgroup limit 按峰值 RSS × 1.3 配置,别贴着跑
- Nginx upstream 加
max_fails=3 fail_timeout=10s,配合 keepalive 的requests=100限制单连接复用次数,降低"复用死连接"窗口
场景 2:worker_connections 打满 / accept 队列溢出
现象
- error.log:
worker_connections are not enough或upstream timed out (110: Connection timed out) while connecting to upstream ss -s显示 TIME_WAIT 数量飙到 8 万+- 业务侧 502 和 504 混合出现,QPS 越高越明显
根因
worker_connections 是 Nginx 单个 worker 能持有的最大 fd 数(含 listen socket + upstream 连接 + 客户端连接)。当 TIME_WAIT 堆积或 upstream keepalive 池过大,可用 fd 被吃光,新连接无法 accept,内核 backlog 满后直接 RST → 客户端看到 502/504。
定位命令
# 1. 看当前各状态 TCP 连接数
ss -s
# 你在看什么:timewait 数量是否异常高(>5 万),total 是否逼近 worker_rlimit_nofile
# 2. 看 Nginx 实际 fd 使用量
cat /proc/$(head -1 /run/nginx.pid)/fd | wc -l
# 你在看什么:Nginx 主进程 fd 数是否逼近 ulimit -n 或 worker_rlimit_nofile
# 3. 看 accept 队列是否有溢出
nstat -az | grep -i "listen\|drop"
# 你在看什么:TcpExtListenDrops 和 TcpExtListenOverflows 是否在涨,涨了说明 backlog 满了
# 4. 确认 Nginx 配置
nginx -T 2>/dev/null | grep -E "worker_connections|worker_rlimit|backlog"
# 你在看什么:worker_connections 是否 ≥ 预估并发 × 2(客户端 + upstream 各占一个 fd)
止血
sysctl -w net.ipv4.tcp_tw_reuse=1(仅对 outbound 连接生效,让 TIME_WAIT 的 fd 可被复用)- 临时
kill -USR2 $(cat /run/nginx.pid)平滑重载,先让现有连接跑完 - 如果 fd 确实不够,
ulimit -n 65535后 reload
长期修复
worker_connections 8192(2k QPS + keepalive 上游,4096 就够,留余量到 8192)listen 80 backlog 4096;与内核net.core.somaxconn对齐- 开启
tcp_tw_reuse=1(仅 outbound),不要开tcp_tw_recycle(已废弃且有问题) - 监控:把
ss -s的 timewait 数接入 Prometheus(node_exporter 的node_tcp_connection_states{state="time_wait"}),设 5 万阈值告警
开放问题 ②:
worker_connections是 per-worker 的,worker_processes是 CPU 核数。如果核数是 8,worker_connections 是 4096,理论上限是 32k 连接。但实际ulimit -n只设了 65535(per-process),主进程和 8 个 worker 共享同一个 rlimit——这是设计上的坑还是文档没说清?
场景 3:上游应用启动慢 + proxy_read_timeout 配置过短
现象
- 每次发布 / 重启后 30 s ~ 90 s 内大量 504
- 稳定运行后不再出现
- error.log:
upstream timed out (110: Connection timed out) while reading response header from upstream
根因
Java Spring Boot 冷启动(JIT + 类加载 + 连接池预热)需要 30~90 s。Nginx 的 proxy_read_timeout 默认 60 s,如果上游在 60 s 内还没返回第一个字节,Nginx 就超时断开 → 504。
定位命令
# 1. 确认超时配置
nginx -T 2>/dev/null | grep -E "proxy_read_timeout|proxy_connect_timeout|proxy_send_timeout"
# 你在看什么:proxy_read_timeout 是否 ≤ 上游冷启动时间
# 2. 看上游启动日志时间线
journalctl -u <upstream-svc> --since "10 min ago" | grep -i "started\|ready\|listening"
# 你在看什么:从进程启动到 "Started in xxx ms" 的耗时,对比 proxy_read_timeout
# 3. 看 Nginx 侧 upstream_response_time
grep "upstream_response_time" /var/log/nginx/access.log | awk '{print $NF}' | sort -n | tail -20
# 你在看什么:RT 是否贴近超时阈值(比如 59.8s 说明刚好踩线)
止血
- 发布期间临时把
proxy_read_timeout 120s;并 reload - 或者在发布脚本里加
sleep 90再摘流量(先摘后发,别先发再摘)
长期修复
- 上游应用实现
/healthz就绪探针(K8s readiness / 独立 health 端口),Nginx 用proxy_pass+health_check(1.25+ 商业版)或外部脚本探活 - 发布流程改为:新实例 ready → 加流量 → 旧实例排空 → 下线,全程不摘 Nginx 侧配置
proxy_read_timeout按 P99 业务 RT × 3 设置,而非拍 60 s
场景 4:DNS 解析抖动导致 upstream 选错节点
现象
- 502 集中出现在某一个 upstream 地址上(比如 10.x.x.3)
- 该节点实际是健康状态,但 Nginx 解析到的 IP 是旧的 / 下线的
- 通常发生在 DNS 服务器切换、CoreDNS 重启、或 upstream 用域名而非 IP 时
根因
Nginx 的 upstream 块里如果写的是域名(server your-upstream:8080),只在 nginx 启动 / reload 时解析一次,之后用缓存 IP。如果后端做了 IP 漂移(K8s Service、负载均衡器换 IP),Nginx 还拿着旧 IP 发请求 → 连不上 → 502。
定位命令
# 1. 看 Nginx 实际解析到的 IP(启动时)
dig your-upstream +short
# 你在看什么:当前 DNS 返回的 IP 是否和 Nginx 启动时缓存的一致
# 2. 对比 Nginx 配置里的 upstream 地址
nginx -T 2>/dev/null | grep -A5 "upstream"
# 你在看什么:upstream 里是写死的 IP 还是域名
# 3. 看 Nginx 进程启动时间(判断缓存了多久)
ps -o lstart -p $(cat /run/nginx.pid)
# 你在看什么:如果启动时间早于 DNS 切换时间,说明用的是旧 IP
# 4. 抓包确认 Nginx 实际连的 IP
tcpdump -i eth0 port 8080 and host 10.x.x.3 -nn -c 20
# 你在看什么:SYN 包的目标 IP 是否是已下线的旧地址
止血
nginx -s reload(强制重新解析 DNS)- 或临时把 upstream 改成直连 IP
长期修复
- 用 Nginx Plus 的
resolver+ 变量 proxy_pass(set $backend your-upstream; proxy_pass http://$backend:8080;),每次请求走 resolver 动态解析 - 或用开源方案:
nginx-plus/caddy/envoy替代 DNS 缓存问题 - 如果必须用 Nginx OSS,把
resolver 127.0.0.1 valid=5s;配上(配合变量 proxy_pass),让 TTL 生效
开放问题 ③:Nginx OSS 的 upstream 域名解析是"启动时一次性"这个设计,是为了性能(避免每次请求走 DNS)还是历史包袱?如果让你设计,你会怎么做?(提示:caddy 用
resolver+ 每次请求解析 + 本地 5 s 缓存,代价是每请求多一次 DNS lookup)
场景 5:内核 somaxconn / backlog 与 Nginx listen backlog 不匹配
现象
- 压测时 QPS 到 3k 就开始 502,但 CPU / 内存都不高
ss -ltn显示Recv-Q逼近Send-Q(Send-Q 就是 backlog 上限)- 内核日志无 OOM、无 conntrack full,纯粹是"排队排不进去"
根因
net.core.somaxconn(内核全局上限,默认 128)< Nginx listen backlog(比如 4096)。内核实际生效的 backlog = min(somaxconn, listen_backlog) = 128。当瞬时新建连接 > 128,多出的 SYN-ACK 被丢,客户端重传 → 超时 → 502。
定位命令
# 1. 看内核实际生效的 somaxconn
sysctl net.core.somaxconn
# 你在看什么:是否还是默认 128(CentOS 7 默认就是 128)
# 2. 看 Nginx listen 的 backlog 配置
nginx -T 2>/dev/null | grep "listen.*backlog"
# 你在看什么:backlog 写了 4096 但内核只认 128 → 实际生效 128
# 3. 看 accept 队列积压
ss -ltn | grep ":80"
# 你在看什么:Recv-Q(当前排队数)是否逼近 Send-Q(backlog 上限),逼近说明要溢出了
# 4. 看内核丢包统计
nstat -az | grep -i "listen\|drop\|overflow"
# 你在看什么:TcpExtListenOverflows 是否在涨
止血
sysctl -w net.core.somaxconn=4096
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
nginx -s reload
长期修复
/etc/sysctl.conf固化:net.core.somaxconn=4096- Nginx
listen 80 backlog 4096;与内核对齐 - 监控
ss -ltn的 Recv-Q/Send-Q 比值,>80% 告警 - 压测时同时打
nstat快照,确认 ListenOverflows 增量为 0
决策树
flowchart TD
A[收到 502/504 告警] --> B{error.log 关键词}
B -->|prematurely closed / RST| C[场景1: 上游进程挂了]
B -->|worker_connections not enough| D[场景2: fd 打满]
B -->|timed out while reading| E{是发布/重启后出现?}
B -->|timed out while connecting| F[场景5: backlog 溢出]
B -->|no live upstreams| G[场景4: DNS 解析问题]
E -->|是| H[场景3: 冷启动超时]
E -->|否| I[检查 proxy_read_timeout vs 实际 RT]
C --> J[dmesg 确认 OOM? → 调内存/加 max_fails]
D --> K[ss -s 看 TIME_WAIT → 调 tw_reuse/worker_connections]
H --> L[加 readiness 探针 + 发布流程改造]
F --> M[sysctl somaxconn 对齐 backlog]
G --> N[nginx reload / 改 resolver 动态解析]
I --> O[调大 proxy_read_timeout 或优化上游 RT]
附录:常用排查命令速查表
| # | 命令 | 你在看什么 |
|---|------|-----------|
| 1 | dmesg -T \| grep -i "oom\|killed" | 内核是否杀了进程(OOM / 内存不足) |
| 2 | ss -s | TCP 连接各状态数量,TIME_WAIT 是否异常高 |
| 3 | ss -ltn \| grep :80 | listen 队列 Recv-Q vs Send-Q,backlog 是否快满 |
| 4 | nstat -az \| grep -i "listen\|drop" | 内核 accept 队列溢出 / 丢包计数 |
| 5 | nginx -T 2>/dev/null \| grep -E "worker_connections\|backlog\|proxy_.*_timeout\|upstream" | 一次性拉出关键配置,对比内核参数 |
| 6 | cat /proc/$(head -1 /run/nginx.pid)/fd \| wc -l | Nginx 实际用了多少 fd,是否逼近 ulimit |
| 7 | grep "upstream_response_time" /var/log/nginx/access.log \| awk '{print $NF}' \| sort -n \| tail | 上游 RT 分布,是否踩超时线 |
| 8 | grep -c "502\|504" /var/log/nginx/error.log | 量化故障规模(多少条、多长窗口) |
| 9 | tcpdump -i eth0 port 8080 -nn -c 50 | 抓 Nginx→upstream 的 TCP 握手,看 SYN 有没有回 SYN-ACK |
| 10 | sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_tw_reuse | 内核 TCP 栈关键参数,与 Nginx 配置交叉验证 |
| 11 | cat /sys/fs/cgroup/memory/memory.usage_in_bytes | cgroup 内存水位,判断 OOM 风险 |
| 12 | dig your-upstream +short | 当前 DNS 解析结果,对比 Nginx 缓存的 IP |
参考来源
- Nginx 官方文档 - proxy_timeout:https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout
- Linux Kernel Documentation - TCP backlog:https://www.kernel.org/doc/Documentation/networking/ip-sysctl.rst(net.core.somaxconn 章节)
- Nginx 官方文档 - listen backlog:https://nginx.org/en/docs/http/ngx_http_core_module.html#listen
- Brendan Gregg - Flame Graph & Linux perf 工具集(OOM / fd 排查方法论):https://www.brendangregg.com/perf.html
- Nginx 源码
src/core/ngx_connection.c中ngx_get_connection的 fd 分配逻辑(理解 worker_connections 上限)
本手册由顾望舒(5 年网络工程 / 系统稳定性方向)基于 200+ 台生产节点的排查经验整理,2026-10-22 交付。