线上 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 服务器、几条报错日志,能照着这套流程一步步往下走,每一步都明确看什么指标、判断什么现象、下一步往哪里收敛。
下面这些场景,是本文的明确覆盖范围,文中给出的命令、配置、回滚方法都按这些场景设计:
error.log 大量出现 502 / 504。access.log
中 HTTP 状态码分布异常:例如 500 比例升高、499(客户端关闭)比例上升、连接被 reset。ERR_SSL_PROTOCOL_ERROR、SSL_ERROR_SYSCALL。ulimit -n、TIME_WAIT / CLOSE_WAIT 异常堆积、新建连接排队。不适用场景(不要把本文当成万能药):
nginx-upload-module、GeoIP 模块缺失),这是构建期问题。要把 Nginx 报错排得快,几个底层概念必须清楚,否则命令拿到也只是盲跑。
Nginx 启动后是一个 master 进程 + 多个 worker 进程。master 只负责加载配置、管理 worker,平滑升级、平滑重启都靠它;worker 是真正处理请求的进程,监听共享端口(通过 SO_REUSEPORT 或共享 listen socket)。
排查时常用的几个进程事实:
/var/run/nginx.pid(编译时可用 --pid-path 改)。worker_processes
决定 worker 数,通常设为 CPU 核数或其倍数。worker_connections 控制。worker_rlimit_nofile
会覆盖 worker 进程的 ulimit -n(前提 master 有权限)。理解这点对排查“连接打满”类故障有直接帮助:worker 是单进程多路复用,单 worker 实际可达上万连接,多 worker 乘起来就是 Nginx 整机并发上限。
Nginx 写两类日志:access.log 和 error.log。位置由 access_log 与 error_log 指令决定,默认在 /var/log/nginx/。
error.log 有 8 个等级,从高到低:emerg、alert、crit、err、warn、notice、info、debug。线上排查最常用 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 报错”都可以先从这三个状态码切入。
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 大量出现时,几乎一定会动这几个参数或看它们的日志。
Nginx 上游连接、客户端连接、文件读写,最终都映射成 Linux 进程的文件描述符(FD)。Nginx 任何一个 worker 进程 FD 耗尽,就表现为新连接被拒绝、connect() failed (24: Too many open files)。
同一时刻系统层面:
net.ipv4.tcp_max_syn_backlog)满了,新连接会被丢弃net.core.somaxconn / 实际 backlog)满了,新连接会被回 RSTss -s)虽然不影响收新连接,但大量占用 ephemeral 端口线上真实场景里,重启 master + worker 后报错“瞬间恢复”,很大概率就是 FD 资源被泄漏或耗尽。
Nginx 在做 HTTPS 反代,或者反代到上游 HTTPS 时,可能遇到 SSL 握手失败。常见原因:
x509: certificate has expired)proxy_ssl_server_name on; 但上游 virtualhost 与请求域名不同)
排查时 nginx -V 看编译参数,再结合 error.log 关键字(如 SSL_do_handshake() failed、SSL_CTX_use_PrivateKey),可以快速确认握手在哪一步失败。
我习惯把排查分四层,从最靠近用户的地方往最底层走,每一层都有明确“能结束”这一层的指标:
第一层:确认报错是不是真在 Nginx
error.log 最近一分钟的报错热点。access.log 最近 1 分钟 / 5 分钟的状态码分布。第二层:缩小范围,区分是上游还是 Nginx 自身
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 场景)第三层:深入到具体组件
reload 状态、配置 include 顺序ulimit -n、端口分配、TCP 状态、swap / OOM第四层:定位根因,做变更
每做一次配置变更之前,都强制要求:
nginx.conf.bak.YYYYMMDD-HHMMSS(脚本里用 $(date +%Y%m%d-%H%M%S) 拼好后缀)。任何不满足这三点的变更都不要在生产上直接执行。
下面这些步骤,是我自己在生产环境里最常用的顺序。不是硬性模板,但每一步的“目的 / 命令 / 观察指标 / 判断逻辑 / 下一步动作”都明示,读者照着照搬可行。
目的:避免误判,把“业务报错”误归到 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(...) failed、invalid PID number 等,都是配置或证书层问题,立刻停下,看步骤 5。[emerg]
出现:基本是 master 启动失败或严重配置错误,必须立即看。[error] 都没有,但业务说 502:大概率是上游层问题,不是 Nginx。判断逻辑:error.log 完全没有相关关键字、Nginx 进程是 alive 的情况下,把方向转向 upstream。
下一步动作:进入步骤 2,看 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 异常表现:
proxy_read_timeout 调优。判断逻辑:根据状态码分布反推方向。报错率低于总请求 0.1% 时观察一段时间即可,高于 1% 通常就要立即介入。
下一步动作:进步骤 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。
目的:判断是不是 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 异常表现:
limit,且 open files 是 soft 限制(默认 1024),就要升 worker_rlimit_nofile 和系统 ulimit -n。判断逻辑:FD 接近 limit → 系统资源问题;连接数远大于合理值 → 上游慢或客户端异常。
下一步动作:进入步骤 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' 预期输出:
nginx
upstream 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 是否真的成功。
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 还是执行了:实际进程没有重载,新配置被丢弃。worker_processes 被改成了 1。shutdown_timeout 设得太长,业务流量切走,但旧 worker 还占用资源。判断逻辑:reload 之后必须能拿出明确的“新 worker 时间戳”作为证据。否则任何配置优化都是无效操作。
下一步动作:进入步骤 7,验证 Nginx 实例层面是否有限制。
目的:当 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 包可能被丢。判断逻辑:这些值偏小但请求量大的时候,必然出错。一般生产建议:
ip_local_port_range
不要太小下一步动作:进步骤 8,做配置变更并验证。
目的:把上面分析出的变更落到配置,并验证。
具体配置示例、变更步骤、回滚方案、风险点都会集中放在“配置示例”和“回滚方案”章节,避免在这一节重复。
实战中的几条要点:
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%Y%m%d-%H%M%S)。nginx -t,不要跳过语法检查。error.log,reload 报错立刻回滚。下面是排查过程中随时会用到的命令汇总,按用途归档。读者可以把这部分单独收藏。
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 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 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 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 bash
ulimit -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 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 关键段,包含几个针对频繁报错场景的高频开关。生产环境里要把这些配置按真实路径落地,示例都给了文件路径和说明。
文件:/etc/nginx/nginx.conf → main {}
nginx
worker_processes auto; # 通常与 CPU 核数对齐 worker_rlimit_nofile 100000; # 把 worker 的 nofile 提到较大值 pid /var/run/nginx.pid; error_log /var/log/nginx/error.log warn;
文件:/etc/nginx/nginx.conf → http {}
nginx
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; open_file_cache max=10240 inactive=20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; server_tokens off; # 关闭版本号,避免泄露 }
文件:/etc/nginx/conf.d/backend.conf
nginx
upstream 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; }
文件:/etc/nginx/conf.d/app.conf
nginx
server { 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; } }
文件:/etc/nginx/conf.d/app_tls.conf
nginx
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_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; }
文件:/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。
文件:/etc/sysctl.d/99-nginx.conf
bash
net.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 还不够,配合指标才能把报错“定位到层”。这里给出标准观测法。
不是每次都能接外部采集器,至少用 stub_status / 自带 metrics 拉几个核心指标:
nginx
location /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 队列上限。日常观察的常用切片:
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 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 如果上游是 Java 应用,注意:
jstat -gc 1000
看 GC 频率jstack
看有没有线程阻塞在 socket readToo many open files 应用侧日志如果上游是 PHP-FPM:
cgi-fcgi -connect 10.0.0.11:9000
看 FPM 状态pm.max_children、pm.start_servers 配置和实际活跃进程数最低限度,线上 Nginx 实例应该有以下告警项(结合实际业务基线调整阈值):
upstream timed out、SSL_do_handshake() 失败等)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 三窗口观察状态码 └─ 稳定无回弹 → 进入复盘 下面这些是我踩过坑的风险点,做运维变更前必须先把对应的处置准备好。
/etc/nginx/conf.d/ 下的 include 文件,但没有用 nginx -s reload,结果是 Nginx 仍按旧配置在跑。流程上必须把 reload 列为配置变更的强制动作。shutdown_timeout 没设的情况,要看 graceful 关闭时长是否覆盖新流量。USR2 启动新 master → USR1 给老 master 发请求切到新 master → WINCH 让老 worker 退出。server_name 多个匹配时,最后一个胜出
。include 顺序错了,证书也会错。proxy_next_upstream 在 POST 大请求场景下,重试可能造成业务重复提交
。建议对带写操作的接口关闭 http_502/503 之外的触发条件,或仅超时类触发。access_log buffer=... 调太大,可能丢日志
。buffer 没写盘但 worker 异常退出时会丢,慎用。X-Forwarded-For 不开或信任错了节点,会让上游看到错的 IP,影响排障。open_file_cache
打开过大会吃内存,按业务实际静态文件量调。geoip 模块加载没加
就用了 geoip_city,启动会失败。这点主要在编译期避免,但偶尔会因为动态模块没载入出现。不论做什么变更,验证都得能列出明确的“通过 / 失败”标准。
bash
nginx -t && nginx -s reload ps -ef --forest | grep nginx # 期望:master + 新 worker,worker 的 lstart 是 reload 时间之后 取最近 5 分钟状态码分布,对比变更前:
bash
# 变更后采样 tail -n 50000 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn # 对比变更前的样本 502 / 504 比例应当下降而不是简单从绝对值看。生产建议:5 分钟窗口 5xx 比例回到基线 ±50% 内。
bash
ss -antp | grep ':8080' | wc -l 变更后保持稳定不抖动。
bash
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
不要超过 systemd LimitNOFILE 的 70%。
bash
openssl 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 bash
nginx -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:
bash
nginx -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 的顺序切换 bash
systemctl 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 bash
sysctl -p /etc/sysctl.conf # 或撤销指定参数 sysctl -w net.core.somaxconn=128 执行回滚后必须重新进入“验证方式”一遍流程,确认报错不再上升。
下面这几条是上线新 Nginx、反代新上游、改 TLS 时的硬性要求,建议在团队 wiki 沉淀成 checklist。
worker_processes / worker_connections。conf.d/ 下的 *.conf 文件或主 nginx.conf 中 include 引入。error_log ... debug 会记录每个请求的 header,生产打爆磁盘。proxy_buffer_size / proxy_buffers 不应盲目抄网上配置
,应按 body_bytes_sent 分布调整。nginx_status 务必要 deny all + allow 监控网段
。它会泄露活动连接数。server_tokens off 一定开
。泄露版本号等于给扫描器提供现成 CVE 表。client_max_body_size
按业务需求设,不要给 0(不限制)。logrotate.d 配置要确认能跑,避免磁盘被 access.log 打爆。把这一次的排错经验归纳一下,三件事最重要:
openssl s_client 实测。下一步如果要把这套流程变成团队级的能力,建议把它包装成一个 playbook / runbook:把命令写好、把变量(如 pid、上游网段)做成可参数化的脚本,接到告警里让它半自动跑。我自己目前在阿里云 / 自建机房两套环境上都还保留这一套模板的实例,配上 Alertmanager 做 7×24 半自动响应,新人上手一个季度内基本就能独立处理 7 成以上的“Nginx 报错”类工单。
线上最容易被当成“Nginx 报错”的,其实是 TLS 握手失败。客户端在浏览器里看到 ERR_SSL_PROTOCOL_ERROR、ERR_SSL_BAD_RECORD_MAC_ALERT、SSL_ERROR_SYSCALL 等一系列报错,第一反应都是“是不是 Nginx 出问题”。所以把 TLS 这一条单独拆出来讲一讲。
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 |
文件损坏 / 路径错误 | 私钥文件内容格式坏、或路径权限不对 |
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 nginx
upstream 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; } }
error.log 出现 ssl_stapling 相关报错、或线上检测 OCSP stapling 状态为不可用时,按下面顺序排查:
nginx -V
看 OpenSSL 是否大于 1.0.2(OCSP basic 必须 >= 1.0.1f,stapling 通常需要 >= 1.0.2)。resolver 必须配置。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'
线上不少业务升级到 HTTP/2 后,又出现“能连接但请求失败”的诡异报错。最常见的是 Connection reset by peer 或 HTTP/2 stream id 不匹配。这一节把可能踩到的坑列一下。
http2 与 proxy_http_version 1.1 必须同时配置nginx
server { 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 下设置 } } 少了任意一行,上游会直接报协议错误。
keepalive 共享连接池。GOAWAY 关闭 → Nginx 处类似 RST。排查时仍然以 access.log 的状态码做主导,512 byte 的状态码含义同 HTTP/1.1。
H2C(HTTP/2 over cleartext)反代场景一般出现在反代 grpc-java、grpc-go 服务时:
nginx
server { 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 长请求是 HTTP/2 stream 持续发送数据:
nginx
grpc_read_timeout 600s; # 给上游足够时间 send_timeout 600s; keepalive_timeout 600s; 参数具体值依业务而定,但思路就是:gRPC 长请求要把所有 timeout 全部放宽,proxy_buffering 等缓冲类配置视情况关闭。
client_max_body_size 设小了,浏览器直接报 413;超过限制但 Nginx 又卡在上行,会变成 504。这是线上踩坑最多的上传场景之一。
nginx
server { 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
不小于业务最长请求时长。client_body_temp_path,监控磁盘空间。client_body_in_file_only on
时必须确保 proxy_pass 上游能 streaming 接收,否则反而会成更大风险。proxy_buffering,会和 disk buffering 冲突。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 内全部走通,性能不会因为文件大小失控。
WebSocket 默认是 hold 住的,必须把 Nginx 的 read timeout 拉长。
nginx
location /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 关掉。
proxy_read_timeout 太短。upstream prematurely closed 是上游 WebSocket 主动断了。/proc/net/tcp 看 ESTAB 是否占着。
下面是文章所有示例合并起来的最终版 main / http / upstream / server 段。线上根据真实业务在 /etc/nginx/conf.d/ 下拆 .conf 文件即可。
nginx
user 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; } 放在这里只是为了后续维护方便单独提取——和文章正文示例保持一致。
下面是我在生产中遇到过的真实工单的完整排查过程。读者对照前面几节的命令和判断逻辑,可以一眼看到整个闭环。
502 大概率是上游层(upstream)。
bash
tail -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" bash
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 # worker pid=12412 fd=20120 # worker pid=12413 fd=20453 # worker pid=12414 fd=18800 # worker pid=12415 fd=17842
FD 数字约 2 万,已经接近 worker_rlimit_nofile 但不超限。再看上游:
bash
ss -antp | grep ':8080' | wc -l # 9500
连接数偏高,是因为 proxy_next_upstream 启用了重试,每个失败请求变成 2 个连接池占位。
更深一层看:
bash
grep -A 5 upstream /etc/nginx/conf.d/backend.conf # 没有 keepalive
加上 keepalive 64;:
nginx
upstream 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; } bash
nginx -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%,符合业务基线 bash
cp -a /etc/nginx/conf.d/backend.conf.bak.YYYYMMDD-HHMMSS /etc/nginx/conf.d/backend.conf nginx -t && nginx -s reload
keepalive 没启用是很多老配置里的硬伤。
499 是客户端主动关。可能是客户端 HTTP 超时太短,或者上游响应慢让客户端等不住。
bash
awk '{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 # ...
upstream_response_time 出现大量 > 1s,甚至 9 秒。
上游 Java 应用某个接口慢,但 Nginx 端没有把慢请求切走(proxy_next_upstream 看到的是 error timeout,不是慢)。
在上游侧增加熔断 + 慢请求报警;Nginx 这边加:
nginx
proxy_next_upstream error timeout http_502 http_503 http_504 non_idempotent;
加 non_idempotent 后,重试判定会更严格,POST/PUT 不再重试,减少重复提交风险。
同 F.1.7 / F.1.8。
nginx.conf,reload 后配置生效看起来“诡异”。bash
nginx -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 worker pid 时间戳没刷新。
是用 kill -HUP 而不是 nginx -s reload。在某些发行版 / systemd unit 下,HUP 信号会被 systemd 截获并忽略。
按 systemd 重启流程:
bash
systemctl reload nginx 或者直接:
bash
nginx -s reload
如果 nginx 不在 path,则用 /usr/sbin/nginx -s reload 或 $(which nginx) -s reload。
worker 进程时间戳刷新,配置生效。
为了方便“在事故现场打开就能用”,把全文章的命令汇总成一个清单:
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
A:99% 是 include 顺序问题。例如 /etc/nginx/conf.d/ 下有两个 *.conf,B 后加载就覆盖 A 的 server_name。解决方案:明确拆分文件名如 10-backend.conf、20-static.conf、数字越小的优先级越高(按字母序)。
A:可能是 worker_processes 被改过,或多 master 切换没完成。pstree -p $$ 看进程树,再 pkill -f 'nginx: master' 重新启动。
A:access_log 写权限丢失、被覆盖写到别的路径、或 buffer 没刷盘,定位:
bash
ls -l /var/log/nginx/ lsof | grep -E 'access.log|error.log' worker process X exited on signal 9 (SIGKILL)。
A:worker 被系统 OOM killer 杀掉,看 dmesg | grep -i oom 与 free -h,必要时调整 worker_rlimit_nofile 或降低 worker_connections。
recv() failed (104: Connection reset by peer)。
A:通常是上游反向连接(如动态 upstream 域名)解析结果抖动。固定上游 IP 列表 / 配置 resolver + 合理 resolver_timeout 即可。
A:很可能是 sub_filter 或 proxy_buffer_size 不够、被截断。看 proxy_buffer_size / proxy_buffers 调整;或临时关 proxy_buffering 看完整 body。
A:F5 不改源 IP 时,要让 Nginx 用 set_real_ip_from 信任头:
nginx
set_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;
A:通常配置了 geoip / map 大规则集、或 limit_req_zone 非常大。limit_req_zone $binary_remote_addr zone=one:10m; 这样的 10m 内存对应 16 万 IP。再大就要考虑用 Redis 共享存储做限流。
A:HTTP/3 必须 OpenSSL >= 1.1.1 且要用 HTTP/3 替代品如 OpenResty / Cloudflare 的支持。普通 Nginx 不支持 QUIC。要么启用 HTTP/2,要么换发行版。
nginx -s reload 报错 nginx: [error] invalid PID number "" in "/var/run/nginx.pid"。
A:配置文件里改动了 pid 路径、或者 nginx 实际运行状态没在 /var/run/nginx.pid 里。检查实际进程 pgrep -f 'nginx: master'。
线上 Nginx 接 Prometheus 是常见做法。下面给一个最小化可用的告警配置思路,让读者把指标与告警对齐业务基线。
通过模块 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 |
上游连接、首包延迟 |
yaml
groups: - 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 }}"
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 高频告警" 注意阈值要按真实业务基线调整;不要盲目套阈值。
如果想用 CI/CD 推动 Nginx 配置文件统一管理,可参考下面思路。
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 加载。
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,并要求输出为空,否则构建失败。
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 上,灰度按机房分布组即可。
灰度发布后 5 分钟内观察:
error.log
关键字 error|emerg|alert|crit 数量nginx_connections_active
与 accepts/handled 比值任意一项明显恶化,按预案立刻回滚。
要让线上 Nginx 不被“不会扩容”害到,建议在容量基线做完之后,再用压测复测一遍。
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 适合复杂场景 根据压测结果估算:
RPS * worker_processes ≈ 整机 RPSworker_connections / worker_processesproxy_buffering / gzip / gzip_static说明:以上每条改动都要重测线上代表样本,不能不基于数据就调。
下面几种是新手最常踩、但报错不显眼的情况,单独拧成一节。
Too many open files 突然出现排查:
bash
cat /proc/$(pgrep -f 'nginx: master')/limits | grep 'open files' sysctl fs.file-max
生产建议把 fs.file-max >= 200000,把每个 worker 进程 LimitNOFILE 拉到 100000 起。
accept: too many open files
这是 accept() 系统调用返回 EMFILE,进程 FD 耗尽。处理思路同 L.1。
failed (24: Too many open files) while connecting to upstream
上游连接的 socket 也是 FD。要保证 worker FD 上限足够,且 worker_connections 不能无限放大。
ip_local_port_range 默认 32768 60999,总可用端口约 28000。如果同时打到上游的连接数长时间 > 28000,会出现端口分配失败。Linux 上同一个 ephemeral 端口 60 秒内不能重复使用,因此短连接场景要把范围拉大:
bash
sysctl net.ipv4.ip_local_port_range=1024 65000 sysctl tcp_tw_reuse=1 vs tcp_tw_recycle
tcp_tw_recycle 在 Linux 4.12 之后默认不再生效,且曾经在 NAT 场景下导致过诡异 bug。建议不再使用它,业务上控制短连接比调整它更直接。
如果 systemd unit 写了 LimitNOFILE=100000,但用户层 ulimit -n 1024,master 启动后会用 unit 设置的值。我建议团队在 Ansible / Terraform 模板里统一管这两个值。
这是给自己和团队一年的实战经验最终汇总出来的一张表。读者把它打印贴在工位旁边也合适。
| 现象 | 第一判断 | 关键命令 | 第二步动作 |
|---|---|---|---|
| 大面积 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)发给有经验的同行,不一定是自己一个人扛到底。
为了让读者在“凌晨三点被叫醒”时能更快行动,按关键字给出最短的应急步骤,全部是命令驱动的。
[emerg](启动失败)nginx: [emerg]nginx -t 找出报错行;通常配置加载失败,/etc/nginx/nginx.conf 语法问题、include 文件缺失、证书路径错误。bind() to 0.0.0.0:80 failed (98: Address already in use)bash
ss -lntp | grep ':80 ' fuser -k 80/tcp # 仅排查用,生产慎用 systemctl stop <占用进程> systemctl start nginx connect() failed (113: No route to host)ip route / traceroute 10.0.0.11;找网工 / 云厂家。connect() failed (110: Connection timed out)upstream timed outproxy_read_timeout,第二步看上游 GC、DB、Cascading IO。upstream prematurely closed connectionclient closed connection while ...proxy_*_timeout、调大客户端的连接保持时长。SSL_do_handshake() failedopenssl s_client -connect ... 复测,按上面附录 A 流程。worker process xxx exited on signal 9dmesg / free -h;检查 systemd MemoryMax / OOMScoreAdjust。worker process xxx exited on signal 11malloc() failedworker_connections、gzip、proxy_buffer_size,检查是否 map 引入爆炸性内存。accept4() failed (28: No space left on device)ip_local_port_range;减少 keepalive 等待。Too many open filesrewrite or internal redirection cyclelast / break 是否用错,正则是否能命中。conflicting server name "xxx"
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 让上下游日志串起来:
nginx
log_format main '$request_id $remote_addr ...';
并配合 proxy_set_header X-Request-ID $request_id; 把 ID 透传给上游。
全部0条评论
快来发表一下你的评论吧 !