Nginx 排错与调试实战:错误日志、core dump 与 502/504 故障树

系统梳理 Nginx 排错与调试方法论,覆盖错误日志分级与 debug 编译、core dump 与 gdb 分析、连接状态与 stub_status 排查、502/504/499 故障树以及常见性能陷阱定位。

接入层的故障有一个共同特点:Nginx 自己往往「没做错什么」,它只是忠实反映了后端、网络或配置的问题。因此排错的难点不在 Nginx 本身,而在如何快速判断「问题出在链路的哪一段」。502 可能是后端崩了、可能是连接池满了、也可能是 upstream 配错了;504 可能是后端慢、可能是 proxy_read_timeout 太短、也可能是网络丢包。本文给出一套可复用的排查方法论:先用日志分级定位现象,再用 core dump 与 gdb 处理崩溃,然后用故障树把状态码映射到根因,最后梳理常见性能陷阱。

一句话总结: Nginx 排错的核心是「把现象映射到链路分段」,日志告诉你在哪一段,故障树告诉你是哪一类原因。

1. 错误日志分级与 debug 编译

一句话总结: error_log 的级别决定信息量与开销,debug 级别必须用 –with-debug 编译的二进制才可用,生产上只临时开启。

Nginx 的日志级别从低到高为 debug、info、notice、warn、error、crit、alert、emerg。设置级别为 error 时,只有 error 及以上级别才会记录;设置 debug 时则全量记录。级别越低,日志量越大——debug 级别在 QPS 上千的站点上每秒能产生几十 MB 日志。

# 全局:生产环境用 warn 或 error
error_log /var/log/nginx/error.log warn;

# 单虚拟主机覆盖级别(排查某个站点时用)
server {
    listen 80;
    error_log /var/log/nginx/site-error.log info;
}

info 级别会记录上游连接、请求重定向等关键事件,开销可接受,适合在「怀疑但不明确」的阶段开启。debug 级别会记录每一次读写、每一个阶段回调,只有在定位极隐蔽的问题时才临时打开:

# 确认当前二进制是否支持 debug
nginx -V 2>&1 | grep -o with-debug

# 若没有输出,需要重新编译
./configure --with-debug ... && make && make install
# 临时开启 debug:用最小影响的方式,只对一个 location 生效
location /api/problem/ {
    error_log /var/log/nginx/debug-api.log debug;
    proxy_pass http://backend;
}

1.1 用日志字段缩小范围

一句话总结: 在 log_format 里记录 upstream_addr、upstream_status、request_time 与 upstream_response_time,就能区分「后端慢」与「客户端慢」。

默认的 access_log 信息量不足以排错。一份排错友好的日志格式应该包含上游地址、上游状态码与两段耗时:

log_format debug '$remote_addr - $remote_user [$time_local] "$request" '
                 '$status $body_bytes_sent "$http_referer" '
                 '"$http_user_agent" '
                 'rt=$request_time urt=$upstream_response_time '
                 'ua=$upstream_addr us=$upstream_status '
                 'cs=$upstream_cache_status';

access_log /var/log/nginx/access.log debug;

这几个字段的组合能直接回答关键问题:rt 远大于 urt 说明瓶颈在客户端或响应传输;urt 远大于 rt 说明是后端慢;us 与 $status 不一致说明 Nginx 对上游响应做了改写;ua 出现多个地址说明发生了 proxy_next_upstream 重试。据此可以用 awk 筛出耗时超过 5 秒的上游请求,或用 grep -o 'us=[0-9]*' ... | sort | uniq -c | sort -rn 统计上游状态码分布。

2. core dump 与 gdb 分析

一句话总结: worker 崩溃(segfault)时必须先拿到 core 文件,再用 gdb 分析调用栈;开启 core dump 需要同时配置系统 ulimit 与 worker_rlimit_core。

Nginx 的 master 进程很稳定,崩溃几乎都发生在 worker 上。worker 崩溃时会在 error_log 里留下 worker process ... exited on signal 11 的记录,但要定位原因必须有 core 文件:

