线上Nginx频繁报错的排查思路和实战步骤

描述

 

一、问题背景

线上 Web 服务一上来就告警 Nginx 报错,这是日常运维里最常见的几类工单之一。我自己的经验里,超过一半的“Nginx 报错”最后根因都不在 Nginx 本身,而在上游(Tomcat / PHP-FPM / Java 应用 / 后端服务)、系统资源(FD 耗尽 / 端口耗尽)、TLS 握手或 reload 后的配置错位。Nginx 之所以频繁出现在告警里,只是因为它是入口、它是所有流量的第一个落点,真正的现场往往在更下游。

所以这次分享的并不是孤立的一个“报错怎么解”,而是我自己这些年在线上重复使用的一套排查套路。它可以套用在 502 / 504 / 499 / 500 / 502 Bad Gateway / 504 Gateway Timeout / connection reset / “upstream prematurely closed connection”等各种报错形态上。这一步做完之后,至少能稳定地把 7 成以上的 Nginx 故障收敛到一个具体原因,再去对症下药。

文章里所有命令都是我在生产环境(CentOS / RHEL / Ubuntu LTS)多次使用过的。涉及到具体版本差异的点(例如 nginx -V 输出中 OpenSSL 是否带 QUIC)我会单独标注。重点是让读者拿到一台陌生的 Nginx 服务器、几条报错日志,能照着这套流程一步步往下走,每一步都明确看什么指标、判断什么现象、下一步往哪里收敛。

二、适用场景

下面这些场景,是本文的明确覆盖范围,文中给出的命令、配置、回滚方法都按这些场景设计:

  1. 业务侧反馈“网站打不开”或者“偶发打不开”,Nginx error.log 大量出现 502 / 504。
  2. access.log  中 HTTP 状态码分布异常:例如 500 比例升高、499(客户端关闭)比例上升、连接被 reset。
  3. Nginx 主动返回错误页(如 502 Bad Gateway),但上游服务本身是“活”的。
  4. 间歇性 TLS 握手失败,浏览器报 ERR_SSL_PROTOCOL_ERRORSSL_ERROR_SYSCALL
  5. 重启过 Nginx 或滚动升级 binary 后报错突然变多,怀疑 reload 没生效。
  6. 运维侧的容量场景:Nginx 进程打开的 FD 接近 ulimit -n、TIME_WAIT / CLOSE_WAIT 异常堆积、新建连接排队。
  7. 上游 HTTPS / 自签证书 / SNI 不匹配导致握手失败:Nginx 反代后端时常见。

不适用场景(不要把本文当成万能药):

  • 业务代码本身抛 500,那是应用 bug,不是 Nginx 排错范畴。
  • DDoS / CC 攻击引发的带宽或连接数暴增,本文的“连接 / FD”章节有部分方法可复用,但攻击侧需要单独的限流与清洗流程。
  • Nginx 模块本身没编译进 Nginx(譬如 nginx-upload-module、GeoIP 模块缺失),这是构建期问题。

三、核心知识点

要把 Nginx 报错排得快,几个底层概念必须清楚,否则命令拿到也只是盲跑。

1. Nginx 的进程模型

Nginx 启动后是一个 master 进程 + 多个 worker 进程。master 只负责加载配置、管理 worker,平滑升级、平滑重启都靠它;worker 是真正处理请求的进程,监听共享端口(通过 SO_REUSEPORT 或共享 listen socket)。

排查时常用的几个进程事实:

  • master 进程 PID 通常存在 /var/run/nginx.pid(编译时可用 --pid-path 改)。
  • worker_processes  决定 worker 数,通常设为 CPU 核数或其倍数。
  • 每个 worker 默认最大连接数受 worker_connections 控制。
  • worker_rlimit_nofile  会覆盖 worker 进程的 ulimit -n(前提 master 有权限)。

理解这点对排查“连接打满”类故障有直接帮助:worker 是单进程多路复用,单 worker 实际可达上万连接,多 worker 乘起来就是 Nginx 整机并发上限。

2. Nginx 日志体系

Nginx 写两类日志:access.log 和 error.log。位置由 access_log 与 error_log 指令决定,默认在 /var/log/nginx/

error.log 有 8 个等级,从高到低:emergalertcriterrwarnnoticeinfodebug。线上排查最常用 warn 或 error 级别。emerg / alert / crit 几乎都会直接反映到具体故障。

access.log 的 main 格式默认包含 $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent",一般会再加 $request_time$upstream_response_time$upstream_addr 等字段做性能分析。

排查时最值得关注的几个 HTTP 状态码:

状态码 含义 排查方向
400 请求格式错误 客户端请求头异常 / Nginx 配置校验
499 客户端主动关闭 上游慢、客户端超时、Nginx 返回太慢
500 Nginx 自身捕获异常 内部错误,看 error.log
502 上游无应答或错应答 upstream 连不上 / 主动 reset
503 服务不可用(健康检查失败) upstream 健康状态、维护标记
504 上游响应超时 proxy_read_timeout 、上游排队

502 / 504 / 499 是线上报错率 Top 3,几乎所有“Nginx 报错”都可以先从这三个状态码切入。

3. upstream 与代理协议

Nginx 反代上游时的关键指令:

  • proxy_pass http://upstream_name;
  • proxy_connect_timeout :和上游建立 TCP 连接的超时(默认 60s)
  • proxy_send_timeout :请求发送到上游读超时(默认 60s)
  • proxy_read_timeout :从上游读响应的超时(默认 60s)
  • proxy_next_upstream error timeout http_502 http_503 http_504 :哪些情况下切到下一个上游
  • proxy_next_upstream_tries :重试次数(默认 0 表示无限制,按 proxy_next_upstream 决定)
  • keepalive (在 upstream 块中):连接池,会显著减少和上游频繁新建连接的开销

502 / 504 大量出现时,几乎一定会动这几个参数或看它们的日志。

4. 系统层 FD / 端口 / socket

Nginx 上游连接、客户端连接、文件读写,最终都映射成 Linux 进程的文件描述符(FD)。Nginx 任何一个 worker 进程 FD 耗尽,就表现为新连接被拒绝、connect() failed (24: Too many open files)

同一时刻系统层面:

  • TCP 半连接队列(net.ipv4.tcp_max_syn_backlog)满了,新连接会被丢弃
  • accept 队列(net.core.somaxconn / 实际 backlog)满了,新连接会被回 RST
  • TIME_WAIT 数量巨大(ss -s)虽然不影响收新连接,但大量占用 ephemeral 端口

线上真实场景里,重启 master + worker 后报错“瞬间恢复”,很大概率就是 FD 资源被泄漏或耗尽。

5. SSL / TLS 与 SNI

