找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5458

积分

0

好友

705

主题
发表于 6 小时前 | 查看: 5| 回复: 0

Nginx 几乎是所有 Web 业务的入口:从 CDN 边缘到四层 LB,再到七层反代和静态资源服务器。一旦它自己出问题,影响面会非常大。但相对而言,Nginx 的日志字段非常规范,排障时完全可以“按图索骥”——只要把日志格式、字段含义、4xx/5xx 状态码以及各类超时参数搞清楚,80% 的故障都能直接在日志里看出来。

这篇文章分四块来讲:

  1. Nginx 日志字段详解(access_log、error_log)。
  2. 各类超时(连接、发送、读取、上游)的真实根因。
  3. 4xx / 5xx 状态码的排查路径。
  4. 日志分析的常用命令、脚本与工具链。

不写花里胡哨的,每一条都对应生产上能直接用的命令和配置。在深入细节前,不妨先想想日常维护中是否也碰到过类似的日志排查痛点?带着具体场景往下看,效果会更好。

一、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,调试时再调到 infodebug。需要注意,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_timeoutclient_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_sizelarge_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_reqlimit_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

其中 $14request_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 的 somaxconntcp_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_sizeproxy_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_timecombined 格式里不一定在第 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 就会一直挂着,反而拖累上游的整体连接数。

12.4 “log_format 字段越多越好”

不是。日志字段越多,磁盘 IO 越大,处理越慢。务必在日志详尽度与调试需求之间找到平衡点。

12.5 “压缩总能减小带宽”

对文本资源(HTML、CSS、JS)确实有效;对图片、视频、二进制流则几乎没用,只会白白增加 CPU 负担。

12.6 “503 一定是上游挂了”

错。Nginx 自身返回 503,也可能是因为触发了 limit_reqlimit_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-vtslua-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() failedupstream timed outno live upstreamsworker connectionsaccept() 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_nofileworker_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;
    }
}

20.3 按 Header 路由

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;
    }
}
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 的取值包括:HITMISSEXPIREDSTALEBYPASSUPDATINGREVALIDATED。在 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;
    }
}

关键点:

  • UpgradeConnection 头必须原样传递。
  • proxy_read_timeoutproxy_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-formattergixynginxbeautifier 都能进行语法和最佳实践检查。

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 故障都可以快速定位:

  1. 日志在哪/var/log/nginx/access.log/var/log/nginx/error.log
  2. Nginx 是否在运行systemctl status nginx
  3. 配置文件在哪/etc/nginx/nginx.conf
  4. 配置是否生效nginx -T 会打印完整的当前生效配置。
  5. 配置是否有错nginx -t
  6. 端口是否在监听ss -ltn | grep :80
  7. 连接数多少ss -scurl localhost/nginx_status
  8. 最近的 5xx 是什么awk '$9 ~ /^5/ {print $9, $7, $NF}' access.log | tail
  9. 最近的 504 慢在哪awk '$9 == 504 {print $14, $18}' access.log | tail
  10. 上游进程是否在curl http://10.0.2.10:8080/health
  11. 上游监听在哪ss -ltn | grep 8080
  12. 磁盘是否满df -h
  13. 文件描述符lsof -p $(pgrep -f nginx) | wc -l
  14. CPU 中断top 中看到 si/hi 偏高。
  15. 网络sar -n DEV 1iftop
  16. 反向抓包tcpdump -i eth0 -nn
  17. 错误日志关键字:搜 connect() failedupstream timed outno live upstreams
  18. reload 历史systemctl status nginx 查看 Started/Reloaded 的时间点。
  19. 最近 5 分钟 QPSwc -l access.log
  20. 最近 5 分钟错误率grep '5[0-9][0-9]' access.log | wc -l 除以 wc -l access.log

把这份清单打印出来贴在工位上,以后每次碰到 Nginx 故障,按顺序排查一遍就行。

基础架构的稳定性,往往就是从这些看起来琐碎的日志和配置细节里“堆”出来的。希望这篇梳理能帮你少踩一些坑。




上一篇:Windows 11六月更新KB5094126:详解低延迟CPU调度等五大功能及安全变更
下一篇:那颗“靠漏电运行”的ARM处理器:一个差点被丢弃的设计,如何装进350亿颗芯片
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-6-14 08:01 , Processed in 0.607879 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表