一句话总结
grep 找行、awk 切列、sed 改内容——三剑客分工。面试手写题集中在:日志统计(Top IP/状态码分布)、批量替换、字段提取。必背管道组合:awk '{print $1}' access.log | sort | uniq -c | sort -rn | head。Shell 基础则抓住:变量、条件、循环、退出码、set -e。
初级理解
grep:按模式过滤行
grep -r 'ERROR' /var/log/app/ # 递归搜目录
grep -c 'ERROR' app.log # 统计匹配行数
grep -i -n 'timeout' app.log # 忽略大小写 + 显示行号
grep -A 3 -B 1 'Exception' app.log # 显示后 3 行前 1 行(看上下文)
grep -v 'DEBUG' app.log # 反向:排除 DEBUG 行
grep -E 'ERROR|WARN' app.log # 扩展正则,多模式
awk:按列处理(默认空格分列,$1 第 1 列,$NF 最后 1 列)
awk '{print $1, $7}' access.log # 取第 1、7 列
awk -F: '{print $1}' /etc/passwd # 指定冒号分隔
awk '$9 == 500 {print $1}' access.log # 条件过滤:状态码 500 的 IP
awk 'END{print NR}' access.log # 总行数(比 wc -l 更灵活)
sed:流编辑(查找替换是主角)
sed 's/foo/bar/' file.txt # 每行第一个 foo → bar
sed 's/foo/bar/g' file.txt # 全局替换
sed -i 's/8080/9090/g' app.conf # 直接改文件(改前建议备份)
sed -n '100,200p' file.txt # 打印 100~200 行(超大文件翻页看)
中级深入
日志统计三连(手写题 TOP1,逐词都能解释):
# 访问量 Top10 的 IP
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
# ↑ 排序 ↑ 相邻去重计数 ↑ 按次数倒序
# 原理链:awk 抽列 → sort 把相同值排到一起 → uniq -c 才能计数 → sort -rn 数字倒序
# 状态码分布
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# 每分钟请求量(取时间字段前 17 位聚合)
awk '{print substr($4,2,17)}' access.log | sort | uniq -c
find + xargs 组合(批量操作):
find /data/logs -name '*.log' -mtime +30 -delete # 删 30 天前日志
find . -name '*.tmp' | xargs rm -f # xargs 转成参数
find . -name '*.sh' -print0 | xargs -0 grep -l 'TODO' # 文件名带空格用 -print0/-0
find . -name '*.java' | xargs grep -l 'OldApi' # 找引用旧 API 的文件
Shell 脚本骨架(写"规范脚本"的印象分):
#!/bin/bash
set -euo pipefail # 出错即退 / 未定义变量报错 / 管道任一环失败即失败
LOG_DIR=${1:-/var/log/app} # 参数默认值
if [[ ! -d "$LOG_DIR" ]]; then
echo "目录不存在: $LOG_DIR" >&2 # 错误信息走 stderr
exit 1
fi
for f in "$LOG_DIR"/*.log; do
echo "处理: $f"
done
高级拓展
awk 进程级应用(能写小程序):
# 统计接口耗时 P95(假设第 N 列是耗时 ms)
awk '{a[NR]=$NF} END{asort(a); print a[int(NR*0.95)]}' access.log
# 分组求和:按 URL 统计总耗时 Top5
awk '{sum[$7]+=$NF; cnt[$7]++} END{for(u in sum) print sum[u], cnt[u], u}' access.log \
| sort -rn | head -5
# 两个文件按 key 关联(类似 join)
awk 'NR==FNR{m[$1]=$2; next} {print $0, m[$1]}' map.txt data.txt
crontab 要点:五段 分 时 日 月 周;*/5 * * * * cmd 每 5 分钟。三个必答坑:① 环境变量与登录 shell 不同——用绝对路径或脚本里 source profile;② 输出没重定向会发邮件——统一 >> /var/log/xx.log 2>&1;③ 脚本要加锁防重入(flock -n /tmp/x.lock)。
性能与安全意识:大文件避免 shell 循环逐行处理(awk 单遍快一个数量级);变量永远加引号(防空格拆词);不要 eval 拼接用户输入;敏感信息别 echo 到日志。
面试加分项:主动对比替代工具——统计用 goaccess、JSON 日志用 jq、复杂转换写 Python 更可维护。"知道什么时候不用三剑客"比"三剑客滚瓜烂熟"更显工程判断。
实战场景
场景一:线上 5 分钟内统计慢请求(面试模拟真实压力)
# 找出最近 5 分钟耗时超过 1s 的请求及分布
tail -n 100000 access.log \
| awk '$NF > 1000 {print $7, $NF}' \
| sort | uniq -c | sort -rn | head -20
# 先 tail 限定范围再 awk(全量 awk 慢),输出"接口 出现次数 耗时"
场景二:批量替换配置并备份
# 替换所有 conf 里的旧域名,逐个备份 .bak
find /etc/app -name '*.conf' \
| xargs -I {} sh -c 'cp {} {}.bak && sed -i "s/old.example.com/new.example.com/g" {}'
# -I {} 占位逐个执行;sh -c 让 cp 和 sed 成组
场景三:服务健康检查脚本(常用题)
#!/bin/bash
set -euo pipefail
URL=http://localhost:8080/health
CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 3 "$URL")
if [[ "$CODE" == "200" ]]; then
echo "OK"
else
echo "UNHEALTHY: $CODE" >&2
exit 1
fi
# 配合 flock 防重入 + crontab 每分钟执行 + 连续失败才告警(状态文件)
面试模拟
Q:为什么统计 Top IP 要先 sort 再 uniq -c?
A:uniq 只对相邻的重复行去重计数——不排序的话同一个 IP 散落在各处会被算多次。所以标准链是 awk 抽列 → sort 归组 → uniq -c 计数 → sort -rn 按次数倒序。数据量极大时可以只用 awk 哈希一步完成:awk '{c[$1]++} END{for(k in c) print c[k],k}',省掉排序进程,快且少一次磁盘 IO(但输出无序,仍要 sort)。
Q:set -e 不够吗,为什么要 set -euo pipefail?
A:set -e 有著名盲区:① 命令在 if 条件或 && 链里失败不触发退出;② 管道只看最后一个命令的退出码——grep 无匹配 | wc -l 时 wc 成功,grep 的失败被吞掉。pipefail 让整条管道任一环失败即失败;-u 把未定义变量当错误(防 typo 造成空值灾难);-e 在非预期失败时立即退出。三个一起才构成"出错快停"的脚本基线。
Q:日志文件 10 个 G,怎么只看第 500 万行附近?
A:不能用 head|tail 全量过两遍(虽可行但慢)。首选 sed -n '4999990,5000010p' file(sed 流式处理,到目标区间即止但仍需扫描前段);更快的方案是先 grep -n 定位关键字的行号再局部 sed;如果是结构化定位,用 awk 'NR>=4999990 && NR<=5000010'。考察点:知道行号定位在大文件上没有索引捷径,能按"扫描范围最小化"组织命令。