Nginx日志分析技巧:快速找出异常请求和攻击源

admin 2026-07-24 04:24:22 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文系统介绍了Nginx日志分析的核心方法,涵盖自定义logformat、使用awk/grep/sort/uniq等命令行工具高效定位异常请求与攻击源。文章强调通过request_time与upstreamresponsetime对比区分慢请求根因,并给出CC攻击、扫描器、SQL注入的日志特征及基于limitreq/geo/map的封禁思路。内容基于一线实战经验,提供了从日志解读到攻击阻断的完整排查路径,可操作性强。 综合评分: 85 文章分类: 安全运营,WEB安全,应急响应,实战经验,安全工具


步骤 5:按时段看 5xx 突增位置

目的:找到 5xx 集中爆发的时间窗。

命令(按分钟聚合 5xx):

awk 'BEGIN{IGNORECASE=1} /"status":[5-9][0-9][0-9]| [5-9][0-9][0-9] / {
    match($0, /\[([0-9]{2}\/[A-Za-z]+\/[0-9]{4}):([0-9]{2}):([0-9]{2}):/, m)
    minute = m[1]":"m[2]
    count[minute]++
}
END {for (k in count) print count[k], k}' /var/log/nginx/access.log \
  | sort -nr | head -n 20

这一段思路给读者,节奏跟随”分钟粒度”即可,复杂脚本要严格走审校。

预期输出:

2302 08/Jul/2026:14:21
1810 08/Jul/2026:14:20
...

异常表现:

  • 集中在某个时间点 → 对应业务操作、上线发布、定时任务。
  • 缓慢上升 → 后端泄漏或资源耗尽。

下一步动作:进入步骤 6 看慢请求。

步骤 6:找慢请求

目的:把慢请求(一般是 request_time > 1s)单独拎出来分析。

命令(提取慢请求的 IP、URL、耗时):