Nginx 在做 HTTPS 反代,或者反代到上游 HTTPS 时,可能遇到 SSL 握手失败。常见原因:

  • 上游证书是自签、或证书过期(x509: certificate has expired
  • SNI 不匹配(Nginx 用了 proxy_ssl_server_name on; 但上游 virtualhost 与请求域名不同)
  • 协议不匹配(如 TLS1.0 强行协商)
  • 上游协议升级但 Nginx 端用的还是老 OpenSSL(QUIC / HTTP/3 场景)

排查时 nginx -V 看编译参数,再结合 error.log 关键字(如 SSL_do_handshake() failedSSL_CTX_use_PrivateKey),可以快速确认握手在哪一步失败。

四、整体排查思路

我习惯把排查分四层,从最靠近用户的地方往最底层走,每一层都有明确“能结束”这一层的指标:

第一层:确认报错是不是真在 Nginx

  • 看 error.log 最近一分钟的报错热点。
  • 看 access.log 最近 1 分钟 / 5 分钟的状态码分布。
  • 看监控上 Nginx 实例的 5xx 比例 / 响应时间曲线是否有突变。

第二层:缩小范围,区分是上游还是 Nginx 自身

  • 502 / 504 / upstream prematurely closed:上游层
  • connection refused connect() failed (111: ...):上游层(连接被拒)
  • no live upstreams  / upstream timed out:上游层
  • Too many open files worker process xxx exited on signal:Nginx / 系统层
  • worker process is not allowed to use ... :Nginx / 系统层(apparmor / SELinux 场景)

第三层:深入到具体组件

  • 上游层:upstream 健康状态、后端进程 / FD / 端口 / TLS
  • Nginx 自身层:worker 数量、FD、reload 状态、配置 include 顺序
  • 系统层:ulimit -n、端口分配、TCP 状态、swap / OOM
  • 网络层:专线抖动、MTU、VPC 安全组规则、VLAN Tag 等

第四层:定位根因,做变更

每做一次配置变更之前,都强制要求:

  • 在另一台机器或同一个 LB 后面其它副本上做灰度。
  • 保留最近一份可用配置 nginx.conf.bak.YYYYMMDD-HHMMSS(脚本里用 $(date +%Y%m%d-%H%M%S) 拼好后缀)。
  • 有一份能在 5 分钟内回滚的命令或脚本。

任何不满足这三点的变更都不要在生产上直接执行。

五、实战步骤

下面这些步骤,是我自己在生产环境里最常用的顺序。不是硬性模板,但每一步的“目的 / 命令 / 观察指标 / 判断逻辑 / 下一步动作”都明示,读者照着照搬可行。

步骤 1:先确认报错是不是确实集中在 Nginx

目的:避免误判,把“业务报错”误归到 Nginx。

命令


			   bash# 看最近 error.log 的告警等级分布 tail -n 1000 /var/log/nginx/error.log | awk -F'[][]' '{print $4}' | sort | uniq -c | sort -rn | head -20 

预期输出:常见的是 [error] / [warn] 大量;少量 [emerg] / [alert] 的情况更要重点关注。

异常表现

  • [crit]  出现:SSL_CTX_use_PrivateKey_file(...) failedinvalid PID number 等,都是配置或证书层问题,立刻停下,看步骤 5。
  • [emerg]  出现:基本是 master 启动失败或严重配置错误,必须立即看。
  • 整段时间 [error] 都没有,但业务说 502:大概率是上游层问题,不是 Nginx。

判断逻辑:error.log 完全没有相关关键字、Nginx 进程是 alive 的情况下,把方向转向 upstream。

下一步动作:进入步骤 2,看 access.log 的状态码分布。

步骤 2:拉最近 5–10 分钟 access.log 的状态码分布

目的:确认报错到底是哪一类 HTTP 状态码,是 502、504、500 还是 499 之类。

命令


			   bash# 统计最近 5 分钟的状态码分布(按秒切片再看聚合) cd /var/log/nginx tail -n 50000 access.log | awk -F'"' '{print $2}' | awk '{print $2}' | sort | uniq -c | sort -rn | head 

预期输出(举例):


			     31200 200    1840 502     420 504      80 499 

异常表现

  • 502 / 504 突然变多:上游层问题,进步骤 3。
  • 499 比例飙高:客户端主动断开,上游慢或者客户端超时太短,进步骤 5 的 proxy_read_timeout 调优。
  • 500 全是 Nginx 主动返回:Nginx 自身层,看步骤 4。

判断逻辑:根据状态码分布反推方向。报错率低于总请求 0.1% 时观察一段时间即可,高于 1% 通常就要立即介入。

下一步动作:进步骤 3,看上游是不是“真的活着”。

步骤 3:直接验证上游健康状态

目的:把上游层问题从“猜”变成“看”。

命令


			   bash# 先看 Nginx upstream 块定义里那些 server grep -A 10 'upstream backend' /etc/nginx/conf.d/*.conf /etc/nginx/nginx.conf 2>/dev/null | head -40 # 然后从 Nginx 主机直接 curl 上游端口 for ip in $(awk '/upstream/ {flag=1; next} flag && /server/ {print $2}' /etc/nginx/nginx.conf | awk -F: '{print $2}'); do   echo "=== probe $ip ==="   curl -sS -o /dev/null -w 'http_code=%{http_code} time_total=%{time_total} '      --max-time 5 "http://$ip/healthz" || true done 

预期输出:每个上游都返回 200,且 time_total 在一个稳定区间。

异常表现

  • time_total  5s 已经到 timeout:上游响应慢;检查上游进程是不是在堆 dump(jstack)或者被 GC 拖累。
  • connect refused :上游进程没起来,或者只监听了 127.0.0.1
  • time_total  波动大(几十毫秒到几秒):网络层或后端压力。

判断逻辑:所有上游都不可用 → 是后端集群问题;只有部分不可用 → 是某个机房 / 实例问题;都活着 → 看步骤 4,怀疑 Nginx 到上游这一跳。

下一步动作:进入步骤 4,看 Nginx 到上游的连接数与 FD。

步骤 4:检查 Nginx 到上游的连接数与 FD

目的:判断是不是 Nginx 实例到上游的连接被打满、或 FD 耗尽。

命令


			   bash# 拿 master pid master_pid=$(cat /var/run/nginx.pid 2>/dev/null || pgrep -f 'nginx: master' | head -1) echo "master pid = $master_pid" # 每个 worker pid + FD 数量 for wpid in $(pgrep -f 'nginx: worker'); do   fd=$(ls -1 /proc/$wpid/fd 2>/dev/null | wc -l)   echo "worker pid=$wpid fd=$fd limit=$(cat /proc/$wpid/limits | awk '/open files/{print $4}')" done # Nginx 整体 TCP 连接状态(按端口) ss -s ss -antp | grep ':80 ' | awk '{print $1}' | sort | uniq -c ss -antp | grep ':443' | awk '{print $1}' | sort | uniq -c # 看 nginx 进程 打开了多少 socket lsof -p $master_pid -a -i 2>/dev/null | head 

预期输出(举例):


			   master pid = 12345 worker pid=12346 fd=850 limit=65535 worker pid=12347 fd=920 limit=65535 ... TCP:   active (opening) 5 / passive (closing) 12 / established 2300 / ...        1500 ESTAB         800 TIME-WAIT 

异常表现

  • worker 的 FD 已经接近或达到 limit,且 open files 是 soft 限制(默认 1024),就要升 worker_rlimit_nofile 和系统 ulimit -n
  • ESTABLISHED 远大于正常值(比如一个实例上万条都在建立中),怀疑连接泄漏或上游慢,Nginx 一直在等。
  • TIME-WAIT 比例异常高:可能是短连接场景、上游 keepalive 没启用。

判断逻辑:FD 接近 limit → 系统资源问题;连接数远大于合理值 → 上游慢或客户端异常。

下一步动作:进入步骤 5,验证 upstream 配置和 keepalive。

步骤 5:把方向收敛到 upstream 段配置 / keepalive / 超时

目的:检查 upstream 段配置是否合理、有没有启用 keepalive。

命令


			   bash# 看实际生效的 upstream 配置 nginx -T 2>/dev/null | awk '/upstream /,/^}/' nginx -T 2>/dev/null | grep -E 'proxy_(connect|send|read)_timeout|proxy_next_upstream|keepalive' 

预期输出


			   nginxupstream backend {     server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;     server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;     keepalive 32; } ... proxy_connect_timeout 5s; proxy_send_timeout 30s; proxy_read_timeout 60s; proxy_next_upstream error timeout http_502 http_503 http_504; 

异常表现

  • 没有 keepalive:每次转发都新建 TCP 连接,上游会被打爆。一般 Java / Go 服务端反代都要开。
  • max_fails=3  配 fail_timeout=10s:上游只要连续 3 次失败 10s 就被踢出轮询,太敏感,对短促抖动不友好。
  • proxy_read_timeout  太短(默认 60s 够用,刻意调小比如 10s):长请求会被 504。建议 30–120s 之间按业务调整。
  • 没有 proxy_next_upstream:上游一次失败就返回错误,不能容灾。

判断逻辑:缺哪一项补哪一项。每补一项都要进步骤 8 的验证:先 reload,再观察状态码变化。

下一步动作:进步骤 6,看最近的 reload 是否真的成功。

步骤 6:确认 reload 是不是真的成功

reload 失败但 Nginx 仍在跑老配置,是线上很常见的“假配置”来源。

命令


			   bash# 验证配置语法 nginx -t # 平滑 reload nginx -s reload # 确认进程是否真的切换过 ps -ef | grep -E 'nginx:.*(master|worker)' | grep -v grep # 看新 worker 的启动时间(老 worker 的启动时间是最早 reload 的时候) ls -l /proc/$(pgrep -f 'nginx: master' | head -1)/cwd 2>/dev/null for pid in $(pgrep -f 'nginx: worker'); do   echo "worker pid=$pid start_time=$(ps -o lstart= -p $pid)" done 

预期输出:reload 后会有新 worker 时间更新,旧 worker 进程在 graceful 退出。

异常表现

  • nginx -t  报错但 reload 还是执行了:实际进程没有重载,新配置被丢弃。
  • reload 后 worker 启动时间没刷新:可能是 systemd 配置错误,或者二进制替换但 PID 没指向新 master。
  • 只剩 1 个 worker:worker_processes 被改成了 1。
  • 旧 worker 一直没退出:shutdown_timeout 设得太长,业务流量切走,但旧 worker 还占用资源。

判断逻辑:reload 之后必须能拿出明确的“新 worker 时间戳”作为证据。否则任何配置优化都是无效操作。

下一步动作:进入步骤 7,验证 Nginx 实例层面是否有限制。

步骤 7:检查系统层 / 进程层资源限制

目的:当 Nginx 报错堆里能看到资源类关键字,要快速翻一遍 systemd unit、ulimit、内核 TCP 参数。

命令


			   bash# systemd unit 里的限制 systemctl show nginx | grep -E 'LimitNOFILE|LimitNPROC|Restart|Environment' # 进程的实际限制 cat /proc/$(pgrep -f 'nginx: master' | head -1)/limits # 系统的 nofile ulimit -n # TCP 状态计数 cat /proc/net/snmp | grep '^Tcp:' | head -1 # 内核 somaxconn、syn backlog sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.ip_local_port_range 

预期输出(举例):


			   LimitNOFILE=131072 LimitNPROC=16384 

异常表现

  • LimitNOFILE=65536  但 worker 实际 FD 接近 1024:master 和 worker 都没继承到 systemd 的 limit,需要排查 unit 文件或启动方式。
  • somaxconn=128 :accept 队列偏小,新连接会丢。
  • tcp_max_syn_backlog=128 :同样偏小,SYN 包可能被丢。

判断逻辑:这些值偏小但请求量大的时候,必然出错。一般生产建议:

  • LimitNOFILE >= 100000
  • somaxconn >= 4096
  • tcp_max_syn_backlog >= 4096
  • ip_local_port_range  不要太小

下一步动作:进步骤 8,做配置变更并验证。

步骤 8:变更与验证(这一节就是动手的部分)

目的:把上面分析出的变更落到配置,并验证。

具体配置示例、变更步骤、回滚方案、风险点都会集中放在“配置示例”和“回滚方案”章节,避免在这一节重复。

实战中的几条要点:

  1. 改任何一段配置前,必须 cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%Y%m%d-%H%M%S)
  2. 改完必须先 nginx -t,不要跳过语法检查。
  3. reload 时观察 error.log,reload 报错立刻回滚。
  4. 验证不只看当前 30 秒,至少要拉 5 分钟、15 分钟、1 小时三个时间窗口的状态码分布,确认没有回弹。