# nginx.conf 中开启 core dump
worker_rlimit_core 500m;
working_directory /var/core/nginx;
# 系统层也要放开 core 大小限制
echo '/var/core/nginx/core.%e.%p' > /proc/sys/kernel/core_pattern
ulimit -c unlimited
# systemd 管理的服务需要在 unit 里设置 LimitCORE=infinity

注意 worker_rlimit_core 只对 worker 生效,master 与系统级限制同样要放开。另外 working_directory 必须存在且 worker 用户有写权限,否则 core 会写入失败且没有任何提示。

拿到 core 后,用与运行二进制完全一致的 Nginx 可执行文件(带调试符号)分析。gdb /usr/sbin/nginx /var/core/nginx/core.nginx.12345 载入后,bt 看完整调用栈、bt full 带局部变量、frame 3 切帧、info locals 看局部变量、print *r 与 print r->pool 查看请求与内存池。栈顶通常指向出错的模块:如果栈里有第三方模块的符号,优先怀疑该模块的空指针解引用;如果栈顶在 ngx_http_* 内部,则可能是配置导致的非法状态(比如 proxy_pass 的目标为空)。没有 core 时可以用 gdb -p $(pgrep -f 'nginx: worker' | head -1) 附加到存活的 worker,再执行 thread apply all bt 抓取现场。

对于内存泄漏(表现为 RSS 持续上涨),core 与 gdb 帮不上太多忙,需要用 pmap 观察哪个映射在持续增长(pmap -x <pid> | sort -k3 -n | tail -10),再用 valgrind 做一轮检测(valgrind --leak-check=full,会显著降低性能,只适合测试环境)。

3. 连接状态与 stub_status 排查

一句话总结: stub_status 给出 active/reading/writing/waiting 四个连接数,配合 ss 的连接状态分布,可以快速判断是连接堆积还是请求处理慢。

stub_status 是 Nginx 内置的最轻量监控接口:

location = /nginx_status {
    stub_status;
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
    access_log off;
}
curl -s http://127.0.0.1/nginx_status
# Active connections: 1024
# server accepts handled requests
#  1048576 1048576 5242880
# Reading: 2 Writing: 128 Waiting: 894

四个数字的解读是排错的关键:Active 是当前所有活跃连接,等于 Reading、Writing、Waiting 之和;Reading 是正在读取请求头的连接(正常应很小);Writing 是正在向客户端写响应的连接(大 = 响应发送慢或后端慢);Waiting 是空闲的 keepalive 连接(大 = 长连接池,通常正常)。

如果 Waiting 占绝大多数且 Active 接近 worker_connections × worker_processes,说明连接数吃紧,需要调大 worker_connections 或缩短 keepalive_timeout。如果 Writing 很高且持续,说明响应发送受阻——可能是客户端下载慢(移动网络),也可能是后端返回慢导致 Nginx 在等上游。配合系统工具可以进一步看连接状态分布:ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn 统计各状态数量,ss -tan state established '( dport = :8080 )' | wc -l 查看与上游的连接数,ss -tan state time-wait | wc -l 检查 TIME_WAIT 是否堆积(端口耗尽的前兆)。

TIME_WAIT 大量堆积说明连接没有被复用:可能是 upstream 缺 keepalive,也可能是 proxy_http_version 没有设为 1.1。SYN_RECV 堆积则说明有半连接攻击或上游响应握手过慢,需要检查 net.ipv4.tcp_syncookies 与 backlog 参数。

# 修复 TIME_WAIT 堆积的典型配置
upstream backend {
    server 10.0.0.11:8080;
    keepalive 64;                # 必须显式开启连接池
    keepalive_timeout 60s;
}
location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;      # HTTP/1.0 不支持 keepalive
    proxy_set_header Connection "";  # 清除 close,允许复用
}

4. 502/504/499 故障树

一句话总结: 502 是上游响应非法或连接失败,504 是上游超时,499 是客户端提前断开,三者根因完全不同,必须分别排查。

