拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux日志排查实战:从命令组合到线上故障定位

Linux日志排查实战:从命令组合到线上故障定位

干后端和运维这些年,我最大的感受就是:linux命令学得再花哨,到最后每天高频使用的还是看日志那一套。不管是接口突然报 500,还是半夜被告警吵醒,你第一个动作永远是 ssh 上机器,然后 tail、grep、less 三连。日志这东西,平时没人惦记,一旦出问题,它就是现场唯一的目击证人。

这篇内容不打算把命令手册抄一遍,而是拿实际排查场景来讲:日志放在哪、怎么看实时输出、怎么从大海捞针一样找关键行、日志轮转压缩了怎么处理、以及一次线上故障里这些命令是怎么串起来的。适合刚接触 Linux 的开发者,也适合那些会用 tail -f 但遇到复杂日志场景就开始翻文档的同学。

1. 拿到一台机器,先认清日志都放在哪

很多人栽跟头不是不会敲命令,而是不知道日志文件在哪个目录。你连路径都没找对,后面的一切命令都是空谈。

1.1 系统日志和业务日志的常见落盘位置

绝大多数 Linux 发行版都遵循 FHS 文件系统规范,把系统日志放在/var/log下面。但不同发行版的命名有差异,比如 CentOS、RHEL 系列通常用/var/log/messages记录系统整体运行信息,而 Ubuntu、Debian 系列用/var/log/syslog。你在这两个文件里能看到用户登录、计划任务执行、内核打印、服务启动失败这类“大杂烩”日志。

我平时排查时,第一步永远是先列目录:

ls -lht /var/log | head -20

-l显示详细信息,-h把文件大小转成人能读的 K/M/G,-t按时间排序。前面加head -20只取前 20 行,目的是快速看最近有哪些日志文件在更新。这个动作能帮你判断系统当前最活跃的日志源是什么。

更常见的几类固定日志文件:

文件路径典型内容
/var/log/nginx/access.logNginx 访问日志,每行一条请求
/var/log/nginx/error.logNginx 运行错误记录
/var/log/mysql/error.logMySQL 启动、运行、错误信息
/var/log/mysql/slow.logMySQL 慢查询日志
/var/log/secureCentOS/RHEL 系登录验证日志
/var/log/auth.logUbuntu/Debian 系登录验证日志
/var/log/croncron 定时任务执行记录

注意/var/log/secure和/var/log/auth.log这类登录日志,没有 root 权限的话通常打不开,读取时一般要加sudo。还有两个特殊文件/var/log/wtmp和/var/log/btmp是二进制格式,不能直接cat看,需要用last查看成功登录记录、lastb查看失败登录记录。

业务应用日志就五花八门了。很多 Java 应用放在/opt/app/logs或/data/logs下,Python、Go 服务习惯写在进程启动参数指定的路径。这类自定义路径没有统一规律,建议拿到一个项目后先看启动脚本或配置文件里的 log 相关配置,把路径记住。更省事的办法是看 systemd 服务,如果应用是用 systemd 管理的,先执行这个:

systemctl status 服务名

输出里会显示服务的工作目录和执行命令,顺着这个线索找日志路径很稳。

1.2 systemd journal 是另一套日志体系

新版 Linux 系统普遍用 systemd 接管服务管理,同时也接管了部分日志记录。通过journalctl可以查看某个服务的标准输出和标准错误,它的好处是日志集中存储、按服务隔离、自带时间过滤能力。

查看某个服务的日志:

journalctl -u nginx

只看最近一小时的错误日志:

journalctl -u nginx --since "1 hour ago" --priority err

--priority err等价于只显示错误及更严重级别的日志。这个优先级体系很有意思,日志级别从 debug、info、notice、warning、err、crit、alert 到 emerg 依次升高,你指定了err之后,普通 info 日志全被过滤掉,只有错误级别以上的才会留下。定位问题时这种过滤非常高效,因为大部分服务在正常运行时的 info 日志量巨大,错误信息早被淹没了。

不过 journald 日志默认存在内存或环形缓冲里,如果没有配置Storage=persistent,系统重启后历史日志会丢。所以我自己在线上排查时,还是更依赖落盘的文本日志文件,journalctl 主要用来快速看某个服务最近的状态变化。