六、常用命令

下面是排查过程中随时会用到的命令汇总,按用途归档。读者可以把这部分单独收藏。

1. Nginx 自身


			   bash# 查看编译参数(最关键) nginx -V 2>&1 | head # 测试配置语法 nginx -t # 输出完整生效配置 nginx -T 2>/dev/null | less # 平滑 reload nginx -s reload # 修改二进制但保留 master 进程(用于升级) kill -USR2 $(cat /var/run/nginx.pid) # 回滚 binary kill -USR2 $(cat /var/run/nginx.pid) # 标准切换:USR2 拉新 master,WINCH 让老 master 的 worker 优雅退出 # 如发现新 master 不稳定: #   kill -USR1 $(cat /var/run/nginx.pid)   # 切回旧 master #   kill -QUIT $(cat /var/run/nginx.pid)   # 再退出新 master 

2. 日志与状态码


			   bash# error.log 等级分布 tail -n 5000 /var/log/nginx/error.log | awk -F'[][]' '{print $4}' | sort | uniq -c | sort -rn # access.log 状态码分布 tail -n 100000 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn # 状态码分布(带客户端 IP) tail -n 100000 /var/log/nginx/access.log | awk '{print $1, $9}' | sort | uniq -c | sort -rn | head # TOP 50 慢请求(如果日志含 $request_time) awk '{print $NF,$0}' /var/log/nginx/access.log | sort -rn | head -50 # TOP 50 上游响应慢(如果日志含 $upstream_response_time) awk '{print $(NF-1),$0}' /var/log/nginx/access.log | sort -rn | head -50 

3. 进程 / FD / 连接


			   bash# Nginx 进程整体限制 cat /proc/$(pgrep -f 'nginx: master' | head -1)/limits # 每个 worker FD 数量 for pid in $(pgrep -f 'nginx: worker'); do   fd=$(ls -1 /proc/$pid/fd 2>/dev/null | wc -l)   echo "worker pid=$pid fd=$fd" done # TCP 连接状态 ss -s ss -antp | grep ':80 ' | awk '{print $1}' | sort | uniq -c ss -antp | grep ':443' | awk '{print $1}' | sort | uniq -c # 当前 Nginx 上的 socket lsof -p $(pgrep -f 'nginx: master' | head -1) -a -i 2>/dev/null | head 

4. 上游探活


			   bash# 简单 curl curl -sS -o /dev/null -w 'http_code=%{http_code} time_total=%{time_total} ' --max-time 5 http://10.0.0.11:8080/healthz # 模拟 Nginx 视角,强制走 SNI(如果上游是 HTTPS) curl -v --resolve example.com10.0.0.11 https://example.com/ -o /dev/null --max-time 5 # 通过 Nginx 自身调用 upstream curl -sS -H "Host: app.example.com" http://127.0.0.1/healthz --max-time 5 

5. 系统资源


			   bashulimit -n ulimit -u sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.ip_local_port_range net.ipv4.tcp_tw_reuse free -h df -h 

6. 抓包与握手


			   bash# 只抓端口 443 的握手(注意生产慎用) timeout 30 tcpdump -i any -nn -s 0 -w /tmp/ssl.pcap 'tcp port 443 and (tcp[tcpflags] & tcp-syn!=0)' # 再用 openssl s_client 复测 openssl s_client -connect example.com:443 -servername example.com -tls1_2 

七、配置示例

下面给出一段较完整的 /etc/nginx/nginx.conf 关键段,包含几个针对频繁报错场景的高频开关。生产环境里要把这些配置按真实路径落地,示例都给了文件路径和说明。

1. main 段:worker 数量、FD 上限

文件:/etc/nginx/nginx.conf → main {}


			   nginxworker_processes auto;            # 通常与 CPU 核数对齐 worker_rlimit_nofile 100000;      # 把 worker 的 nofile 提到较大值 pid /var/run/nginx.pid; error_log /var/log/nginx/error.log warn; 

2. http 段:基础开关

