Nginx 几乎是所有 Web 业务的入口:从 CDN 边缘到四层 LB,再到七层反代和静态资源服务器。一旦它自己出问题,影响面会非常大。但相对而言,Nginx 的日志字段非常规范,排障时完全可以“按图索骥”——只要把日志格式、字段含义、4xx/5xx 状态码以及各类超时参数搞清楚,80% 的故障都能直接在日志里看出来。
这篇文章分四块来讲:
- Nginx 日志字段详解(access_log、error_log)。
- 各类超时(连接、发送、读取、上游)的真实根因。
- 4xx / 5xx 状态码的排查路径。
- 日志分析的常用命令、脚本与工具链。
不写花里胡哨的,每一条都对应生产上能直接用的命令和配置。在深入细节前,不妨先想想日常维护中是否也碰到过类似的日志排查痛点?带着具体场景往下看,效果会更好。
一、access_log 字段详解
1.1 默认日志格式
Nginx 默认的 combined 格式如下:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
输出形如:
10.0.1.10 - - [12/Jun/2026:10:00:00 +0800] "GET /api/v1/users HTTP/1.1" 200 1234 "-" "curl/7.79.1"
1.2 推荐的生产日志格式
线上如果只靠 combined,能拿到的信息太少,看不到请求延迟、上游地址和客户端真实 IP。更推荐自定义一套:
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time" '
'req_id=$request_id '
'fwd="$http_x_forwarded_for" scheme="$scheme" '
'host="$host" uri="$request_uri"';
每条字段的含义:
$remote_addr:客户端 IP(如果前方还有反代层,需要结合 $http_x_forwarded_for 来看)。
$remote_user:HTTP Basic Auth 的用户名。
$time_local:服务器本地时间。
$request:完整请求行(方法 + URI + 协议)。
$status:HTTP 状态码。
$body_bytes_sent:响应体字节数,不包含 header。
$http_referer:Referer 头。
$http_user_agent:UA。
$request_time:请求处理总时间(秒,毫秒精度),从 Nginx 收到第一个字节到发送完最后一个字节。
$upstream_connect_time:与上游建立连接的时间,多个上游用逗号分隔。
$upstream_header_time:建立连接并收到上游第一个字节的用时。
$upstream_response_time:与上游交互的总耗时。
$request_id:Nginx 生成的唯一 ID(需 Nginx 1.11+),可串联整个调用链。
$http_x_forwarded_for:客户端真实 IP(如果有)。
$scheme:HTTP 或 HTTPS。
$host:请求的 Host 头。
$request_uri:完整 URL(含 query string)。
1.3 关键字段的取值范围
$request_time 一般在 0.001~0.500 之间,超过 1.0 秒基本就属于慢请求了。
$upstream_response_time 理应变小,通常应 ≤ $request_time。
$upstream_connect_time 正常是 0.000~0.005 秒,超过 0.050 秒就要怀疑网络层面有问题。
$status 里的 4xx/5xx 是重点关注对象。
1.4 error_log 字段
error_log /var/log/nginx/error.log warn; 的输出像这样:
2026/06/12 10:00:00 [error] 1234#0: *5678 upstream timed out (110: Connection timed out) while connecting to upstream, client: 10.0.1.10, server: app.example.com, request: "GET /api/v1/users HTTP/1.1", upstream: "http://10.0.2.10:8080", host: "app.example.com"
error_log 的日志级别依次是 debug | info | notice | warn | error | crit | alert | emerg。线上一般用 warn,调试时再调到 info 或 debug。需要注意,debug 级别会暴露请求体与响应体,性能开销很大,务必谨慎使用。
error_log 里常见的故障关键字:
connect() failed:连不上上游。
upstream timed out:上游超时。
no live upstreams:所有上游均不可用。
client sent invalid header line:客户端发出非法 header。
SSL_do_handshake():SSL 握手失败。
worker connections are not enough:连接数耗尽。
accept() failed (24: Too many open files):文件描述符耗尽。
cache miss / cache hit:缓存相关。
limit req:触发限流。
二、超时配置全景
Nginx 的超时分布在四个层面:客户端、Nginx 自身、上游以及 FastCGI。下面一次性把相关的 timeout 指令理清楚。
2.1 客户端层
client_body_timeout 30s; # 客户端发送请求体的超时
client_header_timeout 30s; # 客户端发送请求头的超时
send_timeout 30s; # Nginx 向客户端发送响应的超时(两次写入之间)
keepalive_timeout 65s; # 长连接保持时间
keepalive_requests 100; # 长连接最多处理多少请求
client_max_body_size 50m; # 客户端请求体最大
client_body_buffer_size 128k; # 请求体缓冲区
client_header_buffer_size 1k; # 请求头缓冲区
large_client_header_buffers 4 8k; # 大请求头缓冲区
client_body_timeout 和 client_header_timeout 都属于“连接空闲超时”,也就是说,过了这个时间客户端还没发完数据,Nginx 就会主动断开连接。
2.2 上游层
upstream backend {
server 10.0.2.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.2.11:8080 max_fails=3 fail_timeout=30s;
keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}
server {
location / {
proxy_connect_timeout 5s; # 与上游建立连接的超时
proxy_send_timeout 60s; # 向上游发送请求的超时
proxy_read_timeout 60s; # 从上游读响应的超时
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 30s;
}
}
四个 timeout 的区别:
proxy_connect_timeout:TCP 三次握手的总时长。生产上一般设 1~5 秒。
proxy_send_timeout:Nginx 向上游发请求时“两次写操作之间”的超时。一般 30~60 秒。
proxy_read_timeout:Nginx 等上游响应时“两次读操作之间”的超时。常规业务 30~60 秒,长连接业务要调大。
proxy_next_upstream_tries:失败重试次数(含原始请求,总共即 1 + retries)。
风险提示:proxy_next_upstream 配置不当会放大故障。比如设为 proxy_next_upstream error timeout 时,所有超时都会触发重试;对于 POST、PUT 等非幂等接口,默认不应重试。
2.3 FastCGI / uwsgi 层
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_connect_timeout 5s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
FastCGI 的三类 timeout 与 PHP-FPM 的 request_terminate_timeout 是对应的。
2.4 Socket 层
server {
listen 80 backlog=2048;
deferred on;
reuseport on;
}
backlog 是 listen 队列长度;deferred 让三次握手最后一步(ACK)与数据合在一起发送。reuseport 允许多个 worker 共享同一端口,能明显提高并发能力。
三、4xx 状态码排查
3.1 400 Bad Request
常见原因:
- 请求头格式有误。
- 请求体过大,超过
client_max_body_size。
- URL 编码错误(比如
%xx 非法)。
- HTTP/0.9 协议被严格化拒绝。
- SSL 握手后没有发 HTTP 请求,而是继续发了 TLS 数据。
排查命令:
# 看 400 集中在哪些 URL
awk '$9 == 400 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 看 400 集中在哪些 IP
awk '$9 == 400 {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
修复思路:
- 调大
client_max_body_size。
- 调大
client_header_buffer_size 或 large_client_header_buffers。
- 如果有非标准客户端发 HTTP/0.9,可在
http {} 内加 allow_http09 on;(仅限内网环境)。
3.2 401 Unauthorized
常见原因:
- 没带 Authorization 头。
- Authorization 头格式错误。
- 用户 token 已过期。
- Basic Auth 用户名或密码错误。
排查:
# 看哪些 URL 401
awk '$9 == 401 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
3.3 403 Forbidden
常见原因:
- 目录列表被关闭(
autoindex off),却访问了目录。
- IP 进了黑名单(
deny)。
- 文件权限问题,Nginx 运行用户无权读取。
- URL 命中了某个
location 里的 deny all。
limit_req 触发后在某些配置下返回 403(实际多半是 503,具体看配置)。
- SELinux 阻止了访问(
/var/log/audit/audit.log 中可见 avc denied)。
- 客户端证书验证失败。
排查命令:
# 详细看一条 403
grep ' 403 ' /var/log/nginx/access.log | tail -1
# 看 error_log 中 403 的相关记录
grep -i 'permission denied\|denied' /var/log/nginx/error.log
3.4 404 Not Found
常见原因:
- 路径打错。
- 静态文件确实不存在。
- 反代后端没有这个路径。
try_files 配错。
- 反代时没有加上
$request_uri,导致上游只看到根路径。
排查:
# 看 404 最多的路径
awk '$9 == 404 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
3.5 405 Method Not Allowed
常见原因:
- 用 POST 请求了静态文件。
- 反代配置中,上游只支持 GET。
limit_except 配置了未包含的方法。
3.6 408 Request Timeout
Nginx 自己超时断开请求,多半是客户端发请求太慢、请求体太慢,或者 keep-alive 客户端长时间不发新请求。
排查:
# 在 error_log 中找
grep -i 'client timed out' /var/log/nginx/error.log
3.7 413 Payload Too Large
请求体超过 client_max_body_size。文件上传类业务务必调大。
修复:
client_max_body_size 200m;
3.8 414 URI Too Long
URL 超过 large_client_header_buffers 的总容量。
large_client_header_buffers 4 16k;
风险提示:调得太大后,可能被攻击者利用超长 URL 消耗 CPU 和内存。
3.9 429 Too Many Requests
触发 limit_req 或 limit_conn 导致。
排查:
# error_log 中找
grep -i 'limiting requests' /var/log/nginx/error.log
3.10 499 Client Closed Request
这是 Nginx 特有的状态码(非 HTTP 标准),表示客户端主动断开连接。常见于:
- 客户端自身超时并主动断开。
- 用户在浏览器中点了停止。
- 移动端 App 因网络问题断连。
若 499 突然飙升,可结合 request_time 来看——大概率是因为响应太慢,客户端等不及就断开了。
四、5xx 状态码排查
4.1 500 Internal Server Error
一般是 Nginx 自身或上游应用异常,例如:
proxy_pass 的后端返回 500。
return 指令搭配错误。
if 指令内语法错误(Nginx 配置中 if 是出名的“坑”)。
- 模块加载失败。
排查:
grep '\[error\]' /var/log/nginx/error.log | tail -50
Nginx 不会改写上游的 5xx 状态码,看到 500 必须先看上游日志。
4.2 502 Bad Gateway
最常碰到的 5xx 之一,意味着 Nginx 连不上上游,或者上游异常关闭了连接。
根因包括:
- 上游服务没启起来。
- 上游端口被防火墙挡掉了。
- 上游进程崩溃但未正确关闭连接。
proxy_pass 配错了 IP 或端口。
- 上游 listen 在 127.0.0.1,但 Nginx 在其他机器上。
- 上游负载过高,accept 队列被打满。
排查命令:
# 看 502 集中在哪些上游
awk '$9 == 502 {print $7, $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
# error_log
grep -i 'connect() failed' /var/log/nginx/error.log | tail -20
4.3 503 Service Unavailable
Nginx 找不到可用的上游(所有上游均被 max_fails 标记为 down),或上游主动返回了 503。
awk '$9 == 503 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
4.4 504 Gateway Timeout
Nginx 等待上游响应超时。proxy_read_timeout 配得过短,或者上游确实太慢,都会引发 504。
awk '$9 == 504 {print $7, $14}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
其中 $14 是 request_time。若 504 时 $request_time 已接近 proxy_read_timeout,基本确认是上游慢。
4.5 502 与 504 的区别
这也是面试常被问到的:
- 502:Nginx 与上游在握手阶段就失败了,或者上游主动断开(已收到 FIN,但状态不对)。
- 504:Nginx 与上游握手成功并发了请求,但上游迟迟不响应(在 read 阶段超时)。
判断方法就是看 $upstream_connect_time 和 $upstream_response_time:
- 502 时
upstream_connect_time 经常是 -(根本连不上)。
- 504 时
upstream_connect_time 正常,而 upstream_response_time 等于 proxy_read_timeout。
4.6 5xx 案例速查表
| 状态码 |
含义 |
常见根因 |
排查方向 |
| 500 |
上游应用异常 |
上游 bug、配置错误 |
看上游应用日志 |
| 502 |
连不上上游 |
上游进程挂、防火墙挡、IP/端口错 |
curl 上游、查 listen、防火墙 |
| 503 |
上游主动拒绝 |
触发限流、过载保护、维护模式 |
看 upstream 的 max_fails、连接池 |
| 504 |
上游响应慢 |
上游慢、proxy_read_timeout 太短 |
看上游 P99 延迟、调大 read_timeout |
| 499 |
客户端主动断 |
客户端超时、用户操作中断 |
看 $request_time,定位慢接口 |
五、超时故障的完整案例
5.1 案例:上游 Tomcat 慢导致 504
现象:每分钟出现 200~300 个 504,业务侧反馈“有部分用户加载慢”。
排查:
# 看 504 的 rt 分布
awk '$9 == 504 {print $14}' /var/log/nginx/access.log | sort -n | tail
# 看上游响应时间
awk '$9 == 504 {print $18}' /var/log/nginx/access.log | sort -n | tail
发现 504 的 request_time 全部集中在 60 秒。
根因:Tomcat 端某个慢查询接口 P99 超过了 60 秒,而 Nginx 配的 proxy_read_timeout 60s 直接就把请求关掉了。
修复:
- 短期:将
proxy_read_timeout 调到 120 秒。
- 中期:Tomcat 端优化 SQL。
- 长期:上游引入熔断机制,对慢请求主动 reject,而不是拖到超时。
5.2 案例:上游 accept 队列满导致 502
现象:高并发场景下,502 集中在某个 Tomcat 节点。
排查:
# 在 Nginx 上
awk '$9 == 502 {print $1, $7, $NF}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
# NF 倒数第二个字段是 upstream address
# 在 Tomcat 上
ss -lnt 'sport = :8080' | head
netstat -s | grep -i listen
在 Tomcat 端,netstat -s 出现大量 SYNs to LISTEN sockets dropped,accept 队列溢出。
根因:Tomcat 接收请求的速度跟不上,连接被 Linux 内核直接丢弃。
修复:
- 调大 Tomcat 的
acceptCount。
- 调大 Linux 的
somaxconn 和 tcp_max_syn_backlog。
- 增加 Tomcat 节点。
5.3 案例:worker_connections 耗尽
现象:高峰期出现 502,error.log 里大量 worker connections are not enough。
根因:Nginx worker 进程的最大连接数耗尽。总连接数 = worker_processes * worker_connections / 2(每个连接在客户端和上游各算一个)。
# 看当前连接数
ss -s
# 看 Nginx 的连接数
curl http://127.0.0.1/nginx_status
修复:
worker_processes auto; # 跟 CPU 核数
worker_rlimit_nofile 65535;
events {
worker_connections 40960;
use epoll;
multi_accept on;
}
5.4 案例:keep-alive 没复用导致 502
现象:上游连接数爆表,Tomcat 报 “connection pool exhausted”。
根因:Nginx 默认每个请求都和上游建立新连接,高并发下会迅速把上游的连接池打满。
修复:
upstream backend {
server 10.0.2.10:8080;
keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
这里的 keepalive 64 指的是每个 worker 复用 64 个连接。
5.5 案例:proxy_buffer 配错导致响应截断
现象:返回的 JSON 或 HTML 不完整,状态码虽然是 200,但客户端解析报错。
根因:proxy_buffer_size 或 proxy_buffers 太小,上游响应还没读完就被 Nginx 先发出去了。
排查:
# 客户端能复现:curl 看到截断
curl -v http://app.example.com/api/big-data
# Nginx error_log
grep -i 'upstream sent too big header' /var/log/nginx/error.log
修复:
proxy_buffer_size 16k;
proxy_buffers 4 64k;
proxy_busy_buffers_size 128k;
proxy_temp_path /var/cache/nginx/proxy_temp 1 2;
proxy_max_temp_file_size 1024m;
5.6 案例:磁盘满导致 502
现象:突然间所有接口都 502,Nginx 进程还在。
根因:磁盘 100% 写满,Nginx 写入临时文件失败。
df -h
du -sh /var/log/nginx/* | sort -h | tail
修复:
- 立即清理:
truncate -s 0 /var/log/nginx/access.log。
- 调整 logrotate,避免日志再次写满。
- 长期:监控磁盘水位,提前扩容。
5.7 案例:DNS 解析超时导致 502
现象:上游配置中用的是域名(如 upstream backend { server backend.example.com:8080; }),重启后出现 502。
根因:Nginx 启动时只解析一次域名,之后不主动重新解析。域名对应的 IP 变化后,Nginx 仍指向旧 IP。
修复:
resolver 10.0.0.1 10.0.0.2 valid=30s;
upstream backend {
server backend.example.com:8080;
}
Nginx 1.17+ 支持 resolver 指令,可定期重新解析。
5.8 案例:keepalive_timeout 与 send_timeout 冲突
现象:客户端用长连接拉数据,每隔 30 秒就断一次。
排查:Nginx 配置为 keepalive_timeout 30s; send_timeout 30s;,客户端在没有新请求的情况下,30 秒后就被踢掉。
修复:把 send_timeout 调到 60 秒或更高,或让客户端保持活跃。
5.9 案例:limit_req 模块异常
现象:明明没有触发限流,却也返回 503。
排查:limit_req_zone 的 burst 太小,且没有设 nodelay,导致请求排队超出 burst 时就返回 503。
# 错误
limit_req zone=api burst=5;
# 修复
limit_req zone=api burst=20 nodelay;
六、access_log 实战分析
6.1 QPS 与带宽
# 每秒请求数
awk '{print $4}' /var/log/nginx/access.log | cut -d: -f1-3 | uniq -c | awk '{print $2, $1}'
# 当天 PV
awk '$4 ~ /12\/Jun\/2026/' /var/log/nginx/access.log | wc -l
# 当天独立 IP
awk '$4 ~ /12\/Jun\/2026/ {print $1}' /var/log/nginx/access.log | sort -u | wc -l
# 带宽估算(粗略)
awk '{sum += $10} END {print sum/1024/1024, "MB"}' /var/log/nginx/access.log
6.2 状态码分布
# 状态码分布
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
# 4xx 占比
awk '$9 ~ /^4/ {c4++} END {print c4/NR*100"%"}' /var/log/nginx/access.log
# 5xx 占比
awk '$9 ~ /^5/ {c5++} END {print c5/NR*100"%"}' /var/log/nginx/access.log
6.3 慢 URL 排行
# 慢接口
awk '{print $14, $7}' /var/log/nginx/access.log | sort -rn | head -20
# $14 是 $request_time
# 慢调用
awk '{print $NF, $7}' /var/log/nginx/access.log | sort -rn | head
# $NF 是 $upstream_response_time
注意:$request_time 在 combined 格式里不一定在第 14 字段,请按你实际定义的日志格式调整 awk 的字段索引。
6.4 Top IP / URL
# QPS 最高的 IP
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 请求最多的 URL
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 错误最多的 IP
awk '$9 ~ /^5/ {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
6.5 找爬虫
# UA 分布
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 高频爬虫
awk -F'"' '$6 ~ /spider|bot|crawl|spider/i {print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
6.6 找异常请求
# 空 UA
awk -F'"' '$6 == "-" {print $1}' /var/log/nginx/access.log | head
# 异常 HTTP 方法
awk '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
# 异常 URL
awk '{print $7}' /var/log/nginx/access.log | grep -E 'etc/passwd|admin|<script' | head
七、日志分析工具链
7.1 goaccess
goaccess 是一款实时 Web 日志分析器,能在终端里直接展示统计图:
# 安装
yum install -y goaccess
# 实时分析
goaccess -f /var/log/nginx/access.log -c
# 生成 HTML 报表
goaccess /var/log/nginx/access.log -o /var/www/report.html -c
在实时模式下能清晰看到:请求总量、独立访客、带宽、状态码分布、Top IP/URL/Referer/UA、慢请求排行以及按小时的请求量。
7.2 ELK / OpenSearch
生产环境通常会采用 ELK(Elasticsearch + Logstash + Kibana)来做日志聚合。
/etc/logstash/conf.d/nginx.conf:
input {
file {
path => "/var/log/nginx/access.log"
start_position => "beginning"
sincedb_path => "/var/lib/logstash/sincedb"
}
}
filter {
grok {
match => { "message" => "%{COMBINEDAPACHELOG}" }
}
date {
match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z" ]
}
geoip {
source => "clientip"
}
}
output {
elasticsearch {
hosts => ["es-1:9200", "es-2:9200"]
index => "nginx-access-%{+YYYY.MM.dd}"
}
}
在 Kibana 上就可以搭建 Dashboard,用来观察状态码分布、P95/P99 延迟、Top URL 及地理分布等。
7.3 PLG(Promtail + Loki + Grafana)
如果要一套轻量级的 ELK 替代方案,不妨试试 PLG。资源占用要比 ELK 小不少。
/etc/promtail/config.yml:
server:
http_listen_port: 9080
positions:
filename: /var/lib/promtail/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: nginx
static_configs:
- targets:
- localhost
labels:
job: nginx-access
host: $(hostname)
__path__: /var/log/nginx/*.log
pipeline_stages:
- regex:
expression: '^(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<proto>[^"]+)" (?P<status>\d+) (?P<bytes>\d+)'
- labels:
status:
method:
path:
Grafana 上使用 {job="nginx-access"} 进行查询,再结合 LogQL 就能做各种聚合分析。
7.4 实时分析脚本
自己动手写一个小脚本,也能实现实时 QPS 监控:
#!/bin/bash
# /usr/local/bin/lvs_qps.sh
LOG=/var/log/nginx/access.log
TICK=1
while true; do
N=$(wc -l < $LOG)
echo "$(date +%T) qps_total=$N"
sleep $TICK
done
如果想更精细,可以按状态码分类:
#!/bin/bash
LOG=/var/log/nginx/access.log
SLEEP=5
declare -A LAST
while true; do
while read line; do
code=$(echo "$line" | awk '{print $9}')
LAST[$code]=$((${LAST[$code]:-0} + 1))
done < <(tail -n 1000 $LOG | awk -v last_time=$(date -d "$LAST_DATE" +%s 2>/dev/null || echo 0) '$4 ~ /12\/Jun/')
echo "===== $(date +%T) ====="
for k in "${!LAST[@]}"; do
echo " $k: ${LAST[$k]}"
done
sleep $SLEEP
done
7.5 awk 高级分析案例
按 URL 聚合 P99 延迟:
#!/bin/bash
# 计算每个 URL 的 P99 延迟
awk '{
url=$7
rt=$14
if (rt ~ /^[0-9.]+$/) {
sum[url] += rt
cnt[url]++
data[url, cnt[url]] = rt
}
}
END {
for (url in cnt) {
n = cnt[url]
# 简单排序
for (i = 1; i <= n; i++) arr[i] = data[url, i]
for (i = 1; i <= n; i++) for (j = i+1; j <= n; j++) if (arr[i] > arr[j]) { t = arr[i]; arr[i] = arr[j]; arr[j] = t }
p99 = arr[int(n*0.99)+1]
avg = sum[url] / n
printf "%-40s avg=%.3fs p99=%.3fs n=%d\n", url, avg, p99, n
}
}' /var/log/nginx/access.log | sort -k3 -rn | head -20
八、日志切割与归档
8.1 logrotate 配置
/etc/logrotate.d/nginx:
/var/log/nginx/*.log {
daily
rotate 30
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
# 通知 Nginx 重新打开日志文件
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
endscript
}
关键点:
postrotate 里用 kill -USR1 让 Nginx 重新打开日志文件。千万不要用 nginx -s reload(会断开连接)。
sharedscripts 保证 postrotate 只执行一次(尤其是在匹配多个文件时)。
风险提示:kill -USR1 不会丢失请求,但日志文件改名瞬间的并发写入,最好提前测试一下。Nginx 收到 USR1 后会 close 旧的 fd 并 open 新 fd,整个切换是原子的。
8.2 日志归档与压缩
# 切割后的日志用 xz 压缩
xz /var/log/nginx/access.log-20260612
# 归档到对象存储
aws s3 cp /var/log/nginx/ s3://log-archive/nginx/ --recursive --exclude "*" --include "*.gz"
# 清理 90 天前的日志
find /var/log/nginx/ -name "*.gz" -mtime +90 -delete
8.3 日志权限
chown -R nginx:nginx /var/log/nginx/
chmod 640 /var/log/nginx/*.log
避免其他用户读取日志,防止泄露用户敏感数据。
九、监控与告警
9.1 关键指标
nginx_http_requests_total:总请求数。
nginx_http_5xx_responses_total:5xx 响应数。
nginx_http_4xx_responses_total:4xx 响应数。
nginx_http_request_duration_seconds_bucket:延迟直方图。
nginx_connections_*:连接数。
nginx_upstream_peers健康:upstream 健康状态。
9.2 nginx-prometheus-exporter
nginx-prometheus-exporter -nginx.scrape-uri=http://localhost/stub_status
Nginx 端需要开启 stub_status:
server {
listen 127.0.0.1:80;
location /stub_status {
stub_status on;
access_log off;
}
}
9.3 告警规则
groups:
- name: nginx
rules:
- alert: Nginx5xxHigh
expr: rate(nginx_http_responses_total{status=~"5.."}[5m]) > 10
for: 5m
labels:
severity: critical
annotations:
summary: "Nginx 5xx 超过阈值"
- alert: Nginx4xxHigh
expr: rate(nginx_http_responses_total{status=~"4.."}[5m]) > 100
for: 5m
labels:
severity: warning
- alert: NginxConnectionsHigh
expr: nginx_connections_active / nginx_connections_accepted > 0.9
for: 5m
labels:
severity: warning
- alert: NginxUpstreamDown
expr: nginx_upstream_check_up{status="down"} == 1
for: 1m
labels:
severity: critical
阈值需要根据业务基线再次调整。
十、配置模板
10.1 通用反代模板
upstream backend {
least_conn;
server 10.0.2.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.2.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.2.12:8080 max_fails=3 fail_timeout=30s;
keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}
server {
listen 80 backlog=2048 deferred reuseport;
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_timeout 30s;
client_body_timeout 30s;
send_timeout 30s;
keepalive_timeout 65s;
keepalive_requests 100;
gzip on;
gzip_min_length 1k;
gzip_comp_level 5;
gzip_types text/plain text/css application/javascript application/json application/xml text/xml;
location / {
proxy_pass http://backend;
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_set_header Connection "";
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 30s;
proxy_buffer_size 16k;
proxy_buffers 4 64k;
proxy_busy_buffers_size 128k;
}
location /health {
access_log off;
return 200 "ok\n";
}
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
}
10.2 HTTPS 反代模板
server {
listen 443 ssl http2;
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 ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 10.0.0.1 10.0.0.2 valid=300s;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://backend;
# ... 同上
}
}
server {
listen 80;
server_name app.example.com;
return 301 https://$server_name$request_uri;
}
10.3 静态资源模板
server {
listen 80;
server_name static.example.com;
root /data/static;
access_log /var/log/nginx/static.access.log main;
error_log /var/log/nginx/static.error.log warn;
location / {
expires 7d;
add_header Cache-Control "public, max-age=604800";
try_files $uri $uri/ =404;
}
location ~* \.(jpg|jpeg|png|gif|webp|avif)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
access_log off;
}
location ~* \.(js|css|woff2?)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
access_log off;
}
}
十一、性能调优
11.1 系统级
# /etc/sysctl.d/99-nginx.conf
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.core.netdev_max_backlog = 300000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.ip_local_port_range = 1024 65535
# 应用
sysctl -p /etc/sysctl.d/99-nginx.conf
11.2 文件描述符
# /etc/security/limits.conf
nginx soft nofile 65535
nginx hard nofile 65535
# systemd 启动的 Nginx
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65535
11.3 CPU 亲和性
worker_cpu_affinity auto;
将 worker 绑定到不同的 CPU 核心,避免相互争抢。
11.4 缓冲区与磁盘
# 临时文件路径
proxy_temp_path /var/cache/nginx/proxy_temp 1 2;
fastcgi_temp_path /var/cache/nginx/fastcgi_temp 1 2;
uwsgi_temp_path /var/cache/nginx/uwsgi_temp 1 2;
scgi_temp_path /var/cache/nginx/scgi_temp 1 2;
# 临时文件最大
proxy_max_temp_file_size 1024m;
将临时文件放在独立的磁盘上,避免与日志、操作系统争抢 IO。
11.5 Gzip 压缩
gzip on;
gzip_min_length 1k;
gzip_comp_level 5;
gzip_types text/plain text/css application/javascript application/json application/xml text/xml image/svg+xml;
gzip_vary on;
gzip_proxied any;
gzip_disable "msie6";
风险提示:Gzip 对 CPU 有压力,CPU 敏感型业务要谨慎配置。若发现 CPU 负载偏高,可以先把 gzip_comp_level 降到 3~4。
11.6 OpenFileCache
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
缓存文件描述符,减少 open() 系统调用的开销。
十二、常见误区
12.1 “调大 worker_connections 就能解决性能问题”
不完全对。worker_connections 只是 Nginx 自身接收连接的能力,整体性能还受文件描述符上限、CPU、磁盘和内存等多方面影响。
12.2 “keepalive 64 越大越好”
并不是。keepalive 连接本身也占内存,且上游对 keepalive 同样存在连接数限制。需根据上游连接池的实际容量来调整。
12.3 “proxy_read_timeout 越大越安全”
也不是。读超时只是“多久没数据就断开”。如果上游虽然很慢,但一直在“一点点”吐数据,Nginx 就会一直挂着,反而拖累上游的整体连接数。
不是。日志字段越多,磁盘 IO 越大,处理越慢。务必在日志详尽度与调试需求之间找到平衡点。
12.5 “压缩总能减小带宽”
对文本资源(HTML、CSS、JS)确实有效;对图片、视频、二进制流则几乎没用,只会白白增加 CPU 负担。
12.6 “503 一定是上游挂了”
错。Nginx 自身返回 503,也可能是因为触发了 limit_req、limit_conn 或是 max_conns 等限制策略。
十三、回滚与变更
13.1 改配置前的检查
# 备份
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%F-%H%M%S)
# 语法检查
nginx -t
# 测试配置生效
nginx -T 2>&1 | less
13.2 平滑 reload
nginx -s reload
reload 不会断开现有连接,仅对新连接生效。
13.3 配置错误导致 reload 失败
# 看 error_log
tail -f /var/log/nginx/error.log
# 看到 "invalid number of arguments" 之类
# 改回正确配置
cp /etc/nginx/nginx.conf.bak.2026-06-12-1000 /etc/nginx/nginx.conf
nginx -s reload
13.4 灰度发布
可以利用 Lua + Redis 实现按比例切流,也可以借助相关模块:
upstream backend_v1 {
server 10.0.2.10:8080;
server 10.0.2.11:8080;
}
upstream backend_v2 {
server 10.0.2.20:8080;
server 10.0.2.21:8080;
}
split_clients "$remote_addr" $upstream {
95% backend_v1;
* backend_v2;
}
server {
location / {
proxy_pass http://$upstream;
}
}
split_clients 属于 Tengine 的指令,标准 Nginx 本身并不包含。但你可以用 nginx-module-vts 或 lua-resty 来实现类似功能。
十四、附录:常用诊断命令
# 实时查看日志
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
# 按状态码筛选
grep ' 5[0-9][0-9] ' /var/log/nginx/access.log | head
# Top URL
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# Top IP
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 慢接口
awk '{print $14, $7}' /var/log/nginx/access.log | sort -rn | head -20
# 看 Nginx 状态
curl -s http://127.0.0.1/nginx_status
# 看连接数
ss -ant | grep ':80' | wc -l
ss -s
# 看 worker 进程
ps aux | grep nginx
# 看配置生效
nginx -T 2>&1 | head
# 看 upstream 状态(商业版/打 patch)
curl -s http://127.0.0.1/upstream_check
# 看缓存
head -c 1024 /var/cache/nginx/proxy_temp/0/00/0000000000
十五、总结
排查 Nginx 日志和 4xx/5xx 故障的关键,在于把以下几件事练成肌肉记忆:
- 日志字段:熟悉每一列的含义,能随手写
awk、不用现查文档。
- 状态码语义:4xx 通常是客户端问题,5xx 是服务端问题,499 则是客户端主动断开。
- 超时层级:分布在客户端、Nginx 自身、上游及 FastCGI 四层,timeout 配置也相应散落在不同区块。
- 错误日志关键字:看到
connect() failed、upstream timed out、no live upstreams、worker connections、accept() failed 就能快速定位方向。
- 命令组合:
awk + sort + uniq + head 是日常武器,goaccess 是终端神器,而 ELK 或 PLG 则属于企业级方案。
- 变更前的三位一体:备份 +
nginx -t + reload,这条准则永远不变。
- 面对任何超时问题,先关注
$request_time、$upstream_connect_time 和 $upstream_response_time 这三个字段,能迅速判断出是建立连接慢,还是实际响应慢。
把上面这些命令、配置和案例都过上一遍,以后再面对 Nginx 入口层的故障,基本就不会两眼一抹黑了。
俗话说“工欲善其事,必先利其器”,日常运维中除了日志分析本身,配套的监控和排障工具也是稳定性的基石。关于更多运维与排障方面的实践,也可以到 云栈社区 的相关板块逛逛,偶尔翻一翻别人的踩坑记录,说不定下一次就能少走许多弯路。
十六、日志切割的进阶玩法
16.1 按虚拟主机切分
http {
log_format vhost '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time uct="$upstream_connect_time" '
'urt="$upstream_response_time"';
server {
listen 80;
server_name a.example.com;
access_log /var/log/nginx/a.access.log vhost;
location / {
proxy_pass http://backend_a;
}
}
server {
listen 80;
server_name b.example.com;
access_log /var/log/nginx/b.access.log vhost;
location / {
proxy_pass http://backend_b;
}
}
}
各个虚拟主机使用独立的日志文件,排查问题时会更聚焦。
16.2 条件日志
# 排除健康检查
map $request_uri $loggable {
default 1;
/health 0;
/favicon.ico 0;
}
access_log /var/log/nginx/access.log main if=$loggable;
if= 是条件日志,只有满足条件时才会记录。可以把健康检查、静态资源请求、爬虫高频 URL 都过滤掉。
16.3 日志缓冲
# 64KB 缓冲,1s 刷盘
access_log /var/log/nginx/access.log main buffer=64k flush=1s;
缓冲可以降低磁盘 IO 压力,但风险在于异常宕机时可能会丢失一部分日志。生产上如果对稳定性要求高于一切,更建议直接写盘。
16.4 异步日志(NGINX Plus / Tengine)
NGINX Plus 和 Tengine 支持将日志异步写到 syslog,避免主进程阻塞:
error_log syslog:server=10.0.5.10:514,facility=local7,tag=nginx;
access_log syslog:server=10.0.5.10:514,facility=local7,tag=nginx main;
风险提示:异步日志存在丢失风险,syslog 网络发生抖动时可能丢消息。
十七、与 OpenResty 集成做高级日志分析
OpenResty = Nginx + LuaJIT,可以用 Lua 在请求级别做更复杂的日志处理。
17.1 安装 OpenResty
yum install -y openresty
17.2 自定义日志格式
/etc/openresty/conf.d/log.lua:
local log = require "ngx.log"
local cjson = require "cjson.safe"
local function get_business_log()
local body = ngx.var.request_body
-- 假设业务参数都在 query 里
local args = ngx.req.get_uri_args()
return {
time = ngx.var.time_local,
ip = ngx.var.remote_addr,
uri = ngx.var.request_uri,
user_id = args.user_id or "-",
trace_id = ngx.var.request_id,
}
end
ngx.log(ngx.INFO, cjson.encode(get_business_log()))
17.3 落盘到独立业务日志
log_format business '$time_iso8601|$remote_addr|"$request"|user_id=$arg_user_id';
access_log /var/log/nginx/business.log business;
将业务日志独立于 access_log,方便数据团队做用户行为分析。
十八、Nginx 自身的故障自愈
18.1 systemd 守护
/etc/systemd/system/nginx.service:
[Unit]
Description=The nginx HTTP and reverse proxy server
After=network.target remote-fs.target nss-lookup.target
[Service]
Type=forking
PIDFile=/var/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
Restart=on-failure
RestartSec=5s
LimitNOFILE=65535
18.2 OOM 后的恢复
Nginx 进程被 OOM Killer 杀掉的典型特征:
/var/log/messages 中出现 Out of memory: Killed process。
nginx.pid 文件还在,但对应的进程却没了。
- 相关端口无人在监听。
排查:
dmesg | grep -i 'oom'
journalctl -k | grep -i 'oom'
cat /var/log/nginx/error.log | tail
修复方向:
- 调大系统
vm.overcommit_memory 参数。
- 调大 Nginx worker 的内存上限。
- 检查并调整
worker_rlimit_nofile 与 worker_connections 的匹配情况。
18.3 Nginx 僵死
Nginx 进程还在,但不再响应任何请求:
ps 查看进程状态,CPU 占用为 0。
ss -ltn 查看端口仍处于监听状态。
- 内部可能发生了死锁或卡在某个系统调用中。
最直接的办法是用 kill -9 强杀,随后 systemd 会自动将其拉起。
十九、Nginx 与 CDN、边缘计算
19.1 CDN 回源
server {
listen 80;
server_name cdn-origin.example.com;
# CDN 回源白名单
set $allow 0;
if ($http_x_forwarded_for ~* "10\.0\.[0-9]+\.[0-9]+") { set $allow 1; }
if ($allow = 0) { return 403; }
location / {
proxy_pass http://origin;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
CDN 回源场景一般还需要加上 X-Forwarded-For 校验、缓存控制以及回源限流。
19.2 边缘 WAF
Nginx 结合 ModSecurity 就能在边缘实现 WAF:
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity/main.conf;
ModSecurity 内置大量规则集,可有效防御 SQL 注入、XSS、命令注入等常见攻击。
19.3 Nginx + GeoIP
geoip_country /usr/share/GeoIP/GeoIP.dat;
map $geoip_country_code $allowed_country {
CN 1;
US 1;
default 0;
}
server {
if ($allowed_country = 0) { return 403; }
}
按国家限制访问时,记得定期更新 GeoIP 数据库。
二十、Nginx 的高级路由
20.1 按 Host 路由
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://api_backend;
}
}
server {
listen 80;
server_name admin.example.com;
location / {
proxy_pass http://admin_backend;
}
}
20.2 按 URL 路由
server {
listen 80;
server_name app.example.com;
location /api/v1/ {
proxy_pass http://api_v1;
}
location /api/v2/ {
proxy_pass http://api_v2;
}
location /static/ {
root /data/static;
}
location / {
return 404;
}
}
map $http_x_app_version $backend_pool {
"1.0" backend_v1;
"2.0" backend_v2;
default backend_v1;
}
server {
location / {
proxy_pass http://$backend_pool;
}
}
20.4 按 Cookie 路由
map $cookie_session $backend_pool {
"~^s1_" backend_v1;
"~^s2_" backend_v2;
default backend_v1;
}
灰度发布时通过 Cookie 来区分用户很方便。
20.5 按 IP 段路由
geo $is_internal {
default 0;
10.0.0.0/8 1;
172.16.0.0/12 1;
192.168.0.0/16 1;
}
server {
if ($is_internal) {
# 内网走 test
}
}
二十一、Nginx 与缓存
21.1 代理缓存
proxy_cache_path /var/cache/nginx/proxy_cache
levels=1:2
keys_zone=my_cache:10m
inactive=60m
max_size=10g
use_temp_path=off;
server {
location / {
proxy_cache my_cache;
proxy_cache_valid 200 5m;
proxy_cache_valid 404 1m;
proxy_cache_key "$scheme$proxy_host$request_uri";
proxy_pass http://backend;
}
}
21.2 缓存控制
# 不缓存
proxy_cache_bypass $http_pragma $http_authorization;
proxy_no_cache $http_pragma $http_authorization;
# 强制刷新
if ($arg_force_refresh = 1) { proxy_cache_bypass 1; }
21.3 缓存命中率监控
add_header X-Cache-Status $upstream_cache_status;
$upstream_cache_status 的取值包括:HIT、MISS、EXPIRED、STALE、BYPASS、UPDATING 和 REVALIDATED。在 Grafana 上可以用 hit/(hit+miss) 来计算命中率。
二十二、Nginx 与 WebSocket
upstream websocket {
server 10.0.2.10:8080;
server 10.0.2.11:8080;
keepalive 64;
}
server {
location /ws/ {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 长连接需要调大超时
proxy_connect_timeout 7s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
}
}
关键点:
Upgrade 和 Connection 头必须原样传递。
proxy_read_timeout 与 proxy_send_timeout 要调到小时级别。
二十三、Nginx 与 gRPC
upstream grpc_backend {
server 10.0.2.10:50051;
server 10.0.2.11:50051;
keepalive 64;
}
server {
listen 80 http2;
location / {
grpc_pass grpc://grpc_backend;
grpc_set_header X-Request-Id $request_id;
}
}
Nginx 1.13.10 以上版本原生支持 gRPC,但务必确认 HTTP/2 已开启。
二十四、Nginx 安全配置详解
24.1 限制方法
location / {
if ($request_method !~ ^(GET|POST|HEAD|OPTIONS)$) {
return 405;
}
}
24.2 限制 User-Agent
map $http_user_agent $bad_ua {
default 0;
~*sqlmap 1;
~*nmap 1;
~*masscan 1;
}
if ($bad_ua) { return 403; }
24.3 限制 Referer
valid_referers none blocked server_names
*.example.com example.com;
if ($invalid_referer) { return 403; }
24.4 CC 攻击防护
limit_req_zone $binary_remote_addr zone=cc:10m rate=10r/s;
server {
location / {
limit_req zone=cc burst=20 nodelay;
proxy_pass http://backend;
}
}
还可以配合 fail2ban 抓取日志来封禁异常 IP。
24.5 Slowloris 防护
client_body_timeout 10s;
client_header_timeout 10s;
send_timeout 10s;
keepalive_timeout 5s;
将各类超时调小,使 Slowloris 这类慢连接攻击难以长时间维持。
24.6 隐藏版本
server_tokens off;
这样返回头中的 Server: nginx 就不带版本号了。
二十五、Nginx 性能压测
25.1 ab 压测
# 1 万次请求,并发 100
ab -n 10000 -c 100 http://10.0.1.10/
# 启用 keepalive
ab -n 10000 -c 100 -k http://10.0.1.10/
# POST 请求
ab -n 1000 -c 10 -p data.json -T application/json http://10.0.1.10/api
25.2 wrk 压测
# 4 线程、200 并发、压 30 秒
wrk -t4 -c200 -d30s http://10.0.1.10/
# 自定义脚本
wrk -t4 -c200 -d30s -s post.lua http://10.0.1.10/api
post.lua:
wrk.method = "POST"
wrk.body = '{"key":"value"}'
wrk.headers["Content-Type"] = "application/json"
25.3 压测观察指标
wrk 输出的 Latency Distribution(P50/P99/P99.9)。
top 中的 CPU 中断占比。
ss -s 中的连接数。
iostat -x 1 中的磁盘 IO。
vmstat 1 中的上下文切换。
25.4 压测时的常见瓶颈
- Nginx worker 满:调大
worker_processes。
- 上游连接打满:增加 Tomcat 节点或调大 keepalive。
- 磁盘 IO 高:将日志切换到独立磁盘。
- CPU 中断占比高:开启网卡多队列并配置 CPU 亲和性。
二十六、Nginx 配置测试与 CI/CD
26.1 单元测试工具
nginx-config-formatter、gixy、nginxbeautifier 都能进行语法和最佳实践检查。
pip install gixy
gixy /etc/nginx/nginx.conf
gixy 能发现:
server_tokens 没有关闭。
if 指令被滥用。
add_header 可能失效。
proxy_pass 末尾缺少斜杠等问题。
26.2 在 CI 中验证
stages:
- test
nginx_test:
stage: test
image: alpine
script:
- apk add nginx gixy
- nginx -t -c $CONFIG_PATH
- gixy $CONFIG_PATH
26.3 灰度发布平台
借助 Lua + Redis 可以实现动态 upstream 列表,也可以使用更稳定的方案,比如 nginx-upsync-module,从 Consul 或 etcd 拉取 upstream,实现配置的动态更新。
二十七、Nginx 在云原生场景
27.1 Nginx Ingress Controller
在 K8s 集群里用 Nginx Ingress Controller 作为七层入口:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80
27.2 流量切分
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-canary
annotations:
nginx.ingress.kubernetes.io/canary-weight: "20"
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-v2
port:
number: 80
27.3 Sidecar Nginx
在一些 Service Mesh 方案中,Nginx 会以 sidecar 的形式部署到每个 Pod。它的优势是能直接处理 Pod 内的 L7 路由,劣势是每多一个 Pod 就多一份资源消耗。
二十八、附录:日志常见字段名(与 ELK / Loki 兼容)
ELK 或 Loki 在收集 Nginx 日志时,常见的字段映射如下:
| Nginx 变量 |
标准字段名 |
类型 |
$remote_addr |
client_ip |
string |
$time_local |
@timestamp |
date |
$request_method |
method |
string |
$request_uri |
url |
string |
$server_protocol |
protocol |
string |
$status |
status |
int |
$body_bytes_sent |
bytes |
int |
$http_referer |
referer |
string |
$http_user_agent |
user_agent |
string |
$request_time |
request_time |
float |
$upstream_connect_time |
upstream_connect_time |
float |
$upstream_response_time |
upstream_response_time |
float |
$remote_user |
user |
string |
$http_x_forwarded_for |
xff |
string |
Grok pattern 示例:
%{IPORHOST:client_ip} - %{USER:remote_user} \[%{HTTPDATE:timestamp}\] "%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:httpversion}" %{INT:status} %{INT:bytes} "%{DATA:referer}" "%{DATA:user_agent}"
二十九、完整 Nginx 排障清单
按照下面的清单逐项检查,95% 的 Nginx 故障都可以快速定位:
- 日志在哪:
/var/log/nginx/access.log、/var/log/nginx/error.log。
- Nginx 是否在运行:
systemctl status nginx。
- 配置文件在哪:
/etc/nginx/nginx.conf。
- 配置是否生效:
nginx -T 会打印完整的当前生效配置。
- 配置是否有错:
nginx -t。
- 端口是否在监听:
ss -ltn | grep :80。
- 连接数多少:
ss -s、curl localhost/nginx_status。
- 最近的 5xx 是什么:
awk '$9 ~ /^5/ {print $9, $7, $NF}' access.log | tail。
- 最近的 504 慢在哪:
awk '$9 == 504 {print $14, $18}' access.log | tail。
- 上游进程是否在:
curl http://10.0.2.10:8080/health。
- 上游监听在哪:
ss -ltn | grep 8080。
- 磁盘是否满:
df -h。
- 文件描述符:
lsof -p $(pgrep -f nginx) | wc -l。
- CPU 中断:
top 中看到 si/hi 偏高。
- 网络:
sar -n DEV 1、iftop。
- 反向抓包:
tcpdump -i eth0 -nn。
- 错误日志关键字:搜
connect() failed、upstream timed out、no live upstreams。
- reload 历史:
systemctl status nginx 查看 Started/Reloaded 的时间点。
- 最近 5 分钟 QPS:
wc -l access.log。
- 最近 5 分钟错误率:
grep '5[0-9][0-9]' access.log | wc -l 除以 wc -l access.log。
把这份清单打印出来贴在工位上,以后每次碰到 Nginx 故障,按顺序排查一遍就行。
基础架构的稳定性,往往就是从这些看起来琐碎的日志和配置细节里“堆”出来的。希望这篇梳理能帮你少踩一些坑。