awk '{
    match($0, /([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/, ip)
    match($0, /\"([A-Z]+) ([^ ]+) HTTP/, req)
    match($0, / request_time\":([0-9.]+)| [0-9.]+ /, rt)
    if (rt[1] + 0 >= 1) {
        print rt[1], ip[1], req[2]
    }
}' /var/log/nginx/access.log | sort -nr | head -n 50

这段 gawk 形式因版本差异较大,标准 awk 里 match() 第三参数是 GNU awk 扩展。如果不想依赖 gawk,更稳的做法是先用 grep 抽行,再用 awk 切列:

grep -E 'request_time":(1[0-9]|[2-9][0-9]|[0-9]{3,})' /var/log/nginx/access.log \
&nbsp; | awk -F'"' '{for(i=1;i<=NF;i++){if($i~/request_time/){rt=$(i+2)}; if($i~/remote_addr/){ip=$(i+2)}; if($i~/request_uri/){uri=$(i+2)}}} rt+0>=1{print rt, ip, uri}' \
&nbsp; | sort -nr | head -n 50

预期输出:类似

3.421 198.51.100.10 /api/order/detail
2.987 198.51.100.10 /api/cart
...

异常表现:

  • 同一个 IP 全部慢 → 客户端网络问题(运营商/区域)。
  • 同一个 URL 全部慢 → 后端或数据库问题。
  • 同一个 $upstream_addr 全部慢 → 单台后端故障。

下一步动作:进入步骤 7 看上游状态。

步骤 7:上游 vs Nginx 谁慢

目的:判断瓶颈在后端还是 Nginx 自身。

命令:

awk -F'"' '{for(i=1;i<=NF;i++){if($i~/upstream_response_time/){urt=$(i+2)}; if($i~/request_time/){rt=$(i+2)}; if($i~/status/){st=$(i+2)}; if($i~/upstream_addr/){ua=$(i+2)}} if(urt!="-" && rt+0>=1) print rt, urt, ua, st}' \
&nbsp; /var/log/nginx/access.log | head -n 50

预期输出:类似

3.421 3.300 10.0.0.7:80 200
2.987 2.900 10.0.0.7:80 200

异常表现:

  • rt ≈ urt

    且均大 → 后端慢。

  • rt

    远大于 urt → 写日志/缓冲/压缩/慢客户端,慢在 Nginx 自身。

  • urt=-

    且 st=502 → Nginx 没连上上游。

下一步动作:进入步骤 8 看错误详情。

步骤 8:error_log 错误码分类

目的:把 error_log 按”原因”聚拢。

命令:

grep -oE '\[(error|warn|crit)\] [0-9+#, ]+\*[0-9]+: .*' /var/log/nginx/error.log \
&nbsp; | grep -oE '(no live upstreams|connect\(\) failed|upstream timed out|SSL_.*|client sent invalid|limiting requests|.*directory.*access denied)' \
&nbsp; | sort | uniq -c | sort -nr | head -n 20

预期输出:类似

182 upstream timed out
41 no live upstreams
12 SSL_do_handshake() failed
7 limiting requests

异常表现:

  • upstream timed out

    集中 → 调 proxy_*_timeout / 检查后端。

  • no live upstreams

    → 上游都死了,健康检查可能没配或失败。

  • limiting requests

    → 触发了限速,确认是否正常业务或被打。

下一步动作:进入步骤 9。

步骤 9:识别攻击类型

目的:识别 CC、扫描器、SQL 注入几类攻击。

命令(CC):

awk '{c[$1]++} END {for (k in c) if (c[k] >= 1000) print c[k], k}' /var/log/nginx/access.log \
&nbsp; | sort -nr | head

上面统计的是按行首字段计数。如果是 JSON:

cat access.log | jq -r '.remote_addr' | sort | uniq -c | sort -nr | awk '$1 >= 1000' | head

命令(扫描器):

grep -E '/(admin|wp-login|phpmyadmin|\.env|\.git|backup|\.sql|\.bak)/?(\$|\?|#|)' /var/log/nginx/access.log \
&nbsp; | awk -F'"' '{print $6}' | sort | uniq -c | sort -nr | head

$6 在 combined 格式里通常是 $http_user_agent

命令(SQL 注入特征):

grep -Ei 'union(\s|%20)+select|select(\s|%20)+\*|or(\s|%20)+1(=|%3d)1|benchmark\(|sleep\(|information_schema' /var/log/nginx/access.log \
&nbsp; | awk -F'"' '{print $1}' | sort | uniq -c | sort -nr | head

预期输出:CC 上千、扫描器多条命中特定 UA、SQL 注入几条但可能 IP 集中。

下一步动作:进入步骤 10 处置。

步骤 10:用 limit_req / map / geo 处置

目的:把高频攻击源阻断。

配置(/etc/nginx/conf.d/security.conf):

# 限速:每秒 30 个请求,burst 60,限后队列延迟处理
limit_req_zone $binary_remote_addr zone=req_zone:10m rate=30r/s;

# UA 黑名单
map $http_user_agent $bad_ua {
&nbsp; &nbsp; default 0;
&nbsp; &nbsp; "~*sqlmap" &nbsp; &nbsp;1;
&nbsp; &nbsp; "~*nmap" &nbsp; &nbsp; &nbsp;1;
&nbsp; &nbsp; "~*masscan" &nbsp; 1;
&nbsp; &nbsp; "~*(curl|wget|python-requests)" 1;
&nbsp; &nbsp; "~^$" &nbsp; &nbsp; &nbsp; &nbsp; 1; &nbsp;# 空 UA
}

# 按国家封禁(举例,需合规评估)
# geoip2 模块需要额外编译安装,这里以传统 geo 给出思路
geo $geo_country_code $is_high_risk {
&nbsp; &nbsp; default 0;
&nbsp; &nbsp; XX 1; &nbsp;# 按需
}

server {
&nbsp; &nbsp; listen 80;
&nbsp; &nbsp; server_name _;

&nbsp; &nbsp; # 全局限制
&nbsp; &nbsp; limit_req zone=req_zone burst=60 nodelay;

&nbsp; &nbsp; # UA 黑名单直接 403
&nbsp; &nbsp; if ($bad_ua) {
&nbsp; &nbsp; &nbsp; &nbsp; return 403;
&nbsp; &nbsp; }

&nbsp; &nbsp; # 高风险国家直接 403(仅举例)
&nbsp; &nbsp; if ($is_high_risk) {
&nbsp; &nbsp; &nbsp; &nbsp; return 403;
&nbsp; &nbsp; }

&nbsp; &nbsp; location / {
&nbsp; &nbsp; &nbsp; &nbsp; proxy_pass http://backend;
&nbsp; &nbsp; &nbsp; &nbsp; include /etc/nginx/proxy_params;
&nbsp; &nbsp; }
}

/etc/nginx/proxy_params

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;

生效与验证:

nginx -t
nginx -s reload
tail -f /var/log/nginx/access.log | grep -E ' 403 '

异常表现:

  • 误杀:业务反馈正常用户拿 403;先去掉 map 一项或增加例外。
  • 限速失败:可能是 limit_req_zone 的 key 不对,反代场景务必用 $binary_remote_addr 并配合 real_ip

下一步动作:进入步骤 11 验证闭环。

步骤 11:验证闭环

目的:确认处置后攻击流量被挡、误伤为 0。

命令:

# 攻击 IP 后续状态码应为 403
grep '203.0.113.45' /var/log/nginx/access.log | tail -n 50 | awk '{print $9}' | sort | uniq -c

预期:

50 403

业务验证:

  • 拿正常用户 UA 不应被误杀。
  • 拿 sqlmap UA 试探应 403。

下一步动作:进入步骤 12 做回滚预案与清理。

步骤 12:日志轮转、磁盘占用、清理

目的:日志切走、磁盘不爆、旧日志可归档。

/etc/logrotate.d/nginx

/var/log/nginx/*.log {
&nbsp; &nbsp; daily
&nbsp; &nbsp; missingok
&nbsp; &nbsp; rotate 14
&nbsp; &nbsp; compress
&nbsp; &nbsp; delaycompress
&nbsp; &nbsp; notifempty
&nbsp; &nbsp; create 0640 nginx adm
&nbsp; &nbsp; sharedscripts
&nbsp; &nbsp; postrotate
&nbsp; &nbsp; &nbsp; &nbsp; [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
&nbsp; &nbsp; endscript
}

命令:

# 模拟 logrotate
logrotate -d /etc/logrotate.d/nginx
# 真正切一次
logrotate -f /etc/logrotate.d/nginx

# 磁盘占用
df -h /var/log

# 删除旧日志(务必确认)
find /var/log/nginx -name '*.gz' -mtime +30 -ls
find /var/log/nginx -name '*.gz' -mtime +30 -delete

风险提醒:

  • find -delete

    不进回收站,先 find ... -ls 看一遍。

  • 不要直接 rm /var/log/nginx/access.log 然后 nginx -s reload:当前 fd 还在写旧 inode,磁盘不会释放。先 > access.log truncate,再让 logrotate 接管。

  • 不要用 echo "" > access.log 在生产强压时段,可能打散日志。

  • 日志清理要分批:find -mtime +30 -delete 比 find -delete 全清要安全。

下一步动作:进入第十三节(生产环境注意事项)做兜底确认。

六、常用命令

把上面用到的命令整合成速查表。每条都标注目的与判断逻辑。

| 命令 | 目的 | 关键判断 | | — | — | — | | nginx -V | 看编译参数(--with-debug、模块) | 检查模块是否齐 | | nginx -t | 测试配置语法 | 改完必跑 | | nginx -T | 输出完整配置 | 排查默认值差异 | | nginx -s reload | 平滑重载配置 | 不要 nginx -s restart | | tail -F /var/log/nginx/access.log | 实时看日志 | ^F 比 -f 更可靠 | | awk '{print $7}' access.log | sort | uniq -c | sort -nr | head | TOP N URL | 字段位置以 combined 格式为准 | | grep -E ... | wc -l | 统计某类请求总数 | 配合 cut 做时间窗聚合 | | grep 'IP' access.log | awk '{print $9}' | sort | uniq -c | 单 IP 状态码分布 | 攻击 vs 正常 vs 限速 | | cat access.log | python3 -c '...json...' | 处理 JSON 日志 | 不依赖 jq 也能用 | | jq -r '.remote_addr' access.log | JSON 字段抽取 | 装 jq 即可 | | grep -E '| 5[0-9][0-9] |' 或对应 JSON 字段 | 5xx 过滤 | 注意要双空格包裹 | | iptables -L -n -v | grep DROP | 防火墙封禁情况 | 二次确认封禁来源 | | wc -l access.log | 行数 | 文件大小粗判断 |

每个字段的取值含义,反复出现在第七节配置示例里会再展开。

七、配置示例

下面给一套 Nginx + logrotate + iptables 的完整可落地配置清单。路径默认以 CentOS/RHEL 为准,Debian/Ubuntu 类似。

1. Nginx http 块(含 log_format)

/etc/nginx/nginx.conf

user &nbsp;nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid &nbsp; &nbsp; &nbsp; &nbsp;/var/run/nginx.pid;

events {
&nbsp; &nbsp; worker_connections 4096;
&nbsp; &nbsp; multi_accept on;
&nbsp; &nbsp; use epoll;
}

http {
&nbsp; &nbsp; include &nbsp; &nbsp; &nbsp; /etc/nginx/mime.types;
&nbsp; &nbsp; default_type &nbsp;application/octet-stream;
&nbsp; &nbsp; sendfile &nbsp; &nbsp; &nbsp; &nbsp;on;
&nbsp; &nbsp; tcp_nopush &nbsp; &nbsp; &nbsp;on;
&nbsp; &nbsp; tcp_nodelay &nbsp; &nbsp; on;
&nbsp; &nbsp; keepalive_timeout &nbsp;65;
&nbsp; &nbsp; keepalive_requests 1000;

&nbsp; &nbsp; # gzip...
&nbsp; &nbsp; include /etc/nginx/conf.d/gzip.conf;

&nbsp; &nbsp; # 自定义日志
&nbsp; &nbsp; log_format main escape=json
&nbsp; &nbsp; &nbsp; &nbsp; '{'
&nbsp; &nbsp; &nbsp; &nbsp; '"time":"$time_iso8601",'
&nbsp; &nbsp; &nbsp; &nbsp; '"remote_addr":"$remote_addr",'
&nbsp; &nbsp; &nbsp; &nbsp; '"x_forwarded_for":"$http_x_forwarded_for",'
&nbsp; &nbsp; &nbsp; &nbsp; '"request_method":"$request_method",'
&nbsp; &nbsp; &nbsp; &nbsp; '"request_uri":"$request_uri",'
&nbsp; &nbsp; &nbsp; &nbsp; '"server_name":"$server_name",'
&nbsp; &nbsp; &nbsp; &nbsp; '"request_time":"$request_time",'
&nbsp; &nbsp; &nbsp; &nbsp; '"upstream_addr":"$upstream_addr",'
&nbsp; &nbsp; &nbsp; &nbsp; '"upstream_response_time":"$upstream_response_time",'
&nbsp; &nbsp; &nbsp; &nbsp; '"upstream_connect_time":"$upstream_connect_time",'
&nbsp; &nbsp; &nbsp; &nbsp; '"upstream_status":"$upstream_status",'
&nbsp; &nbsp; &nbsp; &nbsp; '"status":"$status",'
&nbsp; &nbsp; &nbsp; &nbsp; '"bytes_sent":"$body_bytes_sent",'
&nbsp; &nbsp; &nbsp; &nbsp; '"connection_requests":"$connection_requests",'
&nbsp; &nbsp; &nbsp; &nbsp; '"http_user_agent":"$http_user_agent",'
&nbsp; &nbsp; &nbsp; &nbsp; '"http_referer":"$http_referer"'
&nbsp; &nbsp; &nbsp; &nbsp; '}';

&nbsp; &nbsp; access_log /var/log/nginx/access.log main buffer=32k flush=5s;
&nbsp; &nbsp; error_log &nbsp;/var/log/nginx/error.log warn;

&nbsp; &nbsp; # 安全/限速
&nbsp; &nbsp; limit_req_zone &nbsp; &nbsp; &nbsp;$binary_remote_addr zone=req_zone:10m &nbsp;rate=30r/s;
&nbsp; &nbsp; limit_conn_zone &nbsp; &nbsp; $binary_remote_addr zone=conn_zone:10m;
&nbsp; &nbsp; limit_req_status &nbsp; &nbsp;429;
&nbsp; &nbsp; limit_conn_status &nbsp; 429;

&nbsp; &nbsp; map $http_user_agent $bad_ua {
&nbsp; &nbsp; &nbsp; &nbsp; default &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0;
&nbsp; &nbsp; &nbsp; &nbsp; "~*sqlmap" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;1;
&nbsp; &nbsp; &nbsp; &nbsp; "~*nmap" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;1;
&nbsp; &nbsp; &nbsp; &nbsp; "~*masscan" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 1;
&nbsp; &nbsp; &nbsp; &nbsp; "~*python-requests" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 1;
&nbsp; &nbsp; &nbsp; &nbsp; "~*curl" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;1;
&nbsp; &nbsp; &nbsp; &nbsp; "~*wget" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;1;
&nbsp; &nbsp; &nbsp; &nbsp; "~^$" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 1;
&nbsp; &nbsp; }

&nbsp; &nbsp; include /etc/nginx/conf.d/*.conf;
}

2. server 配置示例(带限速与黑名单)

/etc/nginx/conf.d/site.conf

upstream backend {
&nbsp; &nbsp; server 10.0.0.7:8080 max_fails=3 fail_timeout=10s;
&nbsp; &nbsp; server 10.0.0.8:8080 max_fails=3 fail_timeout=10s;
&nbsp; &nbsp; keepalive 64;
}

server {
&nbsp; &nbsp; listen 80;
&nbsp; &nbsp; server_name example.com;
&nbsp; &nbsp; return 301 https://$server_name$request_uri;
}

server {
&nbsp; &nbsp; listen 443 ssl http2;
&nbsp; &nbsp; server_name example.com;

&nbsp; &nbsp; ssl_certificate &nbsp; &nbsp; /etc/nginx/ssl/example.crt;
&nbsp; &nbsp; ssl_certificate_key /etc/nginx/ssl/example.key;
&nbsp; &nbsp; ssl_protocols &nbsp; &nbsp; &nbsp; TLSv1.2 TLSv1.3;
&nbsp; &nbsp; ssl_ciphers &nbsp; &nbsp; &nbsp; &nbsp; HIGH:!aNULL:!MD5;

&nbsp; &nbsp; # UA 黑名单直接 403
&nbsp; &nbsp; if ($bad_ua) {
&nbsp; &nbsp; &nbsp; &nbsp; return 403;
&nbsp; &nbsp; }

&nbsp; &nbsp; # 全局限速
&nbsp; &nbsp; limit_req &nbsp;zone=req_zone burst=60 nodelay;
&nbsp; &nbsp; limit_conn conn_zone 50;

&nbsp; &nbsp; access_log /var/log/nginx/example.access.log main buffer=32k flush=5s;
&nbsp; &nbsp; error_log &nbsp;/var/log/nginx/example.error.log warn;

&nbsp; &nbsp; location / {
&nbsp; &nbsp; &nbsp; &nbsp; proxy_pass &nbsp; &nbsp; &nbsp; &nbsp; http://backend;
&nbsp; &nbsp; &nbsp; &nbsp; proxy_http_version 1.1;
&nbsp; &nbsp; &nbsp; &nbsp; proxy_set_header &nbsp; Connection "";
&nbsp; &nbsp; &nbsp; &nbsp; proxy_set_header &nbsp; Host $host;
&nbsp; &nbsp; &nbsp; &nbsp; proxy_set_header &nbsp; X-Real-IP $remote_addr;
&nbsp; &nbsp; &nbsp; &nbsp; proxy_set_header &nbsp; X-Forwarded-For $proxy_add_x_forwarded_for;
&nbsp; &nbsp; &nbsp; &nbsp; proxy_set_header &nbsp; X-Forwarded-Proto $scheme;

&nbsp; &nbsp; &nbsp; &nbsp; proxy_connect_timeout 3s;
&nbsp; &nbsp; &nbsp; &nbsp; proxy_send_timeout &nbsp; &nbsp;30s;
&nbsp; &nbsp; &nbsp; &nbsp; proxy_read_timeout &nbsp; &nbsp;30s;

&nbsp; &nbsp; &nbsp; &nbsp; proxy_buffer_size &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;16k;
&nbsp; &nbsp; &nbsp; &nbsp; proxy_buffers &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;8 16k;
&nbsp; &nbsp; &nbsp; &nbsp; proxy_busy_buffers_size &nbsp; &nbsp;32k;
&nbsp; &nbsp; }

&nbsp; &nbsp; location ~* \.(js|css|jpg|jpeg|png|gif|ico|svg|woff2?)$ {
&nbsp; &nbsp; &nbsp; &nbsp; expires 7d;
&nbsp; &nbsp; &nbsp; &nbsp; add_header Cache-Control "public, max-age=604800, immutable";
&nbsp; &nbsp; &nbsp; &nbsp; access_log off;
&nbsp; &nbsp; }
}

3. logrotate

/etc/logrotate.d/nginx

/var/log/nginx/*.log {
&nbsp; &nbsp; daily
&nbsp; &nbsp; missingok
&nbsp; &nbsp; rotate 14
&nbsp; &nbsp; compress
&nbsp; &nbsp; delaycompress
&nbsp; &nbsp; notifempty
&nbsp; &nbsp; sharedscripts
&nbsp; &nbsp; create 0640 nginx adm
&nbsp; &nbsp; postrotate
&nbsp; &nbsp; &nbsp; &nbsp; [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
&nbsp; &nbsp; endscript
}

4. iptables 兜底(极端情况下给到 L4)

iptables -I INPUT -s 203.0.113.45 -j DROP
iptables -L -n -v | grep DROP

注意:如果是反代,前面有 SLB/CDN,要 iptables -I INPUT -s SLB段 -j DROP 会把自己打挂。一般用 Nginx 自己 deny 即可。需要更彻底就在 WAF/CDN 那一侧封。

八、日志或指标观察方法

Nginx 自身的指标观察主要靠三类:

  1. error_log 直接看,每条都有时间戳与原因。

  2. access_log 通过分析得到响应时间、状态码分布、TOP IP/URL。

  3. stub_status

    vts 模块的实时计数器。

1. stub_status 观察并发与连接

/etc/nginx/conf.d/status.conf

server {
&nbsp; &nbsp; listen 127.0.0.1:8080;
&nbsp; &nbsp; server_name _;
&nbsp; &nbsp; location /nginx_status {
&nbsp; &nbsp; &nbsp; &nbsp; stub_status on;
&nbsp; &nbsp; &nbsp; &nbsp; access_log off;
&nbsp; &nbsp; &nbsp; &nbsp; allow 127.0.0.1;
&nbsp; &nbsp; &nbsp; &nbsp; deny all;
&nbsp; &nbsp; }
}

命令:

curl -s http://127.0.0.1:8080/nginx_status

输出样例:

Active connections: 120
server accepts handled requests
&nbsp;84321 84321 120932
Reading: 5 Writing: 8 Waiting: 107

判断逻辑:

  • Active connections

    持续上行:连接堆积。

  • Reading + Writing

    不大但 Waiting 高:长连接等待多,可能被打或被慢客户端吃掉。

  • accepts == handled

    不成立:被内核 drop 了,调 worker_rlimit_nofile / somaxconn

2. vts 模块(可选)

如果想看实时面板,按需装:

# 通常需要带 --add-module=/path/to/nginx-module-vts 重新编译 Nginx

不展开安装细节。

3. 日志量监控

每周至少看一眼:

ls -lhS /var/log/nginx/
du -sh /var/log/nginx

异常表现:

  • 单日日志 > 几十 G:要么日志格式太冗余,要么被打,要么没有切。
  • 单文件超过 2G:分析会很慢。

4. 关键指标告警建议

| 指标 | 计算 | 阈值(示例,按业务调整) | | — | — | — | | 5xx 比例 | 5xx_总数 / 总请求数 | > 1% 告警,> 5% 严重 | | 99 线响应时间 | request_time 99 分位 | > 1s 关注,> 3s 告警 | | wait 连接数 | Waiting | > worker_connections * 0.8 | | 磁盘 log 使用率 | du -sh /var/log/nginx | > 80% 告警 |

所有告警阈值都要按业务基线调;高峰期与静默期分开两套阈值更稳。

九、排查路径

下面给一张常见异常的”分类决策树”,结合前面的命令与配置直接对应。每一条都标明在 access_log / error_log 怎么找。

路径 1:网站变慢

  • 现象:业务反馈”打开要好几秒”,监控曲线 RT 上升。

  • 第一步:error_log 里搜 upstream timed outconnect() failedno live upstreams

  • 命中 → 进入”上游相关”分支。

  • 没命中 → 进入下一步。

  • 第二步:access_log 看 $request_time vs $upstream_response_time

  • 接近且都大 → 后端慢。

  • request_time

    远大 → Nginx 自身(写日志/压缩/缓冲)。

  • 第三步:看 worker 数量与连接数(stub_status)。Waiting 高 → 慢连接多。

  • 第四步:客户端慢 → 看 TOP 客户端 IP、UA、地理分布。

路径 2:5xx 突增

  • 现象:监控告警 5xx。

  • 第一步:按分钟聚合 5xx(见步骤 5)。

  • 第二步:按状态码 502 / 503 / 504 / 500 / 499 分桶。

  • 502 集中 → upstream 拿不到响应(连不上、超时、上游挂)。

  • 503 集中 → service unavailable,一般触发 max_fails 后 upstream 块熔断。

  • 504 集中 → upstream timed out

  • 500 集中 → 后端主动报错,跨传。

  • 499 集中 → 客户端主动断连接(”Connection: close”或不耐烦),可能慢请求或限速。

  • 第三步:对应错误码去 error_log 与后端日志对齐时间点。

路径 3:被打 CC / 扫描

  • 现象:连接数打满、带宽打满、某些 URL 高频。

  • 第一步:按 IP 找 TOP。

  • 第二步:按 URL 特征找是否集中。

  • 第三步:按 UA 看是否工具特征。

  • 第四步:决策

  • 单 IP 集中 → limit_req_zone 或 iptables 封。

  • UA 集中 → map $http_user_agent 直接 403。

  • 路径集中 → location ~* URL + limit_req

路径 4:日志写满磁盘

  • 现象:磁盘告警,access_log 把 /var/log 写满。

  • 第一步:du -sh /var/log/nginx

  • 第二步:判断 logrotate 是否在跑。

  • logrotate -d /etc/logrotate.d/nginx

    模拟跑。

  • cat /var/lib/logrotate.status

    看上次成功时间。

  • 第三步:处置

  • 临时:find /var/log/nginx -name '*.log.*.gz' -mtime +3 -ls 确认后 -delete

  • 长期:logrotate 配 rotate 7compressnotifempty

路径 5:反代后看到的是代理 IP

  • 现象:限速完全失效,所有请求被同一个 IP 抓住。
  • 第一步:检查 real_ip_header 与 set_real_ip_from
  • 第二步:确认 XFF 头是否被中间层剥离。
  • 第三步:升级 limit_req_zone 的 key 为 $binary_remote_addr 时,确保 real_ip 已生效。

路径 6:客户端拿 429

  • 现象:正常用户报告”网页打不开”。
  • 第一步:access_log 看是否集中到某些 IP。
  • 第二步:调小限速阈值或加例外 URI 白名单。
  • 第三步:调客户端重试策略(Retry-After 头)。

路径 7:扫描器持续骚扰

  • 现象:404/403 持续高频。
  • 第一步:合并扫描特征进 map,比如对 /wp-login.php 这一类直接 444。
  • 第二步:把 444 替换为 return 444 让 Nginx 直接关连接不回响应。

十、风险提醒

下面列几条极易踩坑的风险点,操作前先看。

1. rm 误删日志

  • 风险:rm /var/log/nginx/access.log + nginx -s reload 不会释放磁盘,因为 fd 仍指向旧 inode。
  • 替代:> /var/log/nginx/access.log 把文件 truncate 老 inode;或 logrotate -f 强制切。

2. truncate / > file

  • 风险:当前 Nginx master 进程的 fd 仍指向旧 inode;老 inode 被删除释放要等 worker 释放。
  • 替代:用 kill -USR1 $(cat /var/run/nginx.pid) 让 Nginx 重新打开日志文件,新写入新文件。

3. find -delete 大量清理

  • 风险:条件写错就清掉所有生产日志。
  • 替代:先 find ... -ls 确认列表,再用 find ... -print | xargs -I{} echo {} 模拟,最后 -delete

4. 限速误杀

  • 风险:limit_req rate=1r/s 配 burst=0 会让大文件下载/同步请求被 429。
  • 替代:对静态资源、GET /static/* 单独配置更高的限速阈值或关闭限速。

5. 封禁段错误

  • 风险:把 SLB 段封了导致 Nginx 自己都连不上反代上游。
  • 替代:先确认内网 IP 段和白名单范围,再封外网段或未知段。

6. reload 配置失败

  • 风险:nginx -t 没跑就直接 nginx -s reload,旧进程继续跑但新配置已被解析失败,启动会因为 nginx: [emerg] 滚出告警。
  • 替代:始终先 nginx -t,出现 test failed 不再 reload。

7. 日志格式错乱

  • 风险:自定义 log_format 引号/字段拼错,Nginx 重启后下游 ELK 解析失败。
  • 替代:用 escape=json 配合 JSON 解析;上线前用一条测试请求确认。

8. buffer=flush 太激进的丢日志

  • 风险:buffer=64k flush=60s:60s 内崩溃丢 60s 日志。
  • 替代:支付/订单这类按 flush=1s;普通业务按 flush=5~10s

十一、验证方式

每一步处置后都要验证,否则就是”心里没数”。

1. 攻击源被封验证

grep '203.0.113.45' /var/log/nginx/access.log | tail -n 100 | awk '{print $9}' | sort | uniq -c

预期:100 403 持续出现。

2. 慢请求是否回到基线

jq -r '.request_time' /var/log/nginx/access.log \
&nbsp; | awk '{if($1+0 > 1) c++} END{print "slow:", c+0}'

再对比同样方法在优化前的日志,看 99 线。

3. 限速生效

for i in {1..50}; do curl -s -o /dev/null -w "%{http_code}\n" http://example.com/api/list; done | sort | uniq -c

预期:看到一部分 200、一部分 429

4. 配置 reload 成功

nginx -t
nginx -s reload
systemctl status nginx
ps aux | grep nginx | grep -v grep

预期:master PID 不变,worker PID 变化。

5. logrotate 成功

logrotate -d /etc/logrotate.d/nginx
logrotate -f /etc/logrotate.d/nginx
ls -lh /var/log/nginx/

预期:旧文件 .1 / .gz 出现,新文件开头几行新内容。

6. 误杀检查

grep '403' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head

判断:如果 TOP 403 出现自己内部服务 IP、健康检查 IP、白名单 UA,全部都是误杀源。

7. stub_status 峰值恢复

watch -n1 'curl -s http://127.0.0.1:8080/nginx_status'

判断:攻击时 Active connections 上升、攻击缓解后回落到正常基线。

十二、回滚方案

任何改动前都应该先有一个回滚预案。下面是常见变更的回滚方案清单。

1. 配置变更

  • 修改前:cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak$(date +%s)
  • 修改后:nginx -t && nginx -s reload
  • 回滚:cp /etc/nginx/nginx.conf.bakTIMESTAMP /etc/nginx/nginx.conf && nginx -t && nginx -s reload

2. limit_req / map 调整

  • 修改前:备份当前 map 与 limit_req_zone
  • 修改后:观察 TOP 403 / 状态码。
  • 回滚:直接用备份文件。

3. logrotate 不切日志

  • 排查:logrotate -d
  • 回滚:恢复 postrotate 段(USR1)。

4. iptables 误封内网

  • 排查:iptables -L -n -v | grep DROP
  • 回滚:iptables -D INPUT -s 203.0.113.45 -j DROP

5. 自定义 log_format 影响下游

  • 排查:ELK 解析报错。
  • 回滚:把 access_log 切回 combinedaccess_log /var/log/nginx/access.log combined;。注意日志格式改变会导致旧解析器失败,建议新格式上线前先在测试集群验证。

6. 日志清理误删

  • 排查:日志突然断档。
  • 回滚:从归档/异地备份恢复。

十三、生产环境注意事项

下面这些是踩过坑之后的总结,每条都是”曾经被绊倒过一次”。

1. 不要裸跑 > access.log

生产环境不要直接 echo "" > access.log。Nginx master 的 fd 仍指向旧 inode,文件被截空,但 inode 还活着,磁盘占用不变,甚至可能因为日志被截,导致下游 ELK 拿不到完整一条 JSON(不是一行的尾)。最稳的处理:

echo -n "" > /var/log/nginx/access.log &nbsp; # truncate 当前文件内容但不换 inode
kill -USR1 $(cat /var/run/nginx.pid) &nbsp; &nbsp; # 让 Nginx 重新打开日志

2. 日志采样与按业务分流

业务量大时,可考虑按 server_name 或 location 分流多个 access_log,方便按域名排查:

access_log /var/log/nginx/example.access.log main buffer=32k flush=5s;
access_log /var/log/nginx/api.access.log &nbsp; &nbsp;main buffer=32k flush=5s;

3. 反代场景的客户端 IP

反代后面必须有 set_real_ip_from + real_ip_header,否则所有”客户端”看起来都是 SLB 内网 IP。常见可信代理段:

set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 100.64.0.0/10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

上面这些是举例,实际要按你的 SLB/CDN 段写。

4. 高并发场景的 buffer

默认 access_log 每次都 write(),高并发时 IO 抖动。buffer=32k flush=5s 减少 IO 抖动,但最坏情况丢 5s 日志。业务侧必须有损失评估

  • 一般业务:5s 丢日志可接受。
  • 支付/订单/计费:5s 丢日志不可接受,调为 buffer=8k flush=1s 或不开 buffer。

5. 限速阈值要按业务调

  • 大文件上传:阈值要小,否则大请求占满限额。
  • 静态资源:可以放更宽。
  • 登录页/支付/验证码:需要更严。

6. 日志告警与归档

  • 每周把上周日志异地归档一份(OSS/S3)。
  • 当前机器只保留 7~14 天。
  • 监控 /var/log 占用,> 70% 告警。

7. 进程与文件描述符

  • worker_rlimit_nofile

    :跟 ulimit -n 保持一致或更高。

  • /proc/sys/net/core/somaxconn

    与 net.ipv4.tcp_max_syn_backlog 都要相应调高。

8. 安全加固

  • 不要把 stub_status 暴露公网。
  • access_log 不要写 /tmp 等可被任意用户读的目录。
  • 控制台的日志归档要走加密传输。

9. 多 Nginx 实例

如果有多实例(比如多个 vhost 一份 nginx 跑,端口不同),每个实例的 PID 路径要配对 logrotate 的 postrotate

10. 异常时不要直接重启

nginx -s reload 已经足够消解大部分配置错误。systemctl restart nginx 会切断现有长连接,影响业务。除非遇到 worker 异常死循环,否则不要用 restart。

十四、总结

Nginx 日志分析不是花活,靠的是”日志格式打全 + 命令行聚拢 + 限速/封禁处置 + 日志切割与验证”。这一套闭环里:

  1. 打全字段

    :自定义 JSON log_format 把 $request_time$upstream_response_time$upstream_addr 这些关键字段打出来。

  2. 聚拢异常

    :按 IP、URL、UA、时间窗聚合,定位集中度。

  3. 证据链收口

    :把异常映射到具体机器、具体接口、具体客户端;不要只看现象。

  4. 处置要稳

    :用 limit_req_zone 与 map 兜底;规则要可回滚、可灰度、可验证。

  5. 日志切割

    :logrotate + USR1 信号 + 磁盘告警。

  6. 验证闭环

    :处置后要看监控、状态码、误杀、回落到基线。

只要这套闭环在,每一次线上问题都能在分钟级定位;遇到安全告警,按上面 CC / 扫描 / SQL 注入的分桶把样本抓出来,处置后用封禁策略兜底。

排障熟练度本质上是”日志-命令-配置”三件套的肌肉记忆。把基础命令、字段含义、配置项记熟,每天复盘一次当天的告警,把坏案例写到 wiki,下一次遇到相同 pattern 就能直接走两步到位。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:马哥Linux运维 点击关注 👉 点击关注 👉《Nginx 日志分析技巧:快速找出异常请求和攻击源》

    评论:0   参与:  0