2. 实时跟踪和翻页阅读:tail/less 才是永不下岗的主力

一旦确定日志路径,接下来就是怎么看了。很多新手习惯直接vi打开日志文件,这在小文件上没问题,但一旦文件到了几百 MB,vi 会先把整个文件加载进内存,打开过程极慢,操作还容易卡死。这时候正确的主力工具是tail和less。

2.1 tail:追文件尾巴的正确姿势

日志是追加写入的,最新内容永远在文件末尾,所以tail天然适合看日志。

查看最后 100 行:

tail -n 100 /var/log/nginx/access.log

-n表示行数,不写默认是 10 行。想同时看多个日志文件可以这样:

tail -n 20 /var/log/nginx/error.log /var/log/myapp/app.log

如果要在日志持续写入时实时跟踪,用-f:

tail -f /var/log/myapp/app.log

-f会保持进程不退出,日志有新行写入时立刻打印到终端。排查线上问题时的标准操作是先开一个窗口执行tail -f,再另开一个窗口复现请求或等待报错发生,终端里就能实时看到日志刷出来。

这里必须多说一句-f和-F的区别。-f跟踪的是文件描述符,如果日志文件被 logrotate 重命名,然后新建了一个同名文件,tail -f会继续读那个已经被改名的旧文件,表面上好像日志停了。而-F会根据文件名重新打开文件,即使轮转后也不怕。我在生产环境几乎只用tail -F:

tail -F /var/log/myapp/app.log

这个细节是排坑重点,后面第 6 章我还会专门讲。

2.2 less:在几百 MB 日志里翻页不卡的秘诀

less是我最爱的日志查看命令,没有之一。它不会一次性把整个文件读进内存,而是按需加载当前屏幕要显示的内容,所以打开多大的文件都不慌。

常用打开方式:

less -N -S /var/log/myapp/app.log

-N显示行号,方便后面引用具体行;-S让超长的行不自动换行,而是水平滚动,这对查看堆栈日志特别友好,日志里的人眼可读性会好很多。

进入 less 之后的核心操作:

按键功能
G跳到文件末尾,看最新日志
g回到文件开头
/关键词向下搜索关键词
?关键词向上搜索关键词
n/N下一个匹配 / 上一个匹配
PgUp/PgDn翻页
q退出

看到堆栈或者长 SQL 时,先用/搜索关键字定位,再用方向键左右移动查看超长内容,这个体验比任何图形化工具都顺。

less 还有一个很多人不知道的跟综模式:直接按Shift+F,效果等同于tail -f,日志有新行会自动滚动。想退出跟综,按Ctrl+C回到普通浏览模式,然后按q退出。这个功能的好处是:它既能像tail一样跟综,又能随时停下来往上翻页看上下文,比单独用 tail 灵活得多。

2.3 head 和 cat 不是没用,只是要分场合

head和cat也是常用的,但要分清使用场景。cat适合文件很小、需要完整浏览的情况,比如看配置文件、看小体积的日志片段。但拿cat去看一个 2GB 的日志文件,终端会直接刷爆,而且为了渲染输出消耗大量 CPU 和 IO,甚至会拖慢服务器,这是一个典型的“用错工具”现场。

head通常用来查看文件开头的内容,确认日志格式:

head -n 20 /var/log/myapp/app.log

有时只想看文件已经写入的字节数前一部分,可以用-c按字节切:

head -c 1M /var/log/myapp/app.log

这条命令可以快速取出文件前 1MB 内容,确认日志的字段结构、时间格式、日志级别分布。需要注意的是,head -c可能把某一行从中间截断,这没问题,你只是想看格式而已,不需要完整行。

生产环境里把 tail、less、grep 组合起来是最高频的操作,后面第 3 章会专门展开 grep 相关的内容。

3. 从海量日志里精准捞针:grep/sed/awk 三板斧

整天tail -f是低效的。日志量大的时候,几分钟就能刷出几千行,靠肉眼盯是盯不过来的。这时你需要的是“过滤”和“提取”能力,核心工具链是 grep、sed、awk。

3.1 grep:先解决“有没有”,再看“在哪”

grep 是日志排查里使用频率最高的命令,基本功能是按行匹配关键词并输出。