这三个状态码是 Nginx 排错中最高频的,把它们的判定条件与常见根因整理成故障树:

502 Bad Gateway
├── 上游进程挂了 / 端口未监听      → ss -tlnp 确认端口
├── 上游返回了非法响应(非 HTTP)  → 后端日志 + tcpdump
├── upstream 配置指向错误地址      → nginx -T 检查配置
├── 连接被拒绝(backlog 满)       → 调大 somaxconn / backlog
├── proxy_next_upstream 全部失败   → 所有后端都不健康
└── 响应头过大超缓冲区             → proxy_buffer_size 调大

504 Gateway Timeout
├── 后端处理确实慢(查后端耗时)   → 优化后端或异步化
├── proxy_read_timeout 太短        → 按业务 P99 调整
├── proxy_connect_timeout 太短     → 网络抖动时误判
├── 上游线程池耗尽导致排队         → 查后端并发上限
└── 网络丢包导致握手/传输慢        → mtr / tcpdump 验证

499 Client Closed Request
├── 客户端超时主动断开             → 客户端超时设置过短
├── 用户关页面 / 切网络            → 正常现象,看比例
├── 后端太慢导致客户端放弃         → 根因其实是后端慢
└── 中间设备(LB/CDN)断开         → 检查链路上游的超时

499 值得特别说明:它是 Nginx 特有的非标准码,表示客户端在服务端返回响应前就断开了连接。很多人把它当成 Nginx 的问题,实际上 499 的根因几乎总在别处——要么是客户端(或链路中间的负载均衡器)超时设置比 Nginx 短,要么是后端太慢导致客户端等不及。因此看到 499 要先问:是谁断开的?他的超时是多少?

排查时用 awk '$9 == 499 {print $7}' access.log | sort | uniq -c | sort -rn 统计 499 的请求路径分布,再用 awk '$9 == 499 {print $NF}' 看耗时分布:如果 urt 都很高,说明后端慢才是根因。

排查 502 的第一步永远是看 error_log 的具体错误信息,用 grep -E "connect\(\) failed|upstream timed out|no live upstreams|prematurely closed" /var/log/nginx/error.log 提取,不同的错误串指向不同根因:

connect() failed (111: Connection refused)  → 后端未监听,进程挂了
upstream timed out (110: ...) while reading → 后端处理超时,对应 504
no live upstreams while connecting          → 所有后端被标记失败
upstream prematurely closed connection      → 后端主动断连(崩溃/重启)
upstream sent invalid header                → 后端返回非 HTTP 内容

4.1 用 tcpdump 与 mtr 区分网络与后端问题

一句话总结: 当 error_log 指向连接层面时,用 tcpdump 看握手与响应,用 mtr 看链路丢包,才能确定是网络问题还是后端问题。

如果 error_log 显示 upstream timed out,需要在 Nginx 机器上抓包确认到底是「请求发出去了但没响应」还是「响应在网络上丢了」:

# 抓取与上游的交互观察时序,并统计重传(重传多说明链路丢包)
tcpdump -i any -nn -A 'tcp port 8080 and host 10.0.0.11' -c 200
tcpdump -i any -nn 'tcp port 8080' | grep -c retransmission

# 链路质量检测:逐跳统计丢包与延迟
mtr -rwzc 20 10.0.0.11

# 验证路径 MTU:1472 + 28 字节头 = 1500
ping -M do -s 1472 -c 3 10.0.0.11

如果抓包显示请求发出后没有任何响应且没有重传,说明后端收到了但处理慢(应用层问题);如果显示大量重传,说明网络链路有问题(可能是 MTU 不匹配、带宽饱和或设备丢包)。MTU 问题特别隐蔽:表现为小请求正常、大响应超时,用上面的 ping -M do 可以快速验证。

5. 常见性能陷阱

一句话总结: 大多数「Nginx 慢」都不是 Nginx 慢,而是配置触发了低效路径:磁盘 IO、缓冲策略、正则回溯或 DNS 解析。