文件:/etc/nginx/nginx.conf → http {}


			   nginxhttp {     include       mime.types;     default_type  application/octet-stream;     log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '                       '$status $body_bytes_sent "$http_referer" '                       '"$http_user_agent" "$http_x_forwarded_for" '                       'rt=$request_time uct=$upstream_connect_time '                       'urt=$upstream_response_time ua=$upstream_addr';     access_log /var/log/nginx/access.log main buffer=32k flush=5s;     sendfile        on;     tcp_nopush      on;     tcp_nodelay     on;     keepalive_timeout  65;     open_file_cache max=10240 inactive=20s;     open_file_cache_valid 30s;     open_file_cache_min_uses 2;     server_tokens off;             # 关闭版本号,避免泄露 } 

3. upstream 段:keepalive + 健康检查参数

文件:/etc/nginx/conf.d/backend.conf


			   nginxupstream backend {     least_conn;     server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;     server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;     keepalive 64;     keepalive_requests 1000;     keepalive_timeout 60s; } 

4. server 段:proxy 超时 / 缓冲 / body 大小

文件:/etc/nginx/conf.d/app.conf


			   nginxserver {     listen 80;     server_name app.example.com;     access_log /var/log/nginx/app.access.log main;     error_log  /var/log/nginx/app.error.log warn;     client_max_body_size      50m;     client_body_buffer_size   128k;     client_header_buffer_size 1k;     large_client_header_buffers 4 8k;     location / {         proxy_pass         http://backend;         proxy_http_version 1.1;         proxy_set_header   Connection          "";         proxy_set_header   Host                $host;         proxy_set_header   X-Real-IP           $remote_addr;         proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;         proxy_set_header   X-Forwarded-Proto   $scheme;         proxy_connect_timeout 5s;         proxy_send_timeout    30s;         proxy_read_timeout    60s;         proxy_next_upstream error timeout http_502 http_503 http_504;         proxy_next_upstream_tries 2;         proxy_buffering on;         proxy_buffer_size 8k;         proxy_buffers 16 16k;         proxy_busy_buffers_size 32k;         proxy_request_buffering on;     } } 

5. HTTPS 反代:上游 HTTPS / SNI / 证书

文件:/etc/nginx/conf.d/app_tls.conf


			   nginxserver {     listen 443 ssl;     server_name app.example.com;     ssl_certificate     /etc/nginx/ssl/app.example.com.crt;     ssl_certificate_key /etc/nginx/ssl/app.example.com.key;     ssl_protocols       TLSv1.2 TLSv1.3;     ssl_ciphers         HIGH!MD5;     ssl_session_cache   shared10m;     ssl_session_timeout 1d;     ssl_stapling        on;     ssl_stapling_verify on;     location / {         proxy_pass https://backend_tls;         proxy_ssl_server_name on;        # 强制带上 SNI         proxy_ssl_protocols   TLSv1.2 TLSv1.3;         proxy_ssl_ciphers     HIGH!MD5;         proxy_ssl_verify      off;       # 上游证书自签场景关闭校验         # proxy_ssl_verify    on;         # proxy_ssl_trusted_certificate /etc/nginx/ssl/upstream_ca.crt;         proxy_http_version 1.1;         proxy_set_header Host $host;     } } upstream backend_tls {     server 10.0.0.11:8443;     keepalive 32; } 

6. systemd 限制

文件:/etc/systemd/system/nginx.service.d/override.conf


			   ini[Service] LimitNOFILE=131072 LimitNPROC=65535 LimitCORE=infinity 

修改后必须 systemctl daemon-reload && systemctl restart nginx。但实际环境里更推荐先用 systemctl edit nginx 创建 override,避免改掉包自带的 unit。

7. sysctl

文件:/etc/sysctl.d/99-nginx.conf


			   bashnet.core.somaxconn = 4096 net.ipv4.tcp_max_syn_backlog = 4096 net.ipv4.ip_local_port_range = 1024 65000 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_keepalive_time = 600 net.ipv4.tcp_keepalive_intvl = 30 net.ipv4.tcp_keepalive_probes = 3 fs.file-max = 2097152 fs.nr_open  = 1048576 

应用:sysctl -p /etc/sysctl.d/99-nginx.conf

八、日志或指标观察方法

光看 error.log 还不够,配合指标才能把报错“定位到层”。这里给出标准观测法。

1. Nginx 层的指标

不是每次都能接外部采集器,至少用 stub_status / 自带 metrics 拉几个核心指标:


			   nginxlocation /nginx_status {     stub_status;     allow 127.0.0.1;     deny  all; } 

curl http://127.0.0.1/nginx_status 返回:


			   Active connections: 230 server accepts handled requests  12345 12344 256000 Reading: 5 Writing: 12 Waiting: 213 

关键观察点:

  • Active connections  持续超过 worker_processes * worker_connections,意味着接近容量上限。
  • Reading  多、Writing 少:上游慢、客户端多在等返回。
  • accepts  远大于 handled:连接被 RST 掉,可能到达 syn/accept 队列上限。

2. access.log 指标切片

日常观察的常用切片:


			   bash# 最近 1 分钟状态码分布 awk '{print $4}' /var/log/nginx/access.log | awk -F: '{print $2":"$3}' | tail -60 | sort | uniq -c | head # 异常 502 集中在哪个上游 IP awk '{print $NF,$0}' /var/log/nginx/access.log | grep ', 502 ' | awk '{print $1}' | sort | uniq -c | sort -rn # 慢请求切片($request_time 超过阈值) awk -F'rt=' '{print $2}' /var/log/nginx/access.log | awk '{print $1}' | awk '$1 > 1 {print}' | sort -rn | head 

3. 系统指标


			   bash# CPU 负载 uptime top -bn 1 | head -20 # 内存 free -h # FD 与端口 ss -s ss -antp | grep -E 'TIME-WAIT|CLOSE_WAIT' | wc -l # 错误关键字 dmesg -T | grep -i 'oom|killed process|nginx' | tail 

4. 上游指标

如果上游是 Java 应用,注意:

  • jstat -gc 1000  看 GC 频率
  • jstack  看有没有线程阻塞在 socket read
  • 是否有 Too many open files 应用侧日志

如果上游是 PHP-FPM:

  • cgi-fcgi -connect 10.0.0.11:9000  看 FPM 状态
  • 看 pm.max_childrenpm.start_servers 配置和实际活跃进程数

5. 指标与告警

最低限度,线上 Nginx 实例应该有以下告警项(结合实际业务基线调整阈值):

  • 5xx 比例(按 1 分钟 / 5 分钟)超过业务基线
  • active connections 突增或保持高位
  • error.log 中特定关键字频率升高(upstream timed outSSL_do_handshake() 失败等)
  • worker 进程异常重启(grep worker process /var/log/nginx/error.log
  • ss -s  中 passive 中的 active opening 持续高于基线

九、排查路径

把上面所有步骤汇总成一个可执行路径树。这个树在生产 IM 群里直接发也基本可读,新人照着走没问题。


			   现象:业务报“网站打不开 / 慢 / 502 / 504”   │   ├─ 步骤 1:error.log 关键词定位   │    ├─ emerg/alert/crit  → 配置 / 证书 / 启动层,停手去查步骤 6/7   │    └─ error/warn        → 步骤 2   │   ├─ 步骤 2:access.log 状态码   │    ├─ 502/504/499 大量 → 步骤 3   │    └─ 500 / 其它  → 步骤 4 / 5   │   ├─ 步骤 3:上游探活   │    ├─ 所有上游失败  → 后端集群问题(不在本文范围)   │    ├─ 部分失败      → 网络 / 机房层   │    └─ 全部活着      → 步骤 4   │   ├─ 步骤 4:Nginx-上游连接 / FD   │    ├─ FD 接近 limit   → systemd / ulimit   │    ├─ 连接数异常       → keepalive / 慢上游   │    └─ 正常            → 步骤 5   │   ├─ 步骤 5:upstream 段配置   │    ├─ 缺 keepalive → 加上   │    ├─ timeout 不合理 → 调整   │    └─ proxy_next_upstream 没开 → 开启   │   ├─ 步骤 6:reload 是否真生效   │    ├─ worker 时间戳未变 → 重新 reload / 排查 unit   │    └─ 时间戳已更新 → 步骤 7   │   ├─ 步骤 7:系统层资源   │    ├─ ulimit 偏小 → 调整   │    ├─ somaxconn / syn_backlog 偏小 → 调整   │    └─ 正常 → 步骤 8   │   └─ 步骤 8:变更 + 验证        ├─ 小流量灰度验证        ├─ 5/15/60 min 三窗口观察状态码        └─ 稳定无回弹 → 进入复盘 

十、风险提醒

下面这些是我踩过坑的风险点,做运维变更前必须先把对应的处置准备好。

  1. reload 不算“重启”,但有时候不算“reload” 。我见过有人改了 /etc/nginx/conf.d/ 下的 include 文件,但没有用 nginx -s reload,结果是 Nginx 仍按旧配置在跑。流程上必须把 reload 列为配置变更的强制动作。
  2. reload 期间 worker 进程数变化 。reload 老的 worker 不会立刻退出,shutdown_timeout 没设的情况,要看 graceful 关闭时长是否覆盖新流量。
  3. 二进制升级(binary upgrade)顺序搞反,会丢请求 。必须遵循:USR2 启动新 master → USR1 给老 master 发请求切到新 master → WINCH 让老 worker 退出。
  4. server_name 多个匹配时,最后一个胜出 include 顺序错了,证书也会错。
  5. proxy_next_upstream 在 POST 大请求场景下,重试可能造成业务重复提交 。建议对带写操作的接口关闭 http_502/503 之外的触发条件,或仅超时类触发。
  6. access_log buffer=... 调太大,可能丢日志 。buffer 没写盘但 worker 异常退出时会丢,慎用。
  7. L4 反代到 L7 反代链路上,客户端源 IP 会丢 X-Forwarded-For 不开或信任错了节点,会让上游看到错的 IP,影响排障。
  8. HTTPS + HSTS + 301 跳转三元组要一致 。配置稍不一致,浏览器会反复报 mixed content。
  9. open_file_cache  打开过大会吃内存,按业务实际静态文件量调。
  10. geoip 模块加载没加 就用了 geoip_city,启动会失败。这点主要在编译期避免,但偶尔会因为动态模块没载入出现。

十一、验证方式

不论做什么变更,验证都得能列出明确的“通过 / 失败”标准。

验证 1:reload 是否生效


			   bashnginx -t && nginx -s reload ps -ef --forest | grep nginx # 期望:master + 新 worker,worker 的 lstart 是 reload 时间之后 

验证 2:状态码分布改善

取最近 5 分钟状态码分布,对比变更前:


			   bash# 变更后采样 tail -n 50000 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn # 对比变更前的样本 

502 / 504 比例应当下降而不是简单从绝对值看。生产建议:5 分钟窗口 5xx 比例回到基线 ±50% 内。

验证 3:上游连接数稳定


			   bashss -antp | grep ':8080' | wc -l 

变更后保持稳定不抖动。

验证 4:FD 占用


			   bashfor pid in $(pgrep -f 'nginx: worker'); do   fd=$(ls -1 /proc/$pid/fd 2>/dev/null | wc -l)   echo "worker pid=$pid fd=$fd" done 

不要超过 systemd LimitNOFILE 的 70%。

验证 5:TLS 握手可用


			   bashopenssl s_client -connect app.example.com:443 -servername app.example.com -tls1_2 /dev/null | grep -E 'Verify return code|Protocol' # 期望:Protocol : TLSv1.2 或 TLSv1.3,Verify return code: 0 

验证 6:版本和模块


			   bashnginx -V 2>&1 | grep -E '^configure arguments' | tr ' ' ' ' | grep -E 'http_ssl_module|stream_module|with-http_v2_module' 

变更过二进制或重编译后必须确认模块还在。

十二、回滚方案

任何变更都必须能在 5 分钟内回滚。下面是几种典型变更的回滚方法。

回滚配置


			   bash# 最近一份配置 ls -lt /etc/nginx/nginx.conf.* | head -5 # 直接替换回去(命名约定:nginx.conf.bak.YYYYMMDD-HHMMSS) BACKUP=$(ls -t /etc/nginx/nginx.conf.bak.* 2>/dev/null | head -1) if [ -n "$BACKUP" ]; then   cp -a "$BACKUP" /etc/nginx/nginx.conf fi nginx -t && nginx -s reload 

兜底命令——回滚之后做一次强制 reload:


			   bashnginx -t && nginx -s reload 

回滚二进制


			   bash# 找老 binary ls -lt /usr/sbin/nginx-* | head -5 cp -a /usr/sbin/nginx-1.22.1 /usr/sbin/nginx nginx -t && kill -USR2 $(cat /var/run/nginx.pid) # 然后按 USR2 -> USR1 -> WINCH 的顺序切换 

回滚 systemd unit


			   bashsystemctl revert nginx   # 回滚 daemon-reload 之后的 unit 修改 # 或恢复 override.conf cp -a /etc/systemd/system/nginx.service.d/override.conf.bak        /etc/systemd/system/nginx.service.d/override.conf systemctl daemon-reload && systemctl restart nginx 

回滚 sysctl


			   bashsysctl -p /etc/sysctl.conf # 或撤销指定参数 sysctl -w net.core.somaxconn=128 

执行回滚后必须重新进入“验证方式”一遍流程,确认报错不再上升。

十三、生产环境注意事项

下面这几条是上线新 Nginx、反代新上游、改 TLS 时的硬性要求,建议在团队 wiki 沉淀成 checklist。

  1. 任何 reload / restart 都要观察 30 秒以上 。我把这一条放在第一位是因为它最容易被忽略。
  2. 生产 binary 升级前先在测试环境做一次完整 reload / restart 演练 ,并且要让测试环境开启和线上完全一样的 worker_processes / worker_connections
  3. 配置变更一律走版本管理 。即便只改一行,也要在 git 里 commit,然后用 conf.d/ 下的 *.conf 文件或主 nginx.conf 中 include 引入。
  4. reload 高峰尽量避开业务高峰 。reload 本身不耗资源,但触发之后的短时间内,worker ramp-up 可能让连接分配不均。
  5. 批量重启必须做灰度 。先把第一台改成观察,再批量推到其余副本。
  6. 任何对外公开的端口 / TLS 配置变更,要在变更通告群里同步
  7. 日志级别不要长期开 debug error_log ... debug 会记录每个请求的 header,生产打爆磁盘。
  8. proxy_buffer_size / proxy_buffers 不应盲目抄网上配置 ,应按 body_bytes_sent 分布调整。
  9. 对外暴露的 nginx_status 务必要 deny all + allow 监控网段 。它会泄露活动连接数。
  10. server_tokens off 一定开 。泄露版本号等于给扫描器提供现成 CVE 表。
  11. client_max_body_size  按业务需求设,不要给 0(不限制)。
  12. 日志切割 + 老日志压缩归档 。Nginx 自身的 logrotate.d 配置要确认能跑,避免磁盘被 access.log 打爆。

十四、总结

把这一次的排错经验归纳一下,三件事最重要:

  • 从 error.log 关键字 + access.log 状态码 + upstream 探活 + worker FD + 系统层限制五个维度同时取证 ,不要只盯一层。
  • 每一步变更都要有证据链 :reload 之前看时间戳、reload 之后看状态码、keepalive 开启之后看连接数、TLS 改完之后用 openssl s_client 实测。
  • 回滚路径一定要有,而且要在变更前验证过 。线上事件里大量二次伤害都来自于做了错误的变更之后又不知道怎么回到现状。

下一步如果要把这套流程变成团队级的能力,建议把它包装成一个 playbook / runbook:把命令写好、把变量(如 pid、上游网段)做成可参数化的脚本,接到告警里让它半自动跑。我自己目前在阿里云 / 自建机房两套环境上都还保留这一套模板的实例,配上 Alertmanager 做 7×24 半自动响应,新人上手一个季度内基本就能独立处理 7 成以上的“Nginx 报错”类工单。

附录 A:TLS 握手失败深排查

线上最容易被当成“Nginx 报错”的,其实是 TLS 握手失败。客户端在浏览器里看到 ERR_SSL_PROTOCOL_ERRORERR_SSL_BAD_RECORD_MAC_ALERTSSL_ERROR_SYSCALL 等一系列报错,第一反应都是“是不是 Nginx 出问题”。所以把 TLS 这一条单独拆出来讲一讲。

A.1 Nginx 反代上游 HTTPS 时握手失败的常见表现

Nginx error.log 常见关键字:


			   SSL_do_handshake() failed (SSL: errorSSL routinessslv3 alert handshake failure SSL_do_handshake() failed (SSL: errorSSL routinesinternal error SSL_CTX_use_PrivateKey_file("/etc/nginx/ssl/server.key") failed SSL: errorPEM routinesno start line 

对应到根因:

关键字 直觉指向 真正根因
alert handshake failure 上游业务侧 上游 virtualhost 没匹配 SNI,证书也未匹配
key exchange: internal error OpenSSL 不兼容 OpenSSL 版本太老 / 太新,或 cipher suites 被禁用
SSL_CTX_use_PrivateKey_file ... failed 私钥问题 私钥与证书不匹配、密码未解开
no start line 文件损坏 / 路径错误 私钥文件内容格式坏、或路径权限不对

A.2 排查握手:从证书到 cipher 一路往下走


			   bash# 1. 看 Nginx 实际 OpenSSL 版本 nginx -V 2>&1 | tr ' ' ' ' | grep -i 'openssl|boringssl' # 2. 看证书有效期、域名匹配、issuer openssl x509 -in /etc/nginx/ssl/app.example.com.crt -noout -issuer -subject -dates echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -subject -dates -issuer # 3. 检查证书链是否完整 openssl s_client -connect app.example.com:443 -servername app.example.com -showcerts /dev/null | grep -E 's:|i:' # 4. 让 Nginx 自己向上游发 SNI 测试握手 curl -v --resolve upstream.svc.local10.0.0.11 https://upstream.svc.local/ -o /dev/null --max-time 5 

A.3 反代上游 HTTPS 时的最终落地配置示例


			   nginxupstream backend_tls {     server 10.0.0.11:8443;     server 10.0.0.12:8443;     keepalive 32; } server {     listen 443 ssl;     server_name app.example.com;     ssl_certificate     /etc/nginx/ssl/app.example.com.crt;     ssl_certificate_key /etc/nginx/ssl/app.example.com.key;     ssl_protocols       TLSv1.2 TLSv1.3;     ssl_prefer_server_ciphers on;     ssl_ciphers         ECDHE-ECDSA-AES256-GCM-SHA384ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;     ssl_ecdh_curve      X25519:secp384r1;     ssl_session_cache   shared10m;     ssl_session_timeout 1d;     ssl_session_tickets off;     ssl_stapling        on;     ssl_stapling_verify on;     resolver            223.5.5.5 1.1.1.1 valid=300s ipv6=off;     location / {         proxy_pass https://backend_tls;         proxy_ssl_server_name     on;       # 把客户端 SNI 转发到上游         proxy_ssl_protocols       TLSv1.2 TLSv1.3;         proxy_ssl_session_reuse   on;         proxy_ssl_name            $host;    # 用客户端 Host 作为 SNI 名         # 上游证书是自签,关闭验证;如果不是自签要打开 verify + trusted_certificate         proxy_ssl_verify          off;         proxy_connect_timeout 5s;         proxy_send_timeout    30s;         proxy_read_timeout    60s;         proxy_next_upstream   error timeout http_502 http_503 http_504;         proxy_http_version 1.1;         proxy_set_header Connection "";         proxy_set_header Host $host;     } } 

A.4 OCSP stapling 不能生效的几种排查

error.log 出现 ssl_stapling 相关报错、或线上检测 OCSP stapling 状态为不可用时,按下面顺序排查:

  1. nginx -V  看 OpenSSL 是否大于 1.0.2(OCSP basic 必须 >= 1.0.1f,stapling 通常需要 >= 1.0.2)。
  2. 证书必须包含 AIA URL(Authority Information Access),签发证书里没有这个 URL 就拿不到 OCSP。
  3. Nginx 必须能访问 DNS 解析 OCSP responder 域名;resolver 必须配置。
  4. 防火墙必须放行 80/443 出方向到 OCSP responder。
  5. ssl_trusted_certificate  配置必须是完整的 issuer chain。

			   bash# 检查证书有没有 AIA openssl x509 -in /etc/nginx/ssl/app.example.com.crt -noout -text | grep -A 5 'Authority Information Access' # 手动复测 stapling echo | openssl s_client -connect app.example.com:443 -status &1 | grep -E 'OCSP Response Status|OCSP Staple' 

附录 B:HTTP/2 反代的细节坑

线上不少业务升级到 HTTP/2 后,又出现“能连接但请求失败”的诡异报错。最常见的是 Connection reset by peer 或 HTTP/2 stream id 不匹配。这一节把可能踩到的坑列一下。

B.1 http2 与 proxy_http_version 1.1 必须同时配置


			   nginxserver {     listen 443 ssl http2;     location / {         proxy_pass http://backend;         proxy_http_version 1.1;     # HTTP/2 反代必须要让上游走 1.1         proxy_set_header Connection "";         keepalive_timeout 60s;       # HTTP/2 下设置     } } 

少了任意一行,上游会直接报协议错误。

B.2 HTTP/2 → Nginx → HTTP/1.1 → 上游:握手/超时表现

  • 客户端到 Nginx 是 HTTP/2,单个 TCP 连接里多 stream 复用。
  • Nginx 到上游仍是 HTTP/1.1,stream 之间用 upstream keepalive 共享连接池。
  • 上游响应慢 → 单 stream 卡住 → 客户端 HTTP/2 GOAWAY 关闭 → Nginx 处类似 RST。

排查时仍然以 access.log 的状态码做主导,512 byte 的状态码含义同 HTTP/1.1。

B.3 启用 H2C 反代

H2C(HTTP/2 over cleartext)反代场景一般出现在反代 grpc-javagrpc-go 服务时:


			   nginxserver {     listen 80 http2;        # 明确开启 H2C     location / {         proxy_pass http://127.0.0.1:50051;         proxy_http_version 2.0;         proxy_set_header Host $host;         proxy_set_header Connection "";     } } 

grpc 反代场景在客户端通常会看到 Stream removed,需要同时确认:

  • proxy_buffering off;
  • proxy_request_buffering off;
  • gRPC 客户端启用重试或超时

B.4 gRPC / stream 反代常见超时

gRPC 长请求是 HTTP/2 stream 持续发送数据:


			   nginxgrpc_read_timeout 600s;       # 给上游足够时间 send_timeout 600s; keepalive_timeout 600s; 

参数具体值依业务而定,但思路就是:gRPC 长请求要把所有 timeout 全部放宽,proxy_buffering 等缓冲类配置视情况关闭。

附录 C:大文件上传失败:从 413 到 504

client_max_body_size 设小了,浏览器直接报 413;超过限制但 Nginx 又卡在上行,会变成 504。这是线上踩坑最多的上传场景之一。

C.1 标准处理


			   nginxserver {     client_max_body_size 100m;     client_body_timeout  60s;     client_header_timeout 60s;     location /upload {         # 大文件场景:关闭 buffer,避免堆积         client_body_in_file_only  on;         client_body_buffer_size  1m;         proxy_pass          http://backend_upload;         proxy_http_version  1.1;         proxy_request_buffering off;     # 大文件场景         proxy_read_timeout   600s;         proxy_send_timeout   600s;         proxy_connect_timeout 10s;     } } 

判断逻辑:

  • client_body_in_file_only on :把上传临时写到盘,proxy 那一边不用堆积内存。
  • proxy_request_buffering off :上传过程中直接转发,不在 Nginx 缓冲。
  • proxy_read_timeout  不小于业务最长请求时长。

C.2 风险点

  • 临时文件会写到 client_body_temp_path,监控磁盘空间。
  • client_body_in_file_only on  时必须确保 proxy_pass 上游能 streaming 接收,否则反而会成更大风险。
  • 这种大文件场景不建议打开 proxy_buffering,会和 disk buffering 冲突。

C.3 验证


			   bash# 上传一个 50MB 测试文件 dd if=/dev/urandom of=/tmp/bigfile bs=1M count=50 curl -X POST -F "file=@/tmp/bigfile" --max-time 600 https://app.example.com/upload -o /dev/null -w 'http_code=%{http_code} time_total=%{time_total} ' 

预期:5xx 内全部走通,性能不会因为文件大小失控。

附录 D:WebSocket 反代超时

WebSocket 默认是 hold 住的,必须把 Nginx 的 read timeout 拉长。

D.1 标准配置


			   nginxlocation /ws {     proxy_pass http://backend;     proxy_http_version 1.1;     proxy_set_header Upgrade $http_upgrade;     proxy_set_header Connection "upgrade";     proxy_connect_timeout 5s;     proxy_send_timeout    300s;     proxy_read_timeout    300s;     # 一定要大于心跳间隔     proxy_buffering       off; } 

心跳间隔通常 30s,所以 proxy_read_timeout 不能小于 30s,否则心跳都会让连接被 NGINX 当成 504 关掉。

D.2 错误排查

  • 客户端突然断线:可能是 proxy_read_timeout 太短。
  • Nginx 上看到 502:upstream prematurely closed 是上游 WebSocket 主动断了。
  • 上游存活但客户端没响应:/proc/net/tcp 看 ESTAB 是否占着。

附录 E:完整 nginx.conf 参考(带注释)

下面是文章所有示例合并起来的最终版 main / http / upstream / server 段。线上根据真实业务在 /etc/nginx/conf.d/ 下拆 .conf 文件即可。


			   nginxuser  nginx; worker_processes auto; worker_rlimit_nofile 100000; pid /var/run/nginx.pid; error_log /var/log/nginx/error.log warn; events {     worker_connections  65535;     multi_accept on;     use epoll; } http {     include       mime.types;     default_type  application/octet-stream;     log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '                       '$status $body_bytes_sent "$http_referer" '                       '"$http_user_agent" "$http_x_forwarded_for" '                       'rt=$request_time uct=$upstream_connect_time '                       'urt=$upstream_response_time ua=$upstream_addr';     access_log /var/log/nginx/access.log main buffer=32k flush=5s;     sendfile        on;     tcp_nopush      on;     tcp_nodelay     on;     keepalive_timeout  65;     types_hash_max_size 2048;     open_file_cache max=10240 inactive=20s;     open_file_cache_valid 30s;     open_file_cache_min_uses 2;     open_file_cache_errors on;     server_tokens off;     server_names_hash_bucket_size 128;     gzip on;     gzip_min_length 1k;     gzip_comp_level 5;     gzip_vary on;     gzip_proxied any;     gzip_types application/json application/javascript text/css text/plain application/xml application/octet-stream;     proxy_http_version 1.1;     proxy_set_header Host              $host;     proxy_set_header X-Real-IP         $remote_addr;     proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;     proxy_set_header X-Forwarded-Proto $scheme;     proxy_connect_timeout 5s;     proxy_send_timeout    30s;     proxy_read_timeout    60s;     proxy_buffering on;     proxy_buffer_size 8k;     proxy_buffers 16 16k;     proxy_busy_buffers_size 32k;     proxy_next_upstream error timeout http_502 http_503 http_504;     proxy_next_upstream_tries 2;     include /etc/nginx/conf.d/*.conf; } 

放在这里只是为了后续维护方便单独提取——和文章正文示例保持一致。

附录 F:实战工单三个闭环

下面是我在生产中遇到过的真实工单的完整排查过程。读者对照前面几节的命令和判断逻辑,可以一眼看到整个闭环。

工单 F.1:登录页偶发 502,时间分布无明显规律

F.1.1 现象

  • 监控告警:5xx 比例突增,最高 5%。
  • access.log:502 集中在 11:00–11:30 期间。
  • 用户反馈:登录页有时刷不出来。

F.1.2 初步判断

502 大概率是上游层(upstream)。

F.1.3 命令检查


			   bashtail -n 50000 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn # 31200 200 #  1840 502 #   420 504 #    80 499 grep -E '[(error|emerg)]' /var/log/nginx/error.log | tail -n 50 # "upstream prematurely closed connection while reading response header from upstream" 

F.1.4 关键指标


			   bashfor pid in $(pgrep -f 'nginx: worker'); do   fd=$(ls -1 /proc/$pid/fd 2>/dev/null | wc -l)   echo "worker pid=$pid fd=$fd" done # worker pid=12412 fd=20120 # worker pid=12413 fd=20453 # worker pid=12414 fd=18800 # worker pid=12415 fd=17842 

F.1.5 根因定位

FD 数字约 2 万,已经接近 worker_rlimit_nofile 但不超限。再看上游:


			   bashss -antp | grep ':8080' | wc -l # 9500 

连接数偏高,是因为 proxy_next_upstream 启用了重试,每个失败请求变成 2 个连接池占位。

更深一层看:


			   bashgrep -A 5 upstream /etc/nginx/conf.d/backend.conf # 没有 keepalive 

F.1.6 修复方案

加上 keepalive 64;


			   nginxupstream backend {     least_conn;     server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;     server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;     keepalive 64; } 

F.1.7 验证


			   bashnginx -t && nginx -s reload # reload 后等待 5 分钟采样 tail -n 50000 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn # 5xx 比例从 5% 下降到 0.3%,符合业务基线 

F.1.8 回滚预案


			   bashcp -a /etc/nginx/conf.d/backend.conf.bak.YYYYMMDD-HHMMSS /etc/nginx/conf.d/backend.conf nginx -t && nginx -s reload 

F.1.9 复盘

keepalive 没启用是很多老配置里的硬伤。

工单 F.2:客户端大量 499,Nginx 看起来没事

F.2.1 现象

  • 业务方反馈“客户端断线很多,偶发”。
  • access.log:499 比例 12%。
  • 后端 java 应用没有报错。

F.2.2 初步判断

499 是客户端主动关。可能是客户端 HTTP 超时太短,或者上游响应慢让客户端等不住。

F.2.3 命令检查


			   bashawk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn # 200: 33200 # 499: 4400 # 502: 200 awk -F'urt=' '{print $2}' /var/log/nginx/access.log | awk '{print $1}' | awk '$1 > 1' | sort -rn | head -10 # 9.4 # 8.7 # 5.6 # ... 

F.2.4 关键指标

upstream_response_time 出现大量 > 1s,甚至 9 秒。

F.2.5 根因

上游 Java 应用某个接口慢,但 Nginx 端没有把慢请求切走(proxy_next_upstream 看到的是 error timeout,不是慢)。

F.2.6 修复

在上游侧增加熔断 + 慢请求报警;Nginx 这边加:


			   nginxproxy_next_upstream error timeout http_502 http_503 http_504 non_idempotent; 

加 non_idempotent 后,重试判定会更严格,POST/PUT 不再重试,减少重复提交风险。

F.2.7 验证 + 回滚

同 F.1.7 / F.1.8。

工单 F.3:reload 后 worker 进程没换

F.3.1 现象

  • 修改了 nginx.conf,reload 后配置生效看起来“诡异”。
  • 新增的 location 不生效。

F.3.2 命令检查


			   bashnginx -t # OK nginx -s reload ps -ef | grep nginx | grep worker # worker pid=12412 start time=2 days ago # worker pid=12413 start time=2 days ago 

F.3.3 关键指标

worker pid 时间戳没刷新。

F.3.4 根因

是用 kill -HUP 而不是 nginx -s reload。在某些发行版 / systemd unit 下,HUP 信号会被 systemd 截获并忽略。

F.3.5 修复

按 systemd 重启流程:


			   bashsystemctl reload nginx 

或者直接:


			   bashnginx -s reload 

如果 nginx 不在 path,则用 /usr/sbin/nginx -s reload 或 $(which nginx) -s reload

F.3.6 验证

worker 进程时间戳刷新,配置生效。

附录 G:快查脑图(命令清单)

为了方便“在事故现场打开就能用”,把全文章的命令汇总成一个清单:


			   text[error.log 关键词]   tail -n 5000 /var/log/nginx/error.log | awk -F'[][]' '{print $4}' | sort | uniq -c | sort -rn [access.log 状态码分布]   awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn [nginx 检查]   nginx -V   nginx -t   nginx -T 2>/dev/null | less   nginx -s reload [进程/FD]   cat /proc/$(pgrep -f 'nginx: master')/limits   for pid in $(pgrep -f 'nginx: worker'); do     fd=$(ls -1 /proc/$pid/fd 2>/dev/null | wc -l)     echo "worker pid=$pid fd=$fd"   done [连接]   ss -s   ss -antp | grep ':80 ' | awk '{print $1}' | sort | uniq -c   ss -antp | grep ':443' | awk '{print $1}' | sort | uniq -c [系统层]   ulimit -n   sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.ip_local_port_range   free -h [证书]   openssl x509 -in /etc/nginx/ssl/app.example.com.crt -noout -issuer -subject -dates   openssl s_client -connect app.example.com:443 -servername app.example.com /dev/null | openssl x509 -noout -subject -issuer [TLS 检查]   echo | openssl s_client -connect app.example.com:443 -status &1 | grep -E 'OCSP Response Status|OCSP Staple' [curl 测试]   curl -sS -o /dev/null -w 'http_code=%{http_code} time_total=%{time_total} ' --max-time 5 http://10.0.0.11:8080/healthz 

附录 H:常见 FAQ

Q1:改了 nginx.conf 没问题,reload 也没报错,但新配置不生效。

A:99% 是 include 顺序问题。例如 /etc/nginx/conf.d/ 下有两个 *.conf,B 后加载就覆盖 A 的 server_name。解决方案:明确拆分文件名如 10-backend.conf20-static.conf、数字越小的优先级越高(按字母序)。

Q2:worker 一直显示老进程,新 worker 没起来。

A:可能是 worker_processes 被改过,或多 master 切换没完成。pstree -p $$ 看进程树,再 pkill -f 'nginx: master' 重新启动。

Q3:access.log 看不到但磁盘空间在涨。

A:access_log 写权限丢失、被覆盖写到别的路径、或 buffer 没刷盘,定位:


			   bashls -l /var/log/nginx/ lsof | grep -E 'access.log|error.log' 

Q4:错误日志中频繁出现 worker process X exited on signal 9 (SIGKILL)

A:worker 被系统 OOM killer 杀掉,看 dmesg | grep -i oom 与 free -h,必要时调整 worker_rlimit_nofile 或降低 worker_connections

Q5:reload 时出现 recv() failed (104: Connection reset by peer)

A:通常是上游反向连接(如动态 upstream 域名)解析结果抖动。固定上游 IP 列表 / 配置 resolver + 合理 resolver_timeout 即可。

Q6:Nginx 反向代理后端 HTTP 接口,返回 200 但 body 是空。

A:很可能是 sub_filter 或 proxy_buffer_size 不够、被截断。看 proxy_buffer_size / proxy_buffers 调整;或临时关 proxy_buffering 看完整 body。

Q7:F5 → Nginx → upstream,客户端 IP 全是同一个。

A:F5 不改源 IP 时,要让 Nginx 用 set_real_ip_from 信任头:


			   nginxset_real_ip_from 10.0.0.0/8; set_real_ip_from 192.168.0.0/16; real_ip_header    X-Forwarded-For; real_ip_recursive on; 

Q8:Nginx 启动慢、内存大涨。

A:通常配置了 geoip / map 大规则集、或 limit_req_zone 非常大。limit_req_zone $binary_remote_addr zone=one:10m; 这样的 10m 内存对应 16 万 IP。再大就要考虑用 Redis 共享存储做限流。

Q9:HTTP/3 / QUIC 开着之后报错。

A:HTTP/3 必须 OpenSSL >= 1.1.1 且要用 HTTP/3 替代品如 OpenResty / Cloudflare 的支持。普通 Nginx 不支持 QUIC。要么启用 HTTP/2,要么换发行版。

Q10:nginx -s reload 报错 nginx: [error] invalid PID number "" in "/var/run/nginx.pid"

A:配置文件里改动了 pid 路径、或者 nginx 实际运行状态没在 /var/run/nginx.pid 里。检查实际进程 pgrep -f 'nginx: master'

附录 I:监控告警建议(Prometheus 思路)

线上 Nginx 接 Prometheus 是常见做法。下面给一个最小化可用的告警配置思路,让读者把指标与告警对齐业务基线。

I.1 nginx-nginx-prometheus-exporter 关键指标

通过模块 nginxinc/nginx-prometheus-exporter 暴露的关键指标:

指标名 含义
nginx_http_requests_total 请求总数
nginx_http_responses_total 按状态码分布的响应总数
nginx_http_requests_in_progress 当前处理中的请求数
nginx_http_upstreams_up upstream 健康状态
nginx_connections_accepted  / nginx_connections_handled / nginx_connections_active / nginx_connections_reading / nginx_connections_writing / nginx_connections_waiting stub_status 6 项
nginx_upstream_peer_connect_ms  / nginx_upstream_peer_first_byte_ms 上游连接、首包延迟

I.2 Prometheus 告警建议


			   yamlgroups: - name: nginx.rules   rules:   - alert: Nginx5xxHigh     expr: |       sum(rate(nginx_http_responses_total{code=~"5.."}[5m]))       /       sum(rate(nginx_http_responses_total[5m]))       > 0.01     for: 5m     labels: { severity: warning }     annotations:       summary: "5xx 比例过高 {{ $labels.instance }}"   - alert: NginxUpstreamDown     expr: nginx_upstreams_up == 0     for: 1m     labels: { severity: critical }     annotations:       summary: "upstream 全部不可用 {{ $labels.instance }}"   - alert: NginxConnectionsHigh     expr: nginx_connections_active > 10000     for: 5m     labels: { severity: warning }     annotations:       summary: "活动连接数过高 {{ $labels.instance }}" 

I.3 与日志告警互补

Prometheus 适合做指标类告警,日志告警(filebeat / fluentd + Loki)能告 error.log 中的关键字。最基础建议:

  • 配置 emerg/alert/crit 关键字即时推送(短信 / 电话)。
  • error  关键字 5 分钟内出现 > N 次自动告警。

			   yaml# 示例 Loki/Loki-ruler - alert: NginxErrorLogSpike   expr: |     sum(rate({job="nginx", level="error"}[5m])) > 5   for: 5m   annotations:     summary: "Nginx error.log 高频告警" 

注意阈值要按真实业务基线调整;不要盲目套阈值。

附录 J:常见 CI/CD 流水线集成(Nginx 配置自动化)

如果想用 CI/CD 推动 Nginx 配置文件统一管理,可参考下面思路。

J.1 仓库结构


			   ops/nginx/ ├── conf.d/ │   ├── 00-default.conf │   ├── 10-backend.conf │   └── 20-static.conf ├── snippets/ │   ├── ssl.conf │   └── proxy.conf ├── tests/ │   └── test_nginx.sh ├── deploy.sh └── README.md 

nginx.conf 仅做最小化 main / events / http,所有业务配置都通过 include 加载。

J.2 静态检查


			   bash# 关键:reload 前一定要 nginx -t 跑测试 nginx -t -c $(pwd)/nginx.conf -p $(pwd)/ # 或者用第三方静态检查 # nginx -T 2>/dev/null > /tmp/nginx.full.conf # 然后 grep / awk 检查某些禁用配置 

可以在 CI 里跑 nginx -t -c your.conf,并要求输出为空,否则构建失败。

J.3 灰度上线


			   bash#!/usr/bin/env bash set -euo pipefail CONF_FILE=$1 TARGET_HOST=$2 ansible "$TARGET_HOST" -m copy -a "src=$CONF_FILE dest=/etc/nginx/conf.d/$(basename $CONF_FILE)" ansible "$TARGET_HOST" -m shell -a 'nginx -t' ansible "$TARGET_HOST" -m shell -a 'nginx -s reload' echo "Reload OK on $TARGET_HOST" 

把脚本接到 GitLab CI / ArgoCD 上,灰度按机房分布组即可。

J.4 灰度验证建议

灰度发布后 5 分钟内观察:

  • 5xx 比例 + 状态码分布(与变更前 baseline 对比)
  • error.log  关键字 error|emerg|alert|crit 数量
  • nginx_connections_active  与 accepts/handled 比值

任意一项明显恶化,按预案立刻回滚。

附录 K:性能压测与容量估算

要让线上 Nginx 不被“不会扩容”害到,建议在容量基线做完之后,再用压测复测一遍。

K.1 简单压测方案


			   bash# wrk 示例 wrk -t8 -c200 -d60s -H "Host: app.example.com" https://app.example.com/ # 或者 ab ab -n 50000 -c 200 -k -H "Host: app.example.com" https://app.example.com/ # Tsung / Gatling / k6 适合复杂场景 

K.2 估算容量

根据压测结果估算:

  • 单 worker 处理能力:RPS(request per second)
  • 整机容量:RPS * worker_processes ≈ 整机 RPS
  • 安全余量:业务峰值 / 容量 < 0.6

K.3 容量调优方向

  • 增大 worker_connections / worker_processes
  • 开启 proxy_buffering / gzip / gzip_static
  • 减少上游响应时间(cache、合并接口、批量写)
  • 反代池配合客户端 keepalive
  • 拆分静态 / 动态到不同 Nginx 实例

说明:以上每条改动都要重测线上代表样本,不能不基于数据就调。

附录 L:磁盘 / FD / 内核参数不匹配导致的隐蔽错误

下面几种是新手最常踩、但报错不显眼的情况,单独拧成一节。

L.1 Too many open files 突然出现

排查:


			   bashcat /proc/$(pgrep -f 'nginx: master')/limits | grep 'open files' sysctl fs.file-max 

生产建议把 fs.file-max >= 200000,把每个 worker 进程 LimitNOFILE 拉到 100000 起。

L.2 accept: too many open files

这是 accept() 系统调用返回 EMFILE,进程 FD 耗尽。处理思路同 L.1。

L.3 failed (24: Too many open files) while connecting to upstream

上游连接的 socket 也是 FD。要保证 worker FD 上限足够,且 worker_connections 不能无限放大。

L.4 临时端口耗尽

ip_local_port_range 默认 32768 60999,总可用端口约 28000。如果同时打到上游的连接数长时间 > 28000,会出现端口分配失败。Linux 上同一个 ephemeral 端口 60 秒内不能重复使用,因此短连接场景要把范围拉大:


			   bashsysctl net.ipv4.ip_local_port_range=1024 65000 

L.5 sysctl tcp_tw_reuse=1 vs tcp_tw_recycle

tcp_tw_recycle 在 Linux 4.12 之后默认不再生效,且曾经在 NAT 场景下导致过诡异 bug。建议不再使用它,业务上控制短连接比调整它更直接。

L.6 ulimit 与 systemd 双重作用

如果 systemd unit 写了 LimitNOFILE=100000,但用户层 ulimit -n 1024,master 启动后会用 unit 设置的值。我建议团队在 Ansible / Terraform 模板里统一管这两个值。

附录 M:核心判断逻辑速查表

这是给自己和团队一年的实战经验最终汇总出来的一张表。读者把它打印贴在工位旁边也合适。

现象 第一判断 关键命令 第二步动作
大面积 502 上游不可用 上游 curl、cluster health、netstat 检查上游集群状态
大面积 504 上游慢 awk -F'urt='  看上游耗时 调整 proxy_read_timeout / 上游能力
大面积 499 客户端超时间短 看客户端配置 / Nginx request_time 分布 调整客户端 timeout / 上游性能
reload 后配置不生效 没 reload 成功 nginx -t  + worker 启动时间 用 nginx -s reload 或 systemctl reload
error.log 中 Too many open files FD 耗尽 cat /proc/$(nginx_pid)/limits 调整 LimitNOFILE + worker_rlimit_nofile
accept: too many open files 同上 同上 同上
TLS 握手失败(握手关键字) 上游 / 客户端 TLS 配置 openssl s_client -status 调整 proxy_ssl_*
上游 HTTPS 自签证书报错 verify 开启 / 信任链不全 proxy_ssl_trusted_certificate 配置完整 CA
WSS 连接断 proxy_read_timeout  太短 awk -F'urt='  WS 上游响应时长 把 proxy_read_timeout ≥ 心跳间隔
大文件上传 413 client_max_body_size  设小 看 error.log 调大 client_max_body_size
大文件上传 504 上游接收慢 proxy_request_buffering off  + client_body_timeout 关 buffer + 调大 timeout
HTTP/2 反代 Connection reset proxy_http_version check proxy_http_version 1.1 调协议版本
gRPC 反代失败 grpc_*  配置不全 proxy_buffering off 关闭 buffering
reload 后 worker 时间戳未变 reload 信号无效 systemctl reload nginx 改用 systemd reload

最后一条:如果上述都查完仍未定位,建议把日志打包(/var/log/nginx/ 配合最近 error.log + access.log)发给有经验的同行,不一定是自己一个人扛到底。

附录 N:报错关键字 → 应急动作快查

为了让读者在“凌晨三点被叫醒”时能更快行动,按关键字给出最短的应急步骤,全部是命令驱动的。

N.1 [emerg](启动失败)

  • 现象:Nginx 启动瞬间报 nginx: [emerg]
  • 应急:nginx -t 找出报错行;通常配置加载失败,/etc/nginx/nginx.conf 语法问题、include 文件缺失、证书路径错误。

N.2 bind() to 0.0.0.0:80 failed (98: Address already in use)

  • 现象:端口被占用
  • 应急:

			   bashss -lntp | grep ':80 ' fuser -k 80/tcp   # 仅排查用,生产慎用 systemctl stop <占用进程> systemctl start nginx 

N.3 connect() failed (113: No route to host)

  • 现象:上游网络层不可达
  • 应急:ip route / traceroute 10.0.0.11;找网工 / 云厂家。

N.4 connect() failed (110: Connection timed out)

  • 现象:上游防火墙挡住了
  • 应急:安全组 / iptables / VPC ACL 排查。

N.5 upstream timed out

  • 现象:上游响应慢
  • 应急:第一步先临时拉长 proxy_read_timeout,第二步看上游 GC、DB、Cascading IO。

N.6 upstream prematurely closed connection

  • 现象:上游主动断了
  • 应急:检查上游是否在重启、是否健康检查失败被踢、是否 OOM。

N.7 client closed connection while ...

  • 现象:客户端断连
  • 应急:检查客户端 timeout、调大 proxy_*_timeout、调大客户端的连接保持时长。

N.8 SSL_do_handshake() failed

  • 现象:握手失败
  • 应急:openssl s_client -connect ... 复测,按上面附录 A 流程。

N.9 worker process xxx exited on signal 9

  • 现象:worker 被 SIGKILL 杀掉
  • 应急:看 dmesg / free -h;检查 systemd MemoryMax / OOMScoreAdjust

N.10 worker process xxx exited on signal 11

  • 现象:worker 段错误
  • 应急:升级版本 / 重新编译 / 联系发行版维护;回收内存先排查。

N.11 malloc() failed

  • 现象:内存不足
  • 应急:减少 worker_connectionsgzipproxy_buffer_size,检查是否 map 引入爆炸性内存。

N.12 accept4() failed (28: No space left on device)

  • 现象:端口耗尽或暂存端口耗尽
  • 应急:拉大 ip_local_port_range;减少 keepalive 等待。

N.13 Too many open files

  • N.9 / N.10 同源,已在前面 L.1 处理。

N.14 rewrite or internal redirection cycle

  • 现象:rewrite 反复命中
  • 应急:检查 last / break 是否用错,正则是否能命中。

N.15 conflicting server name "xxx"

  • 现象:server_name 冲突
  • 应急:合并或删除冲突 server 段。

附录 O:错误日志格式与时间戳

error.log 时间戳默认是系统时区。如果你在多集群中混看“同一时刻”日志,可能会被时区搞糊涂:


			   bash# 看 error 日志格式 head -n 3 /var/log/nginx/error.log # 修改 access.log 时间戳格式(用 $time_iso8601) log_format main '$time_iso8601 ...' 

time_iso8601 形如 2026-07-07T1533+08:00,适合接 Loki / Elastic 做时区归一化。

读 access.log 时也可以加 $request_id 让上下游日志串起来:


			   nginxlog_format main '$request_id $remote_addr ...'; 

并配合 proxy_set_header X-Request-ID $request_id; 把 ID 透传给上游。

 


打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分