用awk分析Nginx日志:七个日常报表

一、为什么要学 awk:日志分析的第一把刀

一个日访问几千 IP 的站点,一天的 Nginx 访问日志大概在几十万行、几百 MB。这种体量下,可视化工具往往还没加载完,而 grep 只能做「筛选」不能做「汇总」。awk 补的正是这一块:它天生按行处理、按列切分、自带计数与求和,一条命令就能把几十万行日志压缩成一张几十行的报表。更关键的是它没有依赖,任何一台 Linux 服务器上都有。

很多教程讲 awk 一上来就是 BEGIN、END、正则、数组,学完却不知道在真实日志上用。这篇文章倒过来:从一个站长每天真正要回答的问题出发,每个问题给一条可以直接粘贴运行、并且已经考虑了大文件与格式差异的命令。

二、先搞清楚自己的日志长什么样

awk 的一切都以「字段」为前提,所以第一步必须看日志格式。Nginx 默认的 combined 格式长这样:

203.0.113.45 - - [05/Nov/2026:14:23:11 +0800] "GET /archives/1980.html HTTP/2.0" 200 18234 "https://www.example.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"

按空白切分后,字段编号是:

字段内容备注
$1客户端 IP有 CDN 时必须先做真实 IP 还原
$4[05/Nov/2026:14:23:11带左方括号,需要处理
$5+0800]时区
$6"GET带左引号
$7请求路径通常是我们要的核心
$9状态码
$10响应字节数可能是 -
$11Referer带引号

这套编号是最常见的坑来源:只要日志格式多一个字段(比如加了 $request_time 或者 $upstream_response_time),后面的字段号就全部错位。所以每次换服务器、换 CDN、改了 log_format,都要重新数一遍。判断方法是先 tail -1 看一眼,再确认你要的字段在第几位。

一个更稳的做法是用 awk 的 FS 直接按引号切分——但那样更复杂。对个人站来说,老老实实数位置、并且把位置写在命令上方的注释里,是最省心的。

三、问题一:今天哪些 IP 访问最多

awk '{print $1}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -20

这是最经典的组合。但纯 awk 写法更省内存,且不需要两级排序:

awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' /var/log/nginx/access.log \
  | sort -rn | head -20

为什么值得用后一种?因为 awk 内部的关联数组在内存里累加,不需要先整体排序整个文件。sort 对几百万行做一次外部排序,磁盘 I/O 很可观,而 awk 版本只对「唯一 IP 数」这一小份数据排序。IP 数一般几千到几万,排序成本可以忽略。

但这里有个前提必须先解决:如果站点在 CDN 后面,$1 全是 CDN 节点的 IP,这个报表毫无意义。先用 tail -3 看一眼,如果全是相似网段,那就得先做真实 IP 还原。Nginx 侧需要 ngx_http_realip_module 和 set_real_ip_from 配置可信来源,或者直接在日志格式里用 $http_x_forwarded_for。这个配置问题不解决,后面所有按 IP 的分析都是错的。

四、问题二:哪些 URL 被打得最多

awk '{count[$7]++} END {for (u in count) print count[u], u}' /var/log/nginx/access.log \
  | sort -rn | head -30

实际使用中要做两处改良。第一,排除静态资源,否则结果永远是 favicon.ico 和某张图片:

awk '$7 !~ /\.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?)($|\?)/ {count[$7]++}
     END {for (u in count) print count[u], u}' /var/log/nginx/access.log \
  | sort -rn | head -30

第二,URL 里的查询串会把同一个页面拆成几十条记录,统计前要截掉问号后面的部分:

awk '{
       u = $7
       sub(/\?.*$/, "", u)
       count[u]++
     }
     END {for (u in count) print count[u], u}' /var/log/nginx/access.log \
  | sort -rn | head -30

这两步做完,报表才有意义。实际经验是:正常情况下前 30 名里应该有一半以上是文章页/栏目页,且分布相对分散。如果某一条页面占了总量的 20% 以上,要么是它真的爆了,要么是有人在刷它。

五、问题三:按小时看流量曲线

