【前情提要】
sed 练熟了,小林能脱敏 IP、提取时间段、删除空行和注释了。老周说:“grep 筛,awk 算,sed 改,三把刀你都会了。但真正分析日志,得把它们串起来。”小林打开终端,准备学组合拳。但他不知道,grep | awk | sort | uniq -c | sort -rn | head 这条管道,每一步都有讲究。
一、老周的任务:日报
周一早上,老周丢给小林一张纸。
上面写着:
日志分析日报
1. 访问总量
2. Top 10 IP
3. Top 10 URL
4. 状态码分布
5. 5xx 错误对应的 URL
6. 平均响应大小
7. 排除内网 IP 后的访问量
“这是什么?”小林问。
“日报。”老周说,“你以后每天要出一份。老板要看,我也要看。你手动做,还是写脚本?”
“写脚本。”
“对。”老周说,“但你写脚本之前,得先知道每条怎么算。你今天把七条命令写出来,明天再整合成脚本。”
小林看着纸上的七条,打开终端。
二、第一步:访问总量
“最简单的。”老周说,“总行数。”
sudo wc -l /var/log/nginx/access.log
输出:
102347 /var/log/nginx/access.log
“10 万条。”小林说。
“但 wc -l 算的是行数。如果日志里有空行,或者格式不对,行数不准。”老周说,“更精确的是 awk 'END {print NR}'。”
sudo awk 'END {print NR}' /var/log/nginx/access.log
输出:
102347
“一样。”小林说。
“对,因为这个日志没有空行。”老周说,“但你要养成习惯,统计用 awk,不用 wc。awk 更灵活,还能加条件。”
小林记下:
# 访问总量
awk 'END {print NR}' /var/log/nginx/access.log
# 只看 200 的数量
awk '$9 == 200' /var/log/nginx/access.log | wc -l
三、第二步:Top 10 IP
“这个你练过。”老周说。
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
输出:
5000 192.168.1.100
1200 192.168.1.101
800 192.168.1.102
300 192.168.1.103
100 192.168.1.104
“等等,这些全是内网 IP。”小林说,“我博客不是给外面看的吗?”
“你本地虚拟机,哪来的外网?”老周说,“你以后上生产,就会看到真实公网 IP。现在先练手。”
四、第三步:Top 10 URL
sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
输出:
3000 /posts/my-first-post/
1500 /
800 /tags/hugo/
500 /about/
300 /images/hugo-logo.png
200 /favicon.ico
100 /posts/hello-world/
50 /archives/
30 /categories/
20 /404.html
“你的文章最高,首页第二。”老周说,“/404.html 有点意思,说明有人访问了不存在的页面。你以后可以看看 404.html 的请求,找出哪些链接坏了。”
“怎么看?”
“awk '$9 == 404 {print $7}'。”老周说,“你后面会练到。”
五、第四步:状态码分布
sudo awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
输出:
95000 200
5000 404
1500 301
500 500
347 502
“200 占绝大多数,404 有 5000,500 有 500,502 有 347。”小林说。
“502 有 347 个?”老周皱眉,“你博客是静态的,怎么会有 502?”
“我不知道。”
“你查一下 error.log。”
sudo grep " 502 " /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -10
输出:
347 /api/health
“全是 /api/health。”小林说,“又是那个死循环脚本留下的。”
“对。”老周说,“状态码分布能告诉你‘哪里出错了’。 你以后每天看一次,异常状态码一多,就去看具体 URL。”
六、第五步:5xx 错误对应的 URL
“把 5xx 的 URL 列出来。”
sudo awk '$9 >= 500 && $9 < 600 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
输出:
347 /api/health
500 /api/login
“/api/login 有 500 个 500?”小林说,“这不对啊,我博客没有登录功能。”
“可能是别人在扫你的站。”老周说,“公网上,每天都有扫描器到处试 /api/login、/admin、/.env。你现在本地,可能是我之前测试留下的。”
“那怎么区分?”
“看 IP。”老周说,“你 awk 把 IP 也带上。”
sudo awk '$9 >= 500 && $9 < 600 {print $1, $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
输出:
347 192.168.1.100 /api/health
500 10.0.0.5 /api/login
“10.0.0.5 是谁?”
“我另一个虚拟机的 IP。”老周说,“我测过你的博客。你看,加上 IP,就能区分是正常请求还是扫描。”
小林记下:
# 5xx URL 分布
awk '$9 >= 500 && $9 < 600 {print $7}' access.log | sort | uniq -c | sort -rn | head
# 5xx URL + IP
awk '$9 >= 500 && $9 < 600 {print $1, $7}' access.log | sort | uniq -c | sort -rn | head
七、第六步:平均响应大小
“$10 是响应大小。”老周说。
sudo awk '{sum += $10; count++} END {print sum/count}' /var/log/nginx/access.log
输出:
1234.56
“平均 1234 字节。”
“但 $10 可能是 -。”老周说,“你加个条件。”
sudo awk '$10 != "-" {sum += $10; count++} END {print sum/count}' /var/log/nginx/access.log
输出:
2345.67
“排除 - 后,平均 2345 字节。”小林说。
“这两个值差挺多,说明有些请求没有响应体。”老周说,“你以后统计,先确认字段是否有效。”
八、第七步:排除内网 IP
“最后一条,排除内网 IP 后的访问量。”老周说。
“怎么排除?”
“grep -v。”
sudo grep -v -E "^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)" /var/log/nginx/access.log | wc -l
输出:
1500
“1500 条外网访问。”小林说。
“这个正则匹配所有内网 IP 段:192.168.、10.、172.16. 到 172.31.。”老周说,“grep -v -E 是排除多个模式的标准写法。”
“那如果我只想看外网 IP 的 Top 10 呢?”
sudo grep -v -E "^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10
输出:
800 203.0.113.5
300 198.51.100.22
200 203.0.113.99
100 198.51.100.44
100 203.0.113.1
“这才是外网访问。”老周说,“排除内网,是为了看真实用户。 你以后生产环境,内网请求可能是监控、健康检查,不是真实用户。”
小林记下:
# 排除内网 IP
grep -v -E "^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)" access.log | wc -l
# 外网 Top 10
grep -v -E "^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)" access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
九、组合拳:一条管道搞定复杂分析
“七条你都写出来了。”老周说,“但真正的高手,是把它们串成一条。”
“怎么串?”
“比如,你想找出‘访问量最高的 5 个外网 IP,且这些 IP 的请求里有 500 错误’。”
小林想了想:“先筛外网 IP,再筛 500,再统计?”
“对。”
sudo grep -v -E "^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)" /var/log/nginx/access.log | awk '$9 == 500 {print $1}' | sort | uniq -c | sort -rn | head -5
输出:
300 203.0.113.5
100 198.51.100.22
“这条管道干了四件事:”老周说。
grep -v -E:排除内网 IPawk '$9 == 500':筛出 500 错误awk '{print $1}':提取 IPsort | uniq -c | sort -rn | head:统计 Top 5
“每一步都缩小范围,最后得出精确结论。”老周说,“这就是组合拳。grep 筛,awk 算,sort | uniq -c 统计。”
“那 sed 呢?”
“sed 在管道里用得少,但脱敏时用。”老周说,“比如你把结果脱敏:”
sudo grep -v -E "^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)" /var/log/nginx/access.log | awk '$9 == 500 {print $1}' | sort | uniq -c | sort -rn | head -5 | sed 's/[0-9]*$/***/'
“把 IP 最后一段替换成 ***。”
十、排错挑战:管道中某一步结果为空
下午,小林想统计“访问量最高的 5 个 404 URL,且排除 /api/health”。
他写:
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | grep -v "/api/health" | sort | uniq -c | sort -rn | head -5
输出为空。
“啥?”小林又敲了一遍。还是空。
他检查每一步:
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | head -5
输出:
/api/health
/api/health
/api/health
/api/health
/api/health
“有 404 啊。”小林说。
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | grep -v "/api/health" | head -5
输出为空。
“grep -v 之后全没了。”小林说,“说明所有 404 都是 /api/health?”
他统计:
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
输出:
5000 /api/health
“对,所有 404 都是 /api/health。”小林说,“难怪排除后为空。”
他喊来老周。
老周看了一眼:“管道中某一步结果为空,不一定是命令写错,可能是数据本身就没有。 你先单独跑每一步,确认每步的输出。”
“那我怎么知道是数据问题还是命令问题?”
“逐步调试。”老周说,“你把管道拆开,每一步 | head 看输出。哪一步开始为空,问题就在哪一步。”
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | head -5
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | grep -v "/api/health" | head -5
“第一步有输出,第二步为空,说明数据全是 /api/health。”老周说,“不是命令问题,是数据问题。”
排错思路总结:
- 管道结果为空,先逐步拆开,每一步
| head看输出。 - 找到第一步为空的命令,问题就在那。
- 如果命令本身没问题,检查数据:
wc -l、head、tail。 - 用
sort | uniq -c统计全部数据,确认是否有你想要的内容。 - 管道调试的核心:分段执行,逐步缩小。
- 别一上来就写完整管道,先写单步,确认无误再串。
“记住,”老周说,“管道是‘流水线’,每一步都可能出问题。 你写复杂管道,先写单步,再串起来。串完之后,如果结果不对,再拆开逐步检查。这是调试管道的基本功。”
十一、练习
- 练习一:用一条管道命令,统计
access.log中访问量最高的前 5 个外网 IP。要求排除192.168.、10.、172.16.到172.31.的内网 IP。 - 练习二:用一条管道命令,找出所有状态码为 500 的请求中,访问量最高的前 5 个 URL。然后加上 IP 字段,找出是哪些 IP 在触发 500。
- 练习三:用管道命令,把
access.log中的 IP 脱敏(最后一段替换为***),然后统计脱敏后每个 IP 的访问量。对比脱敏前后的 Top 5 IP,确认脱敏不影响统计结果。
参考答案
练习一:
sudo grep -v -E "^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -5
练习二:
# 500 请求 Top 5 URL
sudo awk '$9 == 500 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -5
# 500 请求的 IP + URL
sudo awk '$9 == 500 {print $1, $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
练习三:
# 脱敏并统计
sudo sed 's/\([0-9]*\.[0-9]*\.[0-9]*\.\)[0-9]*/\1***/g' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -5
# 原 IP Top 5
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -5
十二、下集预告
组合拳练完了,小林能从日志里挖出各种统计信息。老周说:“你手动跑命令没问题了,但每天手动跑,太累。你得写个脚本,让机器每天自动出日报。”小林打开 nano,准备写日报脚本。但他不知道,date、if、for、cron,每一个都有坑。
下一集:《写一个“日志日报”脚本》