陷阱一:proxy_buffering off 用错了地方。 关闭缓冲适合流式场景,但对普通响应会让 Nginx 变成「逐字节转发」,上游的连接被长期占用,吞吐大幅下降。默认应保持 proxy_buffering on:

# 正确:只对流式接口关闭缓冲
location /stream/ {
    proxy_buffering off;
    proxy_pass http://stream_backend;
}

陷阱二:location 正则过多导致匹配开销。 Nginx 的 location 匹配顺序是「精确 → 前缀 → 正则」,正则 location 按出现顺序逐个尝试。几十条正则 location 会让每个请求都跑一遍正则引擎,成为 CPU 热点:

# 反例:大量正则 location 逐个匹配
location ~ ^/api/v1/users { ... }
location ~ ^/api/v1/orders { ... }
# ... 几十条

# 正例:用前缀 location 收敛,正则只用于必要场景
location /api/v1/ {
    # 内部再用 map 或 rewrite 分流
}

陷阱三:proxy_pass 中带变量触发运行时 DNS 解析。 当 proxy_pass 的目标包含变量时,Nginx 无法在启动时解析域名,会在每个请求上做 DNS 查询(除非配置了 resolver 并配合变量缓存)。这是「配置看起来一样但性能差十倍」的经典原因:

# 反例:带变量但没配 resolver,每次请求都阻塞解析
set $backend "api.internal";
proxy_pass http://$backend;

# 正例一:用 upstream 块,启动时解析一次
upstream api { server api.internal:8080; }

# 正例二:必须用变量时,配置 resolver 并利用 DNS 缓存
resolver 10.0.0.2 valid=30s ipv6=off;
set $backend "api.internal";
proxy_pass http://$backend;

陷阱四:日志同步写盘。 access_log 默认是同步写,高 QPS 下磁盘 IO 会成为瓶颈。可以开启缓冲:

# 缓冲日志写入,批量刷盘
access_log /var/log/nginx/access.log main buffer=64k flush=5s;

陷阱五:gzip 对大文件与已压缩内容重复压缩。 对图片、视频做 gzip 纯属浪费 CPU:

# 只对文本类内容压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1024;      # 小于 1KB 不压缩
gzip_comp_level 4;         # 级别 6 以上收益很小但 CPU 开销陡增
# 定位 CPU 热点:用 perf 采样,看时间花在哪个函数
perf top -p $(pgrep -f 'nginx: worker' | head -1)
# 若 gzip 或正则相关函数占比高,说明命中了上述陷阱

6. 系统层与网络层排查

一句话总结: 接入层故障经常根源于系统层:文件描述符耗尽、端口耗尽、内核参数不当、磁盘 IO 饱和。

Nginx 报 too many open files 时,要同时检查 worker 与系统两级限制:

# 查看当前限制与实际使用量
cat /proc/$(pgrep -f 'nginx: worker' | head -1)/limits | grep "open files"
ls /proc/$(pgrep -f 'nginx: worker' | head -1)/fd | wc -l
# Nginx 侧的连接上限
events {
    worker_connections 10240;   # 每 worker 的连接数
}
# 配合 worker_rlimit_nofile,通常设为 worker_connections 的 2~4 倍
worker_rlimit_nofile 65535;

内核参数是另一个常见瓶颈。反向代理场景下,Nginx 同时作为客户端与服务器,会消耗大量本地端口,因此 ip_local_port_range 与 tcp_tw_reuse 需要调整:

# 反向代理场景的常用内核参数
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -w net.core.netdev_max_backlog=65535
sysctl -w fs.file-max=2097152
# 检查是否出现连接跟踪表满(容器与 NAT 场景常见)
dmesg | grep -i "nf_conntrack: table full"
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max

磁盘 IO 饱和会同时拖慢日志写入与缓存读写,用 iostat 观察:

# 观察磁盘利用率与等待队列
iostat -xz 1 5
# %util 接近 100% 或 await 明显升高说明磁盘是瓶颈

7. 建立排错方法论与预案

一句话总结: 排错效率来自「先取证后动手」的纪律与现成的诊断脚本,而不是临场的灵感。