时间字段 $4 是 [05/Nov/2026:14:23:11,取小时就是第 2 个冒号前的两位:

awk '{split($4, t, ":"); print substr(t[2], 1, 2)}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -k2

纯 awk 版:

awk '{split($4, t, ":"); h[substr(t[2],1,2)]++}
     END {for (i = 0; i < 24; i++) printf "%02d %d\n", i, h[sprintf("%02d", i)]}' \
     /var/log/nginx/access.log

注意这里用了 sprintf("%02d", i) 而不是直接 h[i]。原因是 awk 的数组键是字符串,如果日志里小时写成 09 而循环用数字 9 去取,会取不到(9 和 "09" 是两个不同的键)。这是 awk 新手最容易迷惑的地方之一,症状是「某些小时显示 0,明明有访问」。

有了小时曲线,很多判断就变得简单:正常站点的曲线有明显的高低起伏(凌晨低、上午和晚上高);如果某几个小时突然出现平台上完全平的一条高位,或者凌晨三点有个尖峰,那就值得查。国内站凌晨的尖峰,十有八九是采集器或者扫描器。

六、问题四:状态码分布与 5xx 定位

awk '{s[$9]++} END {for (k in s) print k, s[k]}' /var/log/nginx/access.log | sort -k2 -rn

健康站点的状态码分布大致是:200 占绝大多数,301/302 少量(跳转),304 很多(缓存命中),404 少量,5xx 应该接近零。如果 5xx 超过千分之一,就该查了。定位具体是哪些 URL 在报 5xx:

awk '$9 ~ /^5/ {print $9, $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

两个值得注意的信号。第一,499 不是 5xx 但同样重要——它的含义是「客户端在响应返回前主动断开」,大量 499 说明后端太慢,用户等不及走了。这正是很多「站点变慢」的最早信号:用户还没抱怨,日志先说话了。

awk '$9 == 499 {c[$7]++} END {for (u in c) print c[u], u}' /var/log/nginx/access.log \
  | sort -rn | head -10

第二,401/403 突然增多通常意味着有人在扫后台或者探测目录。配合 URL 一起看,如果 403 集中在 /wp-admin、/.env、/phpmyadmin 这类路径上,就是扫描无疑。

七、问题五:统计真实出口流量

按天统计总流出字节,注意 $10 可能是 -(对 304 响应常见),直接相加会出错:

awk '{
       if ($10 ~ /^[0-9]+$/) total += $10
     }
     END {printf "%.2f MB\n", total / 1024 / 1024}' /var/log/nginx/access.log

如果想知道每个 URL 的流量占比(找出谁在吃掉带宽):

awk '{
       if ($10 ~ /^[0-9]+$/) { u = $7; sub(/\?.*$/, "", u); bytes[u] += $10 }
     }
     END {for (u in bytes) print bytes[u], u}' /var/log/nginx/access.log \
  | sort -rn | head -15 \
  | awk '{printf "%.1f MB  %s\n", $1/1048576, $2}'

这条命令的输出常让人意外:图片、字体、音视频文件往往占了 80% 以上的字节,而文章页本身只占一小部分。这直接决定了优化方向——如果流量主要被图片吃掉,你该做的是压缩图片和开 CDN,而不是去调整 PHP 参数或者加索引。用数据决定优化位置,比凭感觉改配置省事得多。

八、问题六:找出爬虫的真实行为

用 User-Agent 做粗筛,统计各 UA 的请求量:

awk '{
       ua = $12
       for (i = 13; i <= NF; i++) ua = ua " " $i
       sub(/^"/, "", ua)
       sub(/"$/, "", ua)
       count[ua]++
     }
     END {for (u in count) print count[u], u}' /var/log/nginx/access.log \
  | sort -rn | head -20

这里必须处理 UA 里的空格:默认的 combined 格式里 UA 是带引号的一段,内含空格,所以 $12 只是它的开头,必须把从 12 到 NF 的字段拼起来。这是很多人直接 print $12 得到「Mozilla/5.0」这一截的原因。

识别假爬虫的方法:真正的 Googlebot、Bingbot 会声明自己,你可以通过反向 DNS 校验(把 IP 反解成域名,再正解回去看是否是官方域)来确认。更快的方法是从行为上判断——真爬虫遵守 robots.txt、请求间隔均匀、不会盯着某个 URL 反复请求。假爬虫(采集器)的典型特征是:单一 IP 短时间请求几百个文章页、UA 伪装成 Chrome 但完全不加载 CSS/JS。

awk '$12 ~ /Chrome/ && $7 !~ /\.(css|js|png|jpg|gif|woff2?)/ {c[$1]++}
     END {for (ip in c) if (c[ip] > 200) print c[ip], ip}' /var/log/nginx/access.log \
  | sort -rn | head -20

这条命令抓的是「声称自己是浏览器、但只请求 HTML 不请求任何静态资源、且单 IP 超过 200 次」的 IP。真浏览器不会有这种行为,因为它必然要加载 CSS 和图片。这是识别采集器最有效的一条判据,比看 UA 字符串可靠得多。

九、问题七:把报表变成每天自动产出

手动跑一次能解决一次问题,自动化才能形成长期观测。做一个每天凌晨执行的脚本,输出到文件而不是发邮件(邮件容易被当成垃圾):

#!/bin/bash
set -Eeuo pipefail
LOG=/var/log/nginx/access.log
OUT=/var/log/nginx/report/daily-$(date -d yesterday +%Y%m%d).txt
mkdir -p "$(dirname "$OUT")"

{
  echo "== Top 20 IP"
  awk '{c[$1]++} END {for (i in c) print c[i], i}' "$LOG" | sort -rn | head -20
  echo "== Top 20 URL"
  awk '{u=$7; sub(/\?.*$/,"",u); c[u]++} END {for (i in c) print c[i], i}' "$LOG" \
    | sort -rn | head -20
  echo "== Status codes"
  awk '{c[$9]++} END {for (i in c) print i, c[i]}' "$LOG" | sort -k2 -rn
  echo "== Total MB"
  awk '$10 ~ /^[0-9]+$/ {t+=$10} END {printf "%.2f\n", t/1048576}' "$LOG"
} > "$OUT"

几个细节值得说。第一,日志轮转(logrotate)默认是每天切割,所以这个脚本跑在切割之后就只能看到当天的日志——建议先跑报表再切割,或者让报表脚本读 access.log.1。第二,set -Eeuo pipefail 保证任何一步失败都能被发现,否则管道中间断了脚本还会「成功退出」,生成一份空报表。第三,用 date -d yesterday 而不是 date +%Y%m%d,让文件名和内容对得上。

更进一步,可以把「今天的异常」写成几个阈值判断,超了就打印 WARNING 行。比如 5xx 数量超过 50、单 IP 请求超过 5000、某个 URL 占比超过 25%,这三个阈值基本覆盖了最常见的问题。报表的价值不在于每天都看,而在于出问题的那天你能立刻拿到「和平时比哪里不一样」。

十、性能与大文件的两条注意事项

第一,不要对未压缩的历史日志直接跑多次 awk。一天的日志 300MB,跑五次就是 1.5GB 的顺序读,在机械盘或者低配 VPS 上会明显拖慢系统。稳妥做法是先 gzip -c 归档,需要分析时 zcat 一次,把多个统计合并进同一个 awk 程序——awk 天生支持在一次遍历里算多个指标,把上面所有 count[] 数组放在同一个脚本里就行。

第二,虚拟内存要留意。awk 的关联数组把每个唯一键存在内存里。如果日志里 URL 带大量随机查询串(比如 ?t=1699…),唯一键数量可能到百万级,内存占用会到几百 MB。这时先用 sub(/\?.*$/, "", u) 归一化再统计,能把键数压回到几千。这个细节决定了你的脚本在一台 1G 内存的机器上能不能安全跑。

十一、小结

awk 在日志分析上的价值,不是「语法有多强」,而是「一次遍历能算多少件事」。上面七类问题如果用 grep 加管道拼,每个都要扫一遍文件;用 awk 把它们合并进一个程序,就只需要一次 I/O。这个差别在日志变到几百 MB 时会非常明显。

学习顺序建议是:先掌握「关联数组累加 + END 里输出 + sort 排序」这一个模式,它就覆盖了 80% 的需求;再掌握 split 处理时间字段、sub 归一化 URL、NF 拼接多空格字段这三个技巧;最后才是 BEGIN 初始化、格式化输出这些细节。真正要练的不是记语法,而是记住「日志字段在第几位」——这个只有翻自己的日志才学得到。

Last modification:November 5th, 2026 at 07:27 pm

Leave a Comment