后端同学应该都遇到过这种场面:线上接口告警刷屏,你打开跳板机,手忙脚乱地 cd 到日志目录,顺手就是一个 cat app.log,想着先看看报错长什么样,结果屏幕瞬间被几十万行日志淹没,终端卡到连 Ctrl+C 都要半秒才反应。更尴尬的是,你翻了半天,啥也没定位到,反而把想找的报错信息冲得无影无踪。
这篇文章不是来教你怎么用 cat 的,而是要告诉你一件事:在 GB 级生产日志面前,cat 是最糟糕的选择。我会把后端性能排查里真正能扛事的实战命令全部摆出来,从 tail、less 这些基础工具,到 grep、awk、sed 的组合打法,再到压缩日志、滚动日志的应对方案,最后用一个完整的接口超时排查案例,把整套思路串起来。无论你是刚接手线上运维的新手,还是写了几年 Java、Go、Python 后端的老兵,这套东西都能直接抄作业。
先明确一个概念:生产日志动辄几个 GB,几十 GB 也不罕见,这可不是你本地开发环境那个几十 MB 的小文件。cat 的基本逻辑是把整个文件内容全部输出到终端,这个机制在超大文件下会带来一连串问题,我们一个个说。
1. 为什么说"无脑cat"是生产日志排查的坑
1.1 cat 到底错在哪:三个致命问题
第一,内存和 IO 被白白浪费掉。cat 会把整个文件从磁盘读进内存缓冲,再一股脑写到标准输出。GB 级文件意味着你要读几个 GB 的数据,这中间磁盘 IO、内存带宽、CPU 全都跟着遭殃。你只是想知道最后几条报错,结果却让机器把整个文件嚼一遍,典型的杀鸡用牛刀。
第二,输出量太大,终端成重灾区。终端本身是不擅长显示大文本的,你把几万行日志一次性喷到屏幕上,轻则卡顿、滚动条拖不动,重则你的 SSH 会话直接假死。而且日志里经常有各种控制字符、乱码、超长行,终端渲染这些内容时更吃力,这就是为什么很多人 cat 完大文件后,整个窗口像中了邪一样。
第三,也是最关键的,cat 没有检索能力。它只是"倒数据",定位全靠肉眼在滚滚文字里捞针。GB 级日志里想找一个 10 分钟前发生的异常,靠 cat 翻页基本等于大海捞针。你需要的是"过滤""抽取""定位"的能力,cat 一个都给不了。
1.2 动手之前:三分钟看清日志文件底细
排查任何日志问题前,我建议你先花三分钟把"现场"摸清楚,而不是闷头 cat。常用的是这几个命令:
# 文件大小,看是不是真的大到不能碰 ls -lh app.log # 文件类型,确认是纯文本还是压缩包 file app.log # 总行数,知道敌人有多少兵力 wc -l app.log # 目录下所有日志文件按改动时间倒序,知道哪些是最近在写的 ls -lht /usr/local/app/logs/ # 看看最后 20 行,判断日志格式、时间戳样式 tail -n 20 app.log这一步特别重要。先看清文件大小、行数、日志格式、时间格式,后面的命令怎么组合就有了依据。比如我看到时间戳是2025-01-15 10:23:45.123这种格式,后面用 sed 按时间范围截取时就能直接写前缀匹配。要是日志格式是2025/01/15 10:23:45,那又是另一种写法。动手前摸清底细,比一上来就试命令靠谱得多。
2. 基础三件套:tail、head、less 的正确打开方式
2.1 tail:追日志才是核心刚需
排查线上问题时,绝大多数情况你想看的是"最近发生了什么"。这时候 tail 是绝对的主角。
# 只看最后 100 行 tail -n 100 app.log # 实时跟踪文件新增内容(最常用) tail -f app.log # 哪怕日志文件被轮转、改名,也能跟着新文件继续输出 tail -F app.log # 配合 grep 做实时过滤,只盯着 ERROR 和 Exception tail -f app.log | grep --line-buffered -E "ERROR|Exception"我重点说说tail -F这个大写参数。生产环境日志一般都有 logrotate 或类似机制在滚动,文件可能被重命名成 app.log.1,然后新建一个 app.log。如果你用的是小写-f,文件句柄还指向旧文件,你会发现日志"不走了",其实信息都在 app.log.1 里。-F会监听文件名的变化,自动切到新文件上。这个坑我踩过不止一次,线上追了半天日志发现追的是已经滚走的旧文件,血压直接拉满。
grep --line-buffered这个参数也要解释一下。默认 grep 在管道里是块缓冲的,输出不一定实时,加上--line-buffered后每匹配到一行就会立即输出,配合 tail -f 做实时日志监控时体验完全不一样。
2.2 less:替代 cat 的分页查看神器
如果你真的需要从头到尾看一个日志文件,或者想在一个大文件里自由浏览、搜索,less 是 cat 的完美替代品。它的设计哲学就不是"全部输出",而是"按需渲染",文件多大都能秒开。
# 打开文件,按 q 退出 less app.log # 打开时直接跳到某个时间点/关键词(长日志特别好用) less app.log # 在 less 内搜索,/ERROR 回车,然后 n 下一个 N 上一个 /ERROR # 打开文件直接定位到第一次出现 ERROR 的位置 less -p ERROR app.log # 大写 F 跟随模式,相当于 tail -f,按 Ctrl+C 退出 less +F app.log # 显示行号 less -N app.log # 超长行不换行,用左右方向键平移查看 less -S app.logless 里我最常用的几个快捷键先列出来:
| 按键 | 作用 |
|---|---|
G | 跳到文件末尾 |
gg | 跳到文件开头 |
/word | 向下搜索关键词 |
?word | 向上搜索关键词 |
n/N | 下一个匹配 / 上一个匹配 |
F | 进入跟随模式,等价于 tail -f |
q | 退出 |
这里要说一个经验:排查问题的时候,先less -p "ERROR"直接跳到第一个报错位置,再顺着上下文往前推,比从文件开头一页页翻效率高得多。less 打开超大文件的速度基本是即时的,因为它只读取当前屏幕需要的那一部分数据,这正是 cat 做不到的。
2.3 head:只在需要看开头时出场
head 的出场率确实低一些,但你排查"服务启动失败""配置加载异常"这类问题时,日志开头往往就是关键。比如 Spring Boot 应用启动失败,错误堆栈可能就在前几十行。
# 看前 50 行 head -n 50 app.log # 看文件中某个时间段之前的内容,常配合管道 head -n 500 app.log | tail -n 50head 也可以用来做"探路"。面对一个几 GB 的未知文件,先用 head 看前几行摸清格式,再决定下一步用什么命令组合,比直接无脑 cat 稳妥太多。
3. grep、awk、sed 三板斧:把 GB 级日志"筛"成几行
3.1 grep:先定位,再查看
排查看日志的核心思路是"先过滤,后查看",而不是"先查看,再翻找"。grep 就是那个过滤器。
# 搜关键词,显示行号 grep -n "NullPointerException" app.log # 搜出包含关键字的文件有哪些 grep -l "NullPointerException" *.log # 统计出现次数 grep -c "ERROR" app.log # 正则表达式搜索,-E 是扩展正则 grep -E "ERROR|FATAL" app.log # 显示匹配行的前后 5 行上下文,这个特别关键 grep -n -A 5 -B 5 "NullPointerException" app.log # 只输出匹配到的部分,常用于提取某个字段 grep -o '"orderId":"[0-9]*"' app.log # 反向过滤,排除心跳/健康检查等噪音日志 grep -v "heartbeat" app.log这里我要强调-A和-B这两个参数。异常堆栈信息往往在 ERROR 之后的好几行,光匹配 ERROR 看不到堆栈细节等于白干。-A 20直接把后续 20 行上下文带出来,基本能覆盖 Java 异常栈的完整长度。-A后面跟多少行,我一般先试 10,不够再加,别一上来就 100 行,输出还是会太大。
还有个小技巧:grep 搜超长文本文件时,可以用LC_ALL=C临时关闭 locale 相关的字符处理,提速非常明显。比如LC_ALL=C grep -n "timeout" app.log,实测在某些老机器上能快好几倍。
3.2 awk:按列提取、按条件统计
grep 帮你定位到了行,但日志里往往一行包含多个字段:时间、线程号、日志级别、类名、消息内容。想按某一列做筛选、排序、统计,就得请 awk 出场。
# 按空格分隔,打印第 1 列(比如时间)和第 7 列(比如响应码) awk '{print $1, $7}' app.log # 自定义分隔符,比如按逗号分隔提取 awk -F',' '{print $2, $4}' app.log # 条件过滤:第 12 列数值大于 500 的行 awk -F',' '$12 > 500 {print}' app.log # 统计所有 ERROR 出现的次数 awk '/ERROR/{count++} END{print count}' app.log # 按状态码分组计数 awk '{code[$7]++} END{for (k in code) print k, code[k]}' app.log # 统计所有接口调用的耗时并按耗时排序 awk -F'[,:]' '{print $2}' app.log | sort | uniq -c | sort -rnawk 最典型的场景是统计。比如日志格式是时间,接口名,状态码,耗时ms,你想知道哪个接口调用量最大、哪个接口平均耗时最高,awk 一行就能出统计。生产环境做快速归因的时候,这个能力比打开 Excel 分析快太多了。我经常用 awk 把耗时字段揪出来,再管道给 sort 和 head,直接看 Top 10 慢请求。
3.3 sed:按行区间和时间范围截取
sed 在日志排查里最实用的功能是"按行号或按正则范围切片"。
# 打印第 1000 到 2000 行 sed -n '1000,2000p' app.log # 按时间范围截取:从 10:00:00 开始,到 10:30:00 结束 sed -n '/2025-01-15 10:00:00/,/2025-01-15 10:30:00/p' app.log # 既要在时间段内,又要包含 ERROR,可以用管道 sed -n '/2025-01-15 10:00:00/,/2025-01-15 10:30:00/p' app.log | grep -E "ERROR|Exception" # 删除空行看紧凑输出 sed '/^$/d' app.log按时间范围截取是我认为 sed 在日志领域最不可替代的场景。比如你知道线上 10:00 到 10:05 之间出了事,用 sed 把这段时间的日志全部抠出来,生成一个小文件,后面随便怎么翻都方便。这个思路基本贯穿整个排查流程:先按时间切片缩小到一个可控范围,再在这个范围里做精细分析。
3.4 管道组合:一条命令搞定 80% 的排查
单独的工具是零件,管道组合才是完整的排查武器。分享几个我反复在用的组合拳:
# 实时追踪并过滤出 ERROR 和 Exception,带行号 tail -f app.log | grep --line-buffered -n -E "ERROR|Exception" # 查看最近 1 万行里所有 ERROR,并统计数量 tail -n 10000 app.log | grep -E "ERROR" | wc -l # 从最后 5 万行里找出耗时超过 1000ms 的慢请求,按耗时降序取前 20 tail -n 50000 app.log | awk -F',' '{if ($4 > 1000) print $2, $4}' | sort -k2 -rn | head -n 20 # 用 grep 先筛出目标请求 ID 相关的行,再用 less 分页看上下文 grep -n "order_id=123456789" app.log | less # 把某段时间的日志截取出来,再按关键字过滤 sed -n '/2025-01-15 10:00:00/,/2025-01-15 10:30:00/p' app.log | grep -A 20 "NullPointerException" > error_snapshot.txt管道组合的原则是"层层缩小范围"。从 GB 级文件先砍到 MB 级,再砍到 KB 级,最后把命中的几行输出出来。整个过程都是流式处理,内存占用极小,这也是为什么这些命令能扛住超大文件的原因。我见过很多刚入行的同事,一上来就 cat 整个文件然后用鼠标找,就是没理解"流式过滤"这个概念。
4. 压缩日志与滚动日志的处理
4.1 zcat、zgrep、zless:直接查压缩包
生产环境为了省磁盘,历史日志经常是 gzip 压缩过的,文件名长这样:app.log-20250115.gz。你要做的不是先解压再查,而是直接用压缩感知命令。
# 查看压缩包末尾内容(用 zcat 配合 tail) zcat app.log-20250115.gz | tail -n 50 # 在压缩包里搜关键字 zgrep -n "NullPointerException" app.log-20250115.gz # 分页查看压缩文件 zless app.log-20250115.gz # 如果文件是 bz2 格式,用 bzgrep;xz 格式用 xzgrep # 比如: bzgrep -n "ERROR" app.log-20250115.bz2zcat 和 zgrep 的原理是解压后直接管道输出,不会在磁盘上产生临时文件,省时省空间。这里提醒一句:zcat 默认期望文件扩展名是 .gz,如果遇到非标准后缀,可以手动指定格式,比如gzip -dc file.tar.gz2 | grep xxx之类的操作,不过生产环境一般用不上。
4.2 日志轮转之后怎么快速定位
日志轮转后的场景很典型:app.log、app.log.1、app.log.2.gz一长串文件,你想找"昨天下午 3 点"的日志,总不能一个个文件翻。我的做法是:
# 先按时间倒序列出所有日志,找出目标时间段落在哪个文件里 ls -lht app.log* # 在所有 .log 和 .gz 文件里一起搜 zgrep -n "ERROR" app.log* | less # 只搜某一天的压缩日志 zgrep -n "2025-01-14 15:" app.log-20250114*.gz | grep "NullPointerException"日志轮转文件的命名规则各不相同,我见过按日期、按序号、按时间戳的,排查前先ls -lht摸清文件清单是必要动作。这一步省不了,因为跨文件搜错方向会浪费大量时间。
4.3 多文件场景下的全局排查
如果日志被拆成多个文件(比如按业务模块拆、按实例拆),你还得会跨文件搜索:
# 递归搜索目录下所有日志文件 grep -rn "order_id=123456789" /usr/local/app/logs/ # 在压缩包和普通文件里一起搜索 zgrep -n "order_id=123456789" /usr/local/app/logs/*.gz # 只列出哪些文件命中了关键字,不输出具体行 grep -rl "NullPointerException" /usr/local/app/logs/多文件场景下-l(列出文件名)是个宝藏参数。先确认"问题到底出在哪个应用/哪个实例的日志里",再集中精力看那个文件,比在整个目录里盲目搜高效得多。特别是服务通过负载均衡分发请求时,同一个 requestId 可能出现在多个实例的日志里,用grep -l先圈定实例范围,能少走很多弯路。
5. 实战演练:一个接口超时问题的完整排查链路
理论说了一大堆,不如来个完整的实战。我拿前阵子排查过的一个 Java 后端服务举例,症状是用户反馈下单接口偶发超时,报错率不算高但就是不断。
5.1 第一步:先摸清当天的日志分布
登录跳板机,先看日志目录:
cd /usr/local/app/logs/ ls -lht看到当天生产日志已经滚成了order-service.log和order-service.log-20250115.gz。先用 wc -l 估算一下量级,再把最近 50 行拿出来确认日志格式。
tail -n 50 order-service.log日志格式类似:
2025-01-15 10:23:45.123 [http-nio-8080-exec-12] INFO OrderController - request start orderId=123456789 userId=987654 2025-01-15 10:23:45.512 [http-nio-8080-exec-12] INFO OrderService - call stock service cost=387ms 2025-01-15 10:23:46.002 [http-nio-8080-exec-12] ERROR OrderController - order create timeout orderId=123456789关键信息都在同一行里,这给后面的 awk 统计打下了好基础。
5.2 第二步:先按时间窗口缩小范围
用户反馈的集中时段是上午 10:00 到 10:30,我直接用 sed 截出这个时间窗口,保存成临时文件,后面的操作都在小文件上做,响应速度飞快。
sed -n '/2025-01-15 10:00:00/,/2025-01-15 10:30:00/p' order-service.log > /tmp/snapshot.log wc -l /tmp/snapshot.log如果目标时间段跨了文件(比如正好在日志滚动的边界),就把压缩包也一起处理:
zcat order-service.log-20250115.gz | sed -n '/10:00:00/,/10:30:00/p' >> /tmp/snapshot.log这是整个排查流程里性价比最高的一步,把 GB 级文件变成几万行的小文件,后面所有命令都能跑得飞起。
5.3 第三步:从 ERROR 到具体请求
在这个时间窗口里搜异常:
grep -n -E "ERROR|Exception" /tmp/snapshot.log | head -n 50发现大量order create timeout报错,并且每一条报错行都带着 orderId。接下来要确认这些超时请求是不是集中在某个下游服务,于是用 awk 提取耗时字段并统计:
awk -F'cost=' '{print $2}' /tmp/snapshot.log | awk -F'ms' '{print $1}' | sort -rn | head -n 20这里直接看到了耗时最长的 20 次调用,最长的已经到了 5 秒以上。再按报错接口聚合一下,确认问题是不是集中在某个接口:
awk -F'OrderService' '/ERROR/{print $2}' /tmp/snapshot.log | sort | uniq -c | sort -rn统计结果把问题指向了下游库存服务的调用耗时飙升。
5.4 第四步:把异常上下文抠出来,复盘完整链路
定位到具体的 orderId 之后,我把它在日志里出现的所有行全部抓出来,看一条请求的完整生命周期:
grep -n "orderId=123456789" /tmp/snapshot.log结果发现这个请求在 10:23:45 发起,调用库存服务耗时 387ms 本来很正常,但紧接着下游返回异常,重试逻辑又触发了一次调用,最终整体耗时超了 5 秒。整个链路通过一个 orderId 串了起来,问题原因从日志角度已经清楚了:下游服务有偶发性慢调用,加上重试策略放大了耗时。
这个复盘过程如果还用 cat,就是噩梦。你想想,在一个几千万行的生产日志里,靠 cat 去翻一个 orderId 的所有痕迹,基本不可能。但用 grep 加 orderId 定位,几毫秒就出来了。
6. 常见问题与排查技巧实录
6.1 高频翻车场景速查表
我把实际排查中经常遇到的翻车场景和对应解决方案整理成一个速查表,方便你直接对号入座:
| 场景 | 错误做法 | 正确姿势 |
|---|---|---|
| 文件几个 GB,只想看最新日志 | cat app.log | tail -n 100 app.log |
| 日志文件被 rotate,tail 显示不更新 | 小写tail -f | 大写tail -F,自动跟随新文件 |
| 想看某个时间段的日志 | 自己猜行号翻 | sed -n '/起始时间/,/结束时间/p' |
| 搜 ERROR 但看不到堆栈详情 | 只用grep ERROR | grep -A 20 ERROR带出异常栈 |
| 压缩日志没法搜 | 先解压再搜 | zgrep、zcat、zless |
| 正则太慢 | 直接搜 | LC_ALL=C grep提速,或先 sed 缩小范围 |
| 想按字段统计 | 肉眼数 | awk按分隔符提取列再统计 |
| 多个日志文件找目标 | 逐个文件 cat | grep -l先定位文件,再精读 |
这个表格里的每一个"正确姿势"我都踩过对应坑。尤其 tail -F 和 sed 按时间截取这两个点,可以说是生产日志排查中价值最高的两个命令。
6.2 我私藏的几个小技巧
技巧一:给常用命令起别名。排查链路稳定之后,我把常用组合写进了 bashrc,工作效率提升非常明显。
alias err='tail -f app.log | grep --line-buffered -E "ERROR|Exception"' alias snip='sed -n "/10:00:00/,/10:30:00/p"' alias slow='tail -n 10000 app.log | awk -F"," "{if (\$4 > 1000) print}" | sort -k2 -rn'技巧二:快速定位单行 JSON 日志。现在很多后端服务会输出 JSON 格式日志,一行非常长,直接看容易瞎。用 less 打开后加上-S参数,长行会被截断显示,再用左右键平移,比默认自动换行清晰很多。
技巧三:排查顺序永远是"时间窗口 → 关键字 → 字段统计",不要一上来就抓瞎搜。我见到太多人拿到几 GB 日志直接 grep 一个词,结果输出几十万行,依然没法看。正确顺序是先缩小时间范围,再缩小关键字范围,最后用 awk 做统计输出,一步步逼近问题本源。
技巧四:记得看日志文件编码。有些老系统日志是 GBK 编码,grep 中文关键词时会搜不到。先用file app.log确认编码,如果不匹配可以加iconv -f GBK -t UTF-8 app.log | grep xxx转换一下再搜。这个坑在老旧银行、电信系统里特别常见。
技巧五:如果文件实在太大,比如超过 20GB,建议先用 split 切分,再并行处理。split -b 500m app.log part_能切成 500MB 的小块,配合xargs -P 4并行 grep,可以在几分钟内完成全量扫描。不过这只是极端场景的兜底方案,绝大多数情况靠前面的管道组合就够了。
最后再说几句体己话。我工作这几年,见过太多人在生产日志面前手足无措,第一反应永远是 cat,然后被日志洪流淹没,最后只能一脸无奈地去问运维"能不能给我导个小的日志文件"。但其实,排查日志的本质不是"看",而是"算"和"筛"。你看再多的日志,不如花几分钟想清楚:我要找的数据在哪个文件里、在哪个时间段、长什么样、需要提取哪个字段。想清楚这些,用什么命令心里就有数了。
前阵子有个新同事看完我的排查过程,感叹了一句"原来不用 cat 也能查日志"。我觉得这句话挺准确的,你不是不能用 cat,而是没必要用。GB 级日志面前,tail、less、grep、awk、sed、zcat 这套组合拳才是真正吃饭的家伙。把这套东西练熟,下次线上告警来了,你打开终端的那一瞬间,心里是踏实的。