排错时最容易犯的错误是「先改配置试试」。在没拿到证据前改动配置,会让现场消失,问题变得不可复现。正确顺序是:先取证(日志、core、抓包、指标快照)→ 再假设(对照故障树)→ 再验证(最小化改动)。

#!/usr/bin/env bash
# 一键取证脚本:故障时先跑这个,把现场保存下来
OUT=/tmp/nginx-diag-$(date +%s)
mkdir -p "$OUT"

cp /var/log/nginx/error.log "$OUT/"
tail -100000 /var/log/nginx/access.log > "$OUT/access-tail.log"
nginx -T > "$OUT/nginx-dump.conf" 2>&1
nginx -V > "$OUT/nginx-version.txt" 2>&1
curl -s http://127.0.0.1/nginx_status > "$OUT/stub-status.txt"
ss -tan > "$OUT/ss-tan.txt"
ss -s > "$OUT/ss-summary.txt"
ps -eo pid,ppid,pcpu,pmem,rss,etime,cmd | grep nginx > "$OUT/ps.txt"
dmesg | tail -200 > "$OUT/dmesg.txt"

echo "诊断数据已保存到 $OUT"

nginx -T 会 dump 出合并后的完整配置,这是排错的关键工具:很多问题源于 include 文件与主配置的交互,或者某个 location 里的指令覆盖了预期行为。nginx -T 输出的是 Nginx 实际生效的配置,比逐个看源文件可靠得多。

# 检查某个 location 的最终生效配置
nginx -T 2>/dev/null | awk '/location \/api\//,/^\t}/'

# 检查是否有重复定义(后者覆盖前者,容易造成困惑)
nginx -T 2>/dev/null | grep -c "server_name example.com"

最后是预案。把高频故障的处置步骤固化成手册,让任何值班同学都能在 5 分钟内做出正确反应:

【502 突增】
1. 跑取证脚本,保存现场
2. grep error.log 确认错误串类型
3. 若是 connect() failed → 检查后端进程与端口
4. 若是 no live upstreams → 检查后端健康状态与 max_fails 配置
5. 后端不可用时可临时摘除故障节点(改 upstream + reload)

【504 突增】
1. 查 urt 分布确认是否为普遍变慢
2. 抓包确认是后端慢还是网络慢
3. 紧急时可临时调大 proxy_read_timeout 并 reload
4. 记录慢请求样本,事后推动后端优化

【499 突增】
1. 查 499 的请求路径分布与 urt
2. 确认链路前端(LB/CDN)的超时设置
3. 若 urt 高则根因在后端,按 504 流程处理

8. 总结

环节要点
日志分级warn/error 为常态,info 用于排查,debug 需 –with-debug
日志字段记录 urt/ua/us,区分客户端慢与后端慢
崩溃分析worker_rlimit_core + ulimit + gdb bt,二进制须一致
连接排查stub_status 四类连接 + ss 状态分布
502上游连接失败或响应非法,看 error_log 错误串
504上游超时,用抓包与 mtr 区分后端慢与网络慢
499客户端提前断开,根因通常在后端慢或前端超时
性能陷阱缓冲策略、正则数量、变量 DNS、日志刷盘、gzip 范围

Nginx 排错的能力可以概括为一句话:知道去哪里找证据。日志字段告诉你耗时分布,error_log 的错误串告诉你失败类型,core dump 告诉你崩溃位置,抓包告诉你网络真相。把这四类证据用熟,绝大多数「玄学故障」都能在半小时内定位。至此,从 gRPC 代理、WAF 防护、边缘缓存,到零停机变更、模块开发与故障排查,接入层的完整能力图谱已经补齐,可以回过头把配置基线、性能调优与安全加固串成一套可运营的网关体系。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nginx」更多文章

  1. Nginx 在服务网格中的角色:边车代理、mTLS 与 Envoy 取舍
  2. Nginx 大文件上传与请求体处理:缓冲、临时文件与断点续传
  3. Nginx 证书自动化与 ACME:certbot、DNS-01 通配符与自动续期