这个系列写到第十四篇,我发现一个明显的变化:找我帮忙看服务器问题的朋友,问的往往不再是“这个命令怎么用”,而是“系统突然变慢了,我该先看什么”“磁盘明明还有空间,怎么还报错”“端口被占用了,到底是谁干的”。这些问题背后其实都指向同一点——Linux常用命令单看哪一条都不难,难的是在真实故障现场,把合适的命令按正确的顺序串起来,形成一条完整的证据链。
所以第十四篇不打算再罗列新命令,我想换一种方式,用几个高频故障场景把命令串起来讲:系统卡顿、端口冲突、日志分析、磁盘误判、定时任务。这些都是运维日常里最常踩的坑,也是面试里最常被问到的场景题。如果你能把这篇里的排查思路吃透,再遇到“Linux系统故障案例”类的实际问题,至少不会手足无措。
1. 系统卡顿先别重启:负载、CPU排队与IO瓶颈的快速定位
服务器变慢,很多人的第一反应是重启,或者直接 top 看一眼就懵了。重启确实能解决一部分问题,但如果你不知道根因,重启之后过几天又会再犯。更合理的做法是先用几条命令,在几十秒内判断出系统到底“卡”在哪里。
1.1 uptime 的三段负载到底在说什么
先说最容易被误读的命令:uptime。
输出大概是这样的:
code复制 14:23:45 up 30 days, 2:15, 2 users, load average: 0.35, 0.62, 0.78
很多人看到 load average 后面的三个数字就以为这是 CPU 使用率,实际上不是。它表示的是最近 1 分钟、5 分钟、15 分钟内,系统处于“可运行状态”的进程平均数,也就是有多少个进程在等着被 CPU 调度。
判断负载要结合 CPU 核数来看。单核 CPU 的 load 如果是 1.0,相当于 CPU 一直满负荷运转;如果是 4 核 CPU,load 到 4.0 才算满载。所以在看 uptime 之前,我习惯先执行 nproc 确认核心数。
我自己的判断口诀是:1 分钟负载超过核数,说明当前有突发压力,比如正在编译、备份任务;15 分钟负载也持续超过核数,那就不是突发,是系统长期处于过载状态,必须查到底是哪个进程在消耗资源。
1.2 vmstat:两秒钟看出系统在忙什么
uptime 只告诉你系统“累不累”,vmstat 能告诉你它“累在哪”。我常用的格式是:
code复制vmstat 2 5
意思是每隔 2 秒采样一次,一共采样 5 次。重点关注这几列:
- r:等待 CPU 调度的进程数。如果这个值长期大于 CPU 核数,说明 CPU 不够用了,进程在排队。
- b:不可中断睡眠的进程数,通常跟 IO 等待有关。
- us、sy:用户态和内核态 CPU 占用百分比。sy 长期偏高,有可能是系统调用太频繁。
- wa:CPU 等待 IO 完成的时间占比。这个值超过 30% 就要警惕,属于典型的 IO 瓶颈。
- si、so:从交换分区换入换出的内存量。这两个值持续不为 0,说明物理内存吃紧,系统开始用磁盘假装内存,性能会急剧下降。
有一次线上数据库出现慢查询,我登上去第一件事就是跑 vmstat,wa 显示 60 多,紧接着查磁盘状态,发现一块日志盘的 IO 已经跑满。如果只看 top 里 CPU 忙不忙,可能根本发现不了问题根源。
1.3 top 的批处理模式与交互排序
top 大家都用过,但很多人只是看一眼就退出。我通常会在 top 界面里做三个操作:
- 按 P:按 CPU 使用率排序,看哪个进程在烧 CPU。
- 按 M:按内存占用排序,找内存大户。
- 按 T:按累计 CPU 时间排序,这个容易被忽略,它反映的是进程长时间以来的总消耗,而不是瞬间值。
如果是在脚本里抓取进程状态,可以用批处理模式:
code复制top -bn1 | head -20
这里的 b 是批处理模式,n1 是只输出一次。注意 top 里的 %CPU 是瞬时采样值,多核环境下进程的 CPU 占用可以超过 100%,比如一个 Java 进程开了 8 个线程同时跑满,那 %CPU 可能显示 700% 以上,这是正常的,不是 bug。
有一次我在排查一个服务响应变慢的问题,top 里看到一个 Python 脚本的 CPU 占用到了 400%,顺着 PID 查下去才发现是某个定时任务里的死循环。这种问题如果不看 top,真的很难在几百个进程里一眼发现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从端口冲突到僵尸进程:顺着 PID 交叉验证真凶
top 和 vmstat 能告诉你“谁在消耗资源”,但很多实际故障是“某个进程想启动,却发现端口被占”“文件被某个进程锁住删不掉”。这时候就需要把进程、端口、文件这三个维度串起来查。
2.1 ps 的老实用法:别只记得 ps aux
ps aux 是入门级命令,但遇到需要理清进程父子关系、定位僵尸进程的场景,我更推荐用:
code复制ps -ef
ps -ef 输出里的 PPID 是父进程 PID,这在追查“谁启动了谁”时非常有用。ps aux 里的 STAT 列也同样关键:
- R:正在运行,占着 CPU。
- S:可中断睡眠,正常等待。
- D:不可中断睡眠,通常卡在 IO 上,比如磁盘有问题时常见。
- Z:僵尸进程,子进程已经退出,但父进程没有回收它。
僵尸进程的问题很典型:你 ps 能看到一堆 defunct 进程,kill 也杀不掉,因为从内核角度看它们已经“死”了,只是父进程没调用 wait 回收。处理方式通常是找到它的父进程,如果是父进程代码有 bug,就得修代码;临时缓解可以 kill 父进程,让 init 进程接管并回收。
我习惯配合排序找内存大户:
code复制ps aux --sort=-%mem | head -10
这个命令按内存占用降序排列,前 10 行就是最吃内存的进程。排查 OOM 相关问题的时候,这几乎是必查的第一条命令。
2.2 lsof 一把梭:端口被谁占了、文件被谁打开了
“端口被占用”可能是运维里出现频率最高的报错。Nginx 启动失败,提示 bind() to 0.0.0.0:8080 failed,第一反应就是用:
code复制lsof -i:8080
这条命令直接告诉你哪个进程占用了 8080 端口。输出里的 COMMAND 和 PID 就是你要找的“凶手”。如果只想看监听的端口,可以再加 -sTCP:LISTEN 过滤。
lsof 还有个非常实用的用法是按进程查文件:
code复制lsof -p 1234
查看 PID 为 1234 的进程打开了哪些文件。这在判断“某个进程为什么占用磁盘空间”“某个日志文件是否还在被写入”时特别好用。
还有一个用法是查目录下被打开的文件:
code复制lsof +D /var/log
这个命令会列出 /var/log 目录下所有被进程打开的文件。如果你想知道哪些日志文件正在被写,看这个比一个个猜靠谱得多。
2.3 ss 替代 netstat:连接状态一眼看清
老教程里都是 netstat -tunlp,但现在我更推荐用 ss,它更快,输出也更清爽。查看监听端口和进程:
code复制ss -tunlp
t 是 TCP,u 是 UDP,n 用数字显示端口,l 只看监听状态,p 显示进程信息。
如果想快速看一眼当前系统的连接状态汇总:
code复制ss -s
这个命令会统计出有多少 ESTABLISHED、TIME_WAIT、LISTEN 状态的连接。很多人看到大量 TIME_WAIT 就紧张,其实这是 TCP 正常关闭流程的一部分,大量短连接请求后出现几十万个 TIME_WAIT 并不罕见,只要不是持续堆积导致端口耗尽,通常不用过度干预。
到了这一步,我们基本已经把“哪个进程占着哪个端口、这个进程开了哪些文件”理清了。接下来就该看日志了。
3. 日志不会说谎:从 messages 到 dmesg 再到 journalctl 的排查链路
进程状态是“现状”,日志是“历史”。很多故障如果不看日志,你永远只能靠猜。日志这东西,不会说谎,只是很多人不知道去哪个日志里找。
3.1 tail 与 grep 组合:排查现场的第一条命令
实时跟踪日志,最经典的就是:
code复制tail -f /var/log/messages
Ctrl+C 退出。如果是追查某类错误,我会先看最后多少行,再配合 grep 过滤:
code复制tail -n 1000 /var/log/messages | grep -E "error|failed|exception"
grep -E 是扩展正则,一次匹配多个关键词。加上 --color 参数可以让匹配高亮显示,信息密度很高。
这里有个实战提醒:在线上环境千万别直接对几个 GB 的日志文件执行 grep。我见过同事在 /var/log/ 下一条 grep -r 下去,整个机器 IO 直接被打满的。正确做法是先 ls -lh 看看文件大小,用 tail 或者按时间窗口截取之后再去过滤;日志文件如果已经按日期轮转成了 .gz,那就用 zgrep 而不是先解压再 grep。
3.2 dmesg:内核在向你喊话
很多疑难杂症最终答案都藏在 dmesg 里。它是内核环缓冲区消息,记录的是硬件、驱动、内存管理这些底层事件。执行时建议加时间戳:
code复制dmesg -T
-T 参数把时间转换成人类可读格式,不然那一串 epoch 秒数看着头大。
我最常关注的几类关键输出:
- Out of memory: Kill process:内核触发 OOM killer 杀掉了进程。如果 MySQL、Java 应用突然挂掉,先查 dmesg 里有没有这几行。
- EXT4-fs error、I/O error:文件系统或磁盘硬件出问题。
- soft lockup、hard lockup:CPU 长时间没有响应,常见于内核 bug 或极端负载。
有一次半夜接到 MySQL 告警,进程直接消失了。当时第一反应是看 MySQL 错误日志,没发现明显异常,最后是 dmesg 里的 OOM 记录揭开了真相:一个内存吃紧的 Java 服务把系统内存占满了,内核选择了 MySQL 这个内存大户下手。这就是典型的“进程看起来没问题,但系统觉得你有问题”。
3.3 journalctl:systemd 时代的日志管理
现代主流发行版都基于 systemd,日志查看就不只是看文件了。journalctl 我常用的几个组合:
code复制journalctl -u nginx --since "1 hour ago"
journalctl -f
journalctl -p err
-u 指定服务单元,只看某个服务的日志;--since 限定时间窗口;-f 是跟踪模式,相当于 tail -f;-p err 只看 err 及以上级别的日志,过滤掉大量噪音。
这里有一个坑:journald 的日志默认存在 /var/log/journal/,如果不做限制,某些服务日志量很大,可能把 /var 分区吃满。需要在 /etc/systemd/journald.conf 里设置 SystemMaxUse,比如限制成 500M,然后重启 systemd-journald 生效。
老派一点的排查路径是看 /var/log/messages、/var/log/secure 这些 rsyslog 落盘文件。如果系统里没有 journalctl 命令,说明是纯 SysVinit 的老系统,回到 messages 和 secure 文件就好。
4. 磁盘明明没满却报错:inode 耗尽与已删除文件的迷局
磁盘相关的问题,是故障案例里最容易被表面现象骗到的。最常见的一句话是:“我 df 看了,还有好几十 GB 空间,为什么应用还是报磁盘写不进去?”这时候十有八九是 inode 耗尽,或者文件系统本身出了问题。
4.1 df -h 与 df -i 必须一起看
df -h 大家都知道,查看文件系统空间使用。但很多人不知道 df -i 看的是 inode 使用率:
code复制df -i
inode 可以简单理解成文件的“档案编号”。每个文件或目录都要占一个 inode,所以即使磁盘空间没满,如果 inode 被用光了,系统一样无法创建新文件。这种情况最容易出现在缓存目录、临时目录、邮件队列这类海量小文件堆积的场景。
判断是不是 inode 问题,很简单:
code复制df -h
df -i
两个命令一对比就清楚了。如果 df -i 的 Use% 接近 100%,而 df -h 还有很多空间,那恭喜你,找到根因了。
有个真实案例:某天 CI 构建机突然报无法创建临时文件,df -h 还剩 200G,排查半天发现是 /tmp 下堆积了上百万个小临时文件,inode 使用率到了 99%。清理完之后一切恢复正常。
4.2 du 定位空间大户:一层层剥洋葱
确认是空间不足的问题后,下一步就是找什么文件占了空间。我的习惯是从根目录开始一层层往下:
code复制du -sh /* --exclude=/proc --exclude=/sys --exclude=/dev 2>/dev/null | sort -h
这条命令列出根目录下每个一级目录的总大小,按从小到大排序,末尾的就是空间大户。然后继续深入:
code复制du -sh /var/* | sort -h
一层层往下剥,直到锁定具体是哪个目录。
如果是找单个大文件,用 find:
code复制find / -xdev -type f -size +1G -exec ls -lh {} \; 2>/dev/null
-xdev 参数很关键,它让 find 不跨文件系统查找,避免去扫描挂载的存储卷。结合 -size +1G,可以快速找出超过 1G 的大文件。
这里最容易漏掉的是两类:应用日志和 core dump 文件。日志文件如果没有配轮转策略,能轻松长到几十 GB;而进程崩溃产生的 core 文件,有时候一个就好几个 G,还散落在工作目录里。
4.3 已删除文件仍占空间:经典误判现场
有一种特别隐蔽的情况:你用 du 统计,明明某个目录里已经没有这个大文件了,但 df 显示磁盘还是满的。这时候十有八九是有进程在写一个已经被删除的文件。
Linux 的文件机制是:只要有进程还在打开这个文件的句柄,即使文件从目录里被删除了,磁盘空间也不会释放,直到进程关闭句柄或重启。
排查命令是:
code复制lsof +L1 | grep deleted
+L1 表示列出所有链接数小于 1 的文件,grep deleted 直接匹配文件状态。输出了 PID 之后,你去重启对应的进程,df 的空间才会真正释放出来。
我之前帮人处理过一个案例:某个服务每天写日志,运维同学图省事直接 rm 了日志文件,结果 df 一直显示 100%。问了半天才知道,是服务进程还在运行,句柄没释放。正确做法不是 rm,而是用 logrotate 轮转日志,让进程重新打开新文件。
5. 别让重复排查消耗精力:crontab、at 与 xargs 的自动化组合
排查完问题之后,很多操作其实是周期性的:每天清一次临时文件、每周压缩一次日志、凌晨跑一次备份脚本。手动重复做这些事情,既枯燥又容易出错,这时候就需要把常用命令组合成自动任务。
5.1 crontab 的路径与环境变量坑
定时任务的首选是 crontab。编辑任务:
code复制crontab -e
格式是五段式:分 时 日 月 周。比如每天凌晨 2 点执行备份脚本:
code复制0 2 * * * /usr/local/bin/backup.sh
查看当前用户的所有定时任务:
code复制crontab -l
这个命令我每天上班基本都会跑一遍,确认当天的计划任务都正常排队。
crontab 最大的坑是环境变量。cron 执行脚本时的 PATH 非常精简,和你在终端里手动执行时的环境完全不一样。我踩过最典型的一次:手动执行备份脚本一切正常,加了 crontab 之后疯狂报错,日志里全是 command not found。查了半天,原因是脚本内部用了相对路径的 mysqldump,而 cron 环境里找不到这个命令。
解决办法有两个:要么脚本内全部使用绝对路径,要么在脚本开头加 source /etc/profile,先把基础环境加载进来。还有就是记得把脚本输出重定向到日志文件,不然任务出错时你什么都不知道:
code复制0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
5.2 at 一次性任务:只在今晚跑一次
crontab 适合周期性任务,但有些维护操作你只想执行一次,比如今晚凌晨 3 点重启某个服务。这时候用 at 比 crontab 更合适:
code复制echo "/usr/local/bin/restart.sh" | at 03:00
查看当前排队的任务:
code复制atq
删除某个排队任务:
code复制atrm 任务编号
atq 输出的第一列就是任务编号。需要注意的是 at 依赖 atd 服务,如果执行后提示无法提交,先确认 atd 是否在运行。
5.3 xargs 批量操作的边界
定时脚本里经常需要批量处理,xargs 是个好帮手。它可以把前一条命令的输出,作为参数传给后面的命令。经典用法是删除 7 天前的日志文件:
code复制find /var/log/ -name "*.log" -mtime +7 -print0 | xargs -0 rm -f
注意这里用了 -print0 和 -0 配对,目的是处理文件名带空格的情况。如果不加 -0,文件名里一旦有空格,就会被 xargs 当成多个参数,可能误删文件。
xargs 还有个实用场景是批量查看服务状态:
code复制systemctl list-units --type=service --state=running | awk '{print $1}' | xargs -I{} systemctl status {} --no-pager
-I{} 的作用是把前一个命令的输出逐行替换到后面的 {} 位置,适合需要把参数插入到命令中部的场景。
不过 xargs 也不是万能的。批量操作时如果任务量大,比如一次性删除几十万个文件,建议先 xargs 配合 find 少量测试,确认无误后再正式执行,避免误操作毁掉数据。
我个人在实际操作中的体会是:排查故障,最重要的不是你背了多少条命令,而是你有没有一个清晰的排查顺序。先看负载判断系统忙不忙,再用 vmstat 定位瓶颈类型,顺 PID 查进程、端口、文件,最后用日志坐实根因,磁盘问题就 df、du、lsof 三连。这套链路本身就是“Linux常用命令”最值钱的地方。下一篇我打算专门聊聊进程间通信这个面试高频方向,如果你们有更想看的主题,评论区告诉我。