查看包含 ERROR 的所有行,并显示行号:

grep -n "ERROR" /var/log/myapp/app.log

不区分大小写:

grep -i "error" /var/log/myapp/app.log

同时匹配多个关键词,用扩展正则:

grep -E "ERROR|FATAL|Exception" /var/log/myapp/app.log

排除掉干扰信息,比如健康检查请求:

grep -v "healthcheck" /var/log/myapp/app.log

只看匹配到了多少行:

grep -c "ERROR" /var/log/myapp/app.log

我实际排障时最常用的是-B和-A,它们能在匹配行的前后带上指定行数的上下文。比如请求超时日志本身没什么意义,但超时前一行往往记录着某个服务的调用链信息,这时这样查:

grep -B 5 -A 20 "DB_TIMEOUT" /var/log/myapp/app.log

-B 5表示匹配行前 5 行,-A 20表示匹配行后 20 行。这条命令能让你把“异常现场”完整提取出来,而不是只看到孤零零一个错误。

处理多个日志文件时,可以用-l列出包含关键词的文件名,先缩小范围:

grep -l "订单接口超时" /var/log/myapp/*.log

找到文件后再去对应文件里详细看。

还有一个提升效率的细节:grep -F。如果你搜索的字符串包含大量正则特殊字符(比如a.b*),不转义的话会被当成正则解释,结果完全不对。用-F表示“固定字符串”匹配,原样查找,省去转义烦恼:

grep -F "OrderServiceImpl.batchCreate" /var/log/myapp/app.log

3.2 sed:按行号和时间范围切出一段日志

grep 是按关键词捞,但如果你的需求是“把某段时间内的所有日志都取出来”,关键词就不管用了,因为这段时间里的日志什么内容都有。这时用 sed 更合适。

按行号提取,从第 100 行到第 200 行:

sed -n '100,200p' /var/log/myapp/app.log

-n表示关闭默认输出,p表示打印匹配范围的行。这个命令在日常调试中可以用来快速定位某个时间段对应的日志区域。

更实用的场景是按时间范围提取。假设日志第一列是2025-06-15 14:30:00这种格式,想提取 14:30 到 14:35 之间所有日志:

sed -n '/2025-06-15 14:30:00/,/2025-06-15 14:35:00/p' /var/log/myapp/app.log

这个写法的含义是:从匹配到开始时间的行开始打印,一直打印到匹配到结束时间的行。需要注意两点:一是开始时间和结束时间的字符串必须在日志里真实存在;二是如果这段时间内没有日志写入,或者结束时间字符串在开始之前出现,结果会出乎意料,所以最好先grep确认两个时间点都有匹配行。

把提取出来的内容接到管道里继续处理,是更高级的用法:

sed -n '/2025-06-15 14:30:00/,/2025-06-15 14:35:00/p' /var/log/myapp/app.log | grep -E "ERROR|WARN" | less

3.3 awk:提取字段和快速统计

awk 可以理解为“按列处理文本”的工具。日志基本都是结构化的,通用格式里空格分列,Nginx 访问日志默认用空格分列,其中 IP 在第一列、HTTP 状态码在第九列左右。

提取每行的第一列和最后一列:

awk '{print $1, $NF}' /var/log/nginx/access.log

$1是第一列,$NF是最后一列,NF是每行字段数量的内置变量,所以$NF就表示最后一个字段。对新手来说,awk 最熟悉的场景就是这种“取出几个字段看看”。

按响应时间过滤,比如取出耗时超过 3 秒的请求:

awk '$NF > 3000 {print $1, $NF}' /var/log/nginx/access.log

要提前确认最后一列是请求耗时。

更强大的统计能力体现在配合数组和END块。统计访问量最多的前 10 个 IP:

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

这个命令拆开理解:count[$1]++用 IP 做数组下标累加,END块在所有行处理完后执行,把每个 IP 的计数打印出来,最后用sort -rn按数值倒序排列,head -10取前 10 名。整个链条在十几秒内就能统计完一个大访问日志文件,比用 Excel 打开半天实用太多。

如果只想统计有多少个不同 IP:

awk '{ips[$1]=1} END {print length(ips)}' /var/log/nginx/access.log

awk 的功能远不止这些,但对于日志排错来说,掌握字段提取、条件过滤、分组统计这三点就足够应付大多数场景了。

4. 日志轮转压缩后怎么查:zcat 一族和文件顺序

如果你只是看当前的最新日志,tail -F够了。但很多时候问题发生在昨天、前天,文件早就被 logrotate 轮转走了,甚至已经被 gzip 压缩。这时候直接tail压缩文件会得到一堆乱码,正确打开方式得换。

4.1 logrotate 会生成哪些文件

Linux 系统一般通过 logrotate 定期切割日志文件,避免单个文件无限膨胀。切割规则写在/etc/logrotate.conf和/etc/logrotate.d/里。

典型的切割结果可能是这样的:

app.log app.log.1 app.log.2.gz app.log.3.gz

越靠后的文件越旧。app.log是最新正在写的文件,app.log.1是上一轮的旧日志,app.log.2.gz是上上轮且已经压缩过的日志。

有些配置会生成带日期后缀的文件:

app.log app.log-2025-06-14.gz app.log-2025-06-13.gz

看到这种命名基本就明白文件的新旧关系了。另外 logrotate 配置里如果设了delaycompress,新切割出来的.1文件可能还是未压缩的文本,要等下一轮轮转才会被压缩。所以查看历史日志时,不要假设所有非当前文件都是.gz,最好先ls看一遍再动手。

4.2 压缩日志的查看命令

碰到.gz结尾的日志文件,不能直接cat或grep,需要先解压或使用支持压缩格式的专用命令。最常用的是这组:

zcat app.log.2.gz | head -50

zcat的行为等价于“解压并输出到标准输出”,所以后面可以接管道继续处理。

从压缩日志里搜索关键词:

zgrep -n "ERROR" /var/log/myapp/app.log.2.gz

zgrep和grep的参数基本通用,包括-A、-B、-i、-E,只是它内部会自动解压再匹配,使用体验和无压缩日志几乎一样。

分页查看压缩日志:

zless /var/log/myapp/app.log.2.gz

zless在查看超大的压缩日志时很实用,不用把整个文件解压到磁盘,而是边解压边加载。

如果你的日志压缩格式是.xz,对应的是xzcat、xgrep、xzless。如果是运维手工打包的.zip,可以用unzip -p直接把内容输出到管道:

unzip -p app.zip | grep ERROR

-p表示输出到标准输出而不是真的解压出文件,非常适合快速搜索打包日志里的关键内容。

4.3 跨文件提取某段时间日志的顺序问题

当问题跨越了轮转切割的时间点,比如想查 14:30 到 14:35 的日志,而这 5 分钟的日志一部分在app.log,一部分在app.log.1,你就需要按时间顺序把多个文件串起来一起查看。

如果文件都是普通文本未压缩:

cat /var/log/myapp/app.log.1 /var/log/myapp/app.log | grep "2025-06-15 14:3"

注意顺序是从旧到新:先读app.log.1再读app.log,这样输出顺序和真实时间顺序一致。

如果历史文件是 gzip 压缩的,需要把 zcat 和 cat 混用:

zcat /var/log/myapp/app.log.2.gz /var/log/myapp/app.log.3.gz cat /var/log/myapp/app.log.1 /var/log/myapp/app.log -

第二种写法是先用zcat把旧的压缩日志解压输出,再cat前面的普通文件和当前文件,管道最后的-表示从标准输入读取数据,也就是接住zcat输出的内容。这样把所有文件拼接成完整的时间流。这里有个提醒:如果你直接cat了一个.gz文件,终端会刷出乱码,千万不要慌,Ctrl+C停掉就好,日志文件本身不会被破坏。

更省事的方式是如果所有历史文件都是.gz,直接对多个文件使用zgrep:

zgrep -h "ERROR" /var/log/myapp/app.log.2.gz /var/log/myapp/app.log.3.gz

-h参数让 zgrep 不输出文件名前缀,输出干净地只有匹配行。不过它输出的顺序是按你给的参数顺序来的,所以依然要注意文件新旧排列。

5. 一次线上超时故障,我的完整日志排查流程

命令分开讲都会用,但真正能解决问题的还是组合使用。这里我把一次典型的线上故障排查过程完完整整列出来,你看看这些命令是怎么一环扣一环的。

5.1 故障现象与第一直觉

下午 2 点 10 分左右,业务方反馈用户下单超时,接口响应时间从平时的 200ms 飙到 5 秒以上,同时数据库连接池告警。服务器 CPU 看着不高,内存也正常,初步判断问题不在硬件资源上。

这次排查我执行的第一组命令是:

ls -lht /var/log/myapp/ | head -10 date

第一条看日志目录里哪些文件在更新,第二条确认服务器当前时间。为什么要先看时间?因为后续所有日志分析都要和当前时间做对比,如果服务器时间本身漂了,后面的排查就会跑偏。这步虽然简单,但很多人会漏掉。

结果发现app.log的最后修改时间停留在 5 分钟前,这说明应用可能已经写不进日志了,或者日志轮转出了问题,或者应用进程已经不响应了。这本身就是一个重要信号。

5.2 从应用日志到数据库慢查询的排查链路

继续看应用日志的尾部内容:

tail -n 100 /var/log/myapp/app.log

看到大量Thread pool exhausted和DB_TIMEOUT。线程池耗尽通常意味着请求被阻塞,而阻塞根源可能在下游数据库。

先统计一下DB_TIMEOUT出现的次数,并快速看它按秒的分布:

grep -c "DB_TIMEOUT" /var/log/myapp/app.log grep "DB_TIMEOUT" /var/log/myapp/app.log | awk '{print $2}' | cut -c1-8 | sort | uniq -c

第二个命令把时间段的分钟信息提取出来,统计每个分钟内超时次数。$2如果存的是HH:MM:SS时间字段,cut -c1-8截出时分秒,如果只看分钟级别,就截1-5。看到结果后就能判断超时是持续存在还是突然爆发。这次的结果是 14:00 之前完全没有,14:00 之后突然增加,说明问题是从某个时刻开始触发的。

随后提取一条超时记录附近的上下文,看它具体卡在哪个环节:

grep -B 5 -A 20 "DB_TIMEOUT" /var/log/myapp/app.log | less -N

从上下文里发现,每次超时前都有一个 SQL 查询记录了慢查询关键字SlowQuery,但应用日志里没打印 SQL 明细,于是转头去看 MySQL 慢查询日志。

查看慢查询日志尾部:

tail -n 200 /var/log/mysql/slow.log

再用 mysqldumpslow 工具做汇总排序:

mysqldumpslow -t 5 /var/log/mysql/slow.log

mysqldumpslow会把结构相似的 SQL 归类汇总,-t 5取耗时最长的前 5 类。输出中排名第一的 SQL 没有走索引,全表扫描行数超过了千万级。到这里,问题链路完全打通:某条 SQL 因为数据量增长失去了索引优化,查询变慢,数据库连接被长期占住,连接池耗尽,应用线程池也跟着阻塞,最终表现为下单接口超时。

5.3 这次排查里最关键的一个命令

这次排查中真正起到决定性作用的是最后这条:

mysqldumpslow -t 5 /var/log/mysql/slow.log

但更值得记住的是前一步提取上下文的 grep:

grep -B 5 -A 20 "DB_TIMEOUT" /var/log/myapp/app.log

因为它把应用层错误和数据库慢查询之间的因果关系串起来了。如果只看应用日志,只知道超时了;如果只查数据库慢查询,不一定知道是哪个时间点开始恶化的。上下文窗口让你看到请求从哪里来、SQL 走向哪里,这是日志排查里最核心的思维。

很多人遇到这种问题会从头开始把日志一句一句读,但正确的方式是:先tail看最新的关键报错,再grep统计异常频率,再grep -A/-B提取上下文,最后根据线索跳到下一个相关日志文件。这个过程不是靠某一个命令,而是靠一条清晰的分析链路。

6. 日志查看中的几个坑,以及我的日常小技巧

最后聊几个我在实际工作中踩过的坑和慢慢养成的小习惯。这些东西不会写在官方文档里,但对线上排查体验的影响非常大。

6.1 你 tail 不到内容,日志文件其实已经被轮转

有一次排查时,我开着tail -f /var/log/myapp/app.log等了好几分钟,日志一点动静都没有,但业务方明确说刚才还在报错。直觉告诉我不是没有日志,而是我看错了文件。

随后执行ls -lht /var/log/myapp/发现确实有一个app.log.1更新于几分钟前,原来 logrotate 刚把app.log重命名成了app.log.1,然后新建了一个空的app.log。而我之前一直tail -f的是旧文件描述符指向的已改名文件,新日志已经写到app.log.1里去了。

从那以后我直接用tail -F替代tail -f,-F会按照文件名重新打开文件,哪怕文件被轮转重建,也能自动跟上。这个习惯强烈建议你从现在开始养成。

6.2 日志时间不连续?先看时区

跨服排查时最坑的问题是时区不一致。有的应用日志用 UTC,有的用本地时间 CST,如果两台服务器的日志时间戳一个带+00:00一个不带,直接按时间范围拼接日志会得到错乱的顺序。

我现在的处理方式是:每次跨服务器排查前先执行date确认系统时间,再看日志第一行的时间格式,如果日志本身只有本地时间没有时区,就在心里做个换算。更稳妥的做法是在日志配置里强制使用 ISO 8601 格式,比如2025-06-15T14:30:00+08:00这种带时区偏移的格式,后续不管是人工看还是写脚本分析都不会再被时区问题干扰。

6.3 日志文件被进程占用但看不到内容

还有一个隐蔽场景:某个服务进程把日志文件打开了,但后来这个文件被运维手工删除或轮转清理了。你在文件系统里看不到这个文件,但进程依然持有旧文件的文件描述符,日志依旧在往那个“已删除但未释放”的文件里写。这会导致磁盘空间看起来没少,但实际空间被占用。

排查方式:

lsof | grep deleted

这条命令会列出所有被进程打开但已从文件系统删除的文件,后面跟文件大小就能判断是不是大文件在占空间。如果你确认某个日志文件对这个服务已经没用了,可以把对应进程重启一下释放句柄,磁盘空间才会真正回收。

这虽然不是严格意义上的“查看日志命令”,但排查日志写满磁盘、日志丢失这类问题时,经常要联合使用。

6.4 一些让效率翻倍的小习惯

第一,给 grep 加颜色。Linux 默认的 grep 在终端里匹配到的关键词没有高亮,如果日志行很长,找起来费眼。建议在~/.bashrc里加一行:

alias grep='grep --color=auto'

之后 grep 结果里匹配到的部分会高亮显示,扫日志的效率立刻提升。

第二,组合实时过滤。我经常用这条命令同时跟踪日志和过滤关键词:

tail -F /var/log/myapp/app.log | grep --line-buffered "ERROR"

注意一定要加--line-buffered。因为 grep 在管道模式下默认会做块缓冲,也就是输出不会立刻打印,等到攒够一批数据才输出,这样实时跟踪就名存实亡了。加上这个参数后 grep 每处理一行就输出一行,配合tail -F才能做到真实时。

第三,固定自己常用的复杂命令。比如我有个 shell 函数,专门用来同时跟踪多个日志文件并过滤关键词:

logtail() { tail -F "$1" | grep --line-buffered -E "$2" }

用的时候执行logtail /var/log/myapp/app.log "ERROR|WARN"就很方便。

第四,日志文件巨大且单行超长时,优先用less -S而不是直接 tail。less -S可以左右滚动查看超长行,想看具体某个字段就水平移动,日志的完整结构尽收眼底,不会因为一行几十 K 而在终端里疯狂换行,把屏幕刷得乱七八糟。

第五,grep 到二进制文件时,终端会显示Binary file matches而不是输出匹配内容。这种情况通常发生在日志文件里混入了非文本字节,可以加-a强制按文本处理:

grep -a "ERROR" /var/log/myapp/app.log

我用过几次这条命令处理断电后产生乱码的日志文件,效果立竿见影。

这些命令和习惯单独看都不复杂,但真正到线上故障时需要的是把它们熟练地串起来。日志文件没有噱头,无非是找位置、看实时、过滤关键词、处理压缩文件、跨文件合并。把这一套流程练熟之后,别人翻半天日志还没有头绪,你几分钟就能定位到问题根因。

返回列表