常见故障排查场景

排障的顺序比命令本身重要:先确认现象发生在哪一层,再逐层收窄。这一节按「你看到的现象」组织,每条给出第一步该看什么、判断依据是什么、接下来往哪个方向走。命令都可以直接粘贴。

共 31 条命令 · 最近更新

现象:磁盘满了 / 写不进去

典型症状是服务突然报 No space left on device、日志写不进去、数据库拒绝写入,但 df 看起来还有空间。先确认到底哪一层满了,再动手删。

df -h 看哪个挂载点满了

先看 Use% 和 Avail。注意区分两种情况:真的是某块盘满了;或者 inode 用尽——空间还有但创建不了新文件。

df -hT
df -i          # 看 inode 使用率,用尽时同样写不进去

容器里看到满,未必是容器的问题——挂载进来的宿主目录满了会直接体现在容器内。

逐层找出大目录

从根开始逐层下钻,每层只排前 15 名,很快能定位到具体目录。

du -h --max-depth=1 / 2>/dev/null | sort -rh | head -15
du -h --max-depth=1 /var 2>/dev/null | sort -rh | head -15
du -ah /var/log | sort -rh | head -20

du 遇到权限不足会静默跳过,加 2>/dev/null 让输出干净;必要时用 sudo du。

找出被删除但没释放的文件

需安装

进程还持有已删除文件的句柄时,df 显示满但 du 找不到对应目录。lsof 的 deleted 标记能抓出来,重启持有它的进程即可释放。

lsof +L1 2>/dev/null | sort -k7 -rn | head -20
lsof -nP 2>/dev/null | grep deleted | awk '{print $1, $2, $7, $9}' | sort -k3 -rn | head

截断正在写的日志(: > file)通常比 rm 更合适:句柄不失效,空间立即回收。

常见占空间的固定位置

依次检查这几个位置,绝大多数「盘满了」都能在这里找到答案。

journalctl --disk-usage
journalctl --vacuum-size=200M
du -sh /var/lib/docker 2>/dev/null
docker system df
du -sh /var/cache/apt /var/log 2>/dev/null
find / -xdev -type f -size +500M -printf '%s\t%p\n' 2>/dev/null | sort -rn | head

docker system prune 会删掉所有未使用镜像和卷,跑之前先确认没有停着待恢复的容器。

现象:服务起不来 / 反复重启

systemd 的报错很啰嗦,但定位路径是固定的:systemctl status 看退出码和最后几行日志,journalctl 看完整输出,再核对配置和端口。

systemctl status 看状态与退出码

重点看 Active 一行和下面日志的最后几行。failed 后面括号里的退出码很有信息量:203/EXEC 通常是路径或权限问题。

systemctl status nginx --no-pager -l
systemctl is-enabled nginx    # 是否开机自启
systemctl is-failed nginx

203/EXEC 表示可执行文件无法执行:检查 shebang、执行位、以及脚本里 CRLF 换行。

journalctl 看这个服务的完整日志

-u 只看某个单元,-n 限制行数,--since 限定时间,-p err 只看错误级别以上。

journalctl -u nginx -n 100 --no-pager
journalctl -u myapp --since '10 min ago' -p warning
journalctl -u myapp -f          # 实时跟踪
journalctl -b -1 -u myapp       # 上一次启动周期

用 -b -1 能回看「重启之前那次」的日志,排查开机就崩的服务非常有用。

先手动跑一遍可执行文件

从单元文件里读出 ExecStart 的命令行,手动执行一次,报错会直接打在终端上,比 journal 更直观。

systemctl cat myapp | grep -E '^ExecStart'
systemctl show myapp -p ExecStart --no-pager
sudo -u www-data /usr/bin/myapp --config /etc/myapp.conf   # 换对应用户手动跑

手动跑能跑通、systemd 跑不通,九成是环境变量、工作目录或用户不同。

核对配置语法

改完配置没 reload 或语法写错,是「重启后起不来」的头号原因。

nginx -t
sshd -t
systemd-analyze verify /etc/systemd/system/myapp.service

nginx -t 只校验默认路径的配置;用了自定义 -c 时要带上同样的参数。

最近改过什么

按时间列出最近变动的配置和包,能迅速把问题和某次操作对上。

find /etc -type f -mmin -60 -ls 2>/dev/null
rpm -qa --last | head -20            # RHEL 系
grep -E ' (install|upgrade) ' /var/log/dpkg.log | tail -20   # Debian 系

「昨天还好好的」几乎总能在这里找到那个被改动的文件。

现象:端口被占用 / 连不上

先把「本机能不能连」和「外面能不能连」分开,再看是没人监听、监听到了错误地址、还是中间被拦。

ss 看监听与占用

-l 只看监听,-t TCP,-u UDP,-n 不做域名解析,-p 显示进程。要看谁占了某个端口,直接用 grep 过滤。

ss -ltnp
ss -ltnp | grep ':8080'
ss -lunp
ss -s          # 汇总连接数

看到 127.0.0.1:8080 说明只监听本机,外部当然连不上——这是最常见的「端口没通」。

判断是没人监听还是被防火墙拦

先在本机 curl 一次:连不上说明服务本身有问题;本机能连但外面不能连,再看监听地址和防火墙。

curl -sS -m 3 -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
curl -sS -m 3 -o /dev/null -w '%{http_code}\n' http://192.168.1.10:8080/
nc -vz 192.168.1.10 8080

连接被拒绝(Connection refused)说明路由可达但没人监听;超时才是防火墙或安全组在丢包。

防火墙规则

firewalld、ufw、nftables/iptables 并存时容易互相看不到,逐个确认。

sudo firewall-cmd --list-all          # firewalld
sudo ufw status verbose               # ufw
sudo nft list ruleset | head -40      # nftables
sudo iptables -S | head -40           # iptables

云主机还要单独看安全组——本机防火墙全开也可能被云平台挡在外面。

查看端口属于哪个进程

需安装

拿到 PID 后可以直接看它的命令行、工作目录和打开的文件。

sudo ss -ltnp | grep ':8080'
sudo lsof -nP -iTCP:8080 -sTCP:LISTEN
sudo ls -l /proc/<PID>/cwd
sudo tr '\0' ' ' < /proc/<PID>/cmdline; echo

lsof 报的 PID 是命名空间视角的;在容器场景要在宿主机上找 containerd/docker 进程。

现象:CPU / 内存 / IO 异常高

先分清是「真的忙」还是「在等」。CPU 高看用户态还是内核态,内存高看是不是缓存,IO 高看是磁盘慢还是程序在狂写。

uptime 与负载

三个数字分别是 1、5、15 分钟的平均负载。负载高于 CPU 核数说明有排队。

uptime
nproc
cat /proc/loadavg

负载包含等 IO 的进程。磁盘慢的时候负载会很高但 CPU 空闲,别急着加 CPU。

top 交互式定位

进 top 后按 P 按 CPU 排序、M 按内存排序、1 展开每核、c 看完整命令行。

top -o %CPU
top -b -n1 | head -20        # 一次性输出,适合脚本
ps -eo pid,ppid,user,pcpu,pmem,etime,comm --sort=-pcpu | head -15

ps 的 %CPU 是进程整个生命周期的平均值,排查「刚刚飙高」要用 top 或 pidstat。

内存到底被什么占了

free 的 available 才是可用内存,buff/cache 是可以回收的。真正要警惕的是 swap 被大量使用。

free -h
cat /proc/meminfo | head -5
smem -rs rss 2>/dev/null | head       # 按实际占用排序
ps -eo pid,user,rss,pmem,comm --sort=-rss | head -15

OOM 杀进程的记录在 journalctl -k | grep -i oom,或 dmesg | grep -i 'killed process'。

IO 与磁盘延迟

需安装

iostat 的 %util 接近 100 说明磁盘饱和;await 是平均等待毫秒数,明显偏高就是盘慢。

iostat -xz 1 5
vmstat 1 5
sudo iotop -oPa 2>/dev/null | head -20
cat /proc/pressure/io

云盘的突发配额耗尽时会出现「平时很快、突然很慢」,用 fio 或 dd 复测才能确认。

看某个进程具体在做什么

需安装

strace 跟踪系统调用,perf 采样热点函数,是「CPU 高但看不出为什么」时的下一步。

sudo strace -f -p <PID> -T -tt -e trace=network 2>&1 | head -50
sudo perf top -p <PID>
sudo cat /proc/<PID>/stack

strace 会显著拖慢被跟踪进程,生产环境只在必要时短时间挂上去。

现象:Permission denied

Linux 的权限判断分好几段:文件模式位、目录的穿越权限、SELinux/AppArmor、以及挂载选项。报错一样,原因完全不同,按顺序排除。

先看这个路径的每一层权限

读文件需要路径上每个目录都有 x 位;只给文件 r 而父目录没有 x,照样 Permission denied。

namei -l /var/www/app/config.yml
ls -ld /var /var/www /var/www/app /var/www/app/config.yml
id && id -Gn

namei -l 会把路径上每一段的属主和模式位列成一行,是排查这类问题最快的手段。

确认进程实际以哪个用户运行

服务不一定以你登录的用户跑。看进程的 real/effective uid 和所属组,再回头核对文件权限。

ps -o user,group,pid,comm -p <PID>
sudo cat /proc/<PID>/status | grep -E '^(Uid|Gid|Groups)'
systemctl show myapp -p User -p Group --no-pager

改了文件属组却没重启服务,进程持有的仍是旧组——这类「改了没用」要先重启再判断。

SELinux / AppArmor 拦截

权限看起来都对却仍然拒绝,就要怀疑强制访问控制。

getenforce
sudo ausearch -m avc -ts recent | tail -30
sudo aa-status 2>/dev/null | head
dmesg | grep -iE 'denied|avc' | tail -20

临时用 setenforce 0 验证可以,但别把它当成解决方案;正确做法是修 file context 或加策略。

挂载选项与只读文件系统

noexec 会让可执行文件报 Permission denied;只读挂载会让一切写入失败。

mount | grep -E ' /(home|tmp|opt|var) '
findmnt -no OPTIONS /tmp
findmnt -no OPTIONS /

/tmp 被挂成 noexec 时,脚本里解压出来直接运行就会失败——先 chmod +x 到别的目录或挂载点。

现象:SSH 连不上 / 登录异常

按「网络 → 端口 → 认证 → PAM/Shell」的顺序走,每一步都能用一条命令证伪。

客户端看详细握手过程

-vvv 会打印协商、认证、通道建立的每一步,卡在哪一行就是哪一层的问题。

ssh -vvv user@host 2>&1 | tail -40
ssh -o ConnectTimeout=5 -o BatchMode=yes user@host true; echo "退出码: $?"
nc -vz host 22

卡在 kex 是网络或服务问题;到了 Authentication 才失败就是密钥、口令或 PAM 的问题。

服务端看 sshd 日志

认证失败的精确原因(密钥不匹配、用户被锁、超出 MaxStartups)都在这里。

journalctl -u ssh -n 80 --no-pager
journalctl -u sshd -n 80 --no-pager
sudo tail -50 /var/log/auth.log     # Debian 系
sudo tail -50 /var/log/secure       # RHEL 系

服务名在 Debian 是 ssh、RHEL 是 sshd,journalctl -u 写错会得到空输出,很误导。

密钥权限与 known_hosts

私钥权限过宽会被 ssh 直接拒绝;换了主机密钥会有刺眼的告警。

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/authorized_keys
ssh-keygen -R host
ssh-keyscan -t ed25519 host >> ~/.ssh/known_hosts

StrictHostKeyChecking=no 只适合临时排查,长期用会失去中间人攻击防护。

登录后立刻断开或卡住

能认证成功却拿不到 shell,通常是登录 shell、家目录权限或 PAM 的问题。

ssh -t user@host /bin/bash --noprofile --norc   # 绕过启动文件
ls -ld /home/user && ls -la /home/user/.ssh
grep -E '^\S+:.*(false|nologin)' /etc/passwd

.bashrc 里的交互式命令(比如等待输入)会让非交互登录一起卡住,排查时先用 --noprofile --norc。

日志在哪、怎么高效查

先知道东西写在哪,再谈过滤。现代发行版里 journald 和传统日志文件是两套并存的体系。

日志的常见位置

按来源分:内核在 dmesg,服务在 journald,站点应用多数写自己的文件。

/var/log/syslog           # Debian 系总日志
/var/log/messages         # RHEL 系总日志
/var/log/auth.log         # 认证(RHEL 为 secure)
/var/log/kern.log
/var/log/nginx/{access,error}.log
/var/log/apt/  /var/log/dpkg.log
dmesg -T --level=err,warn | tail -30
journalctl -b -p err --no-pager | tail -50

容器里 /var/log 往往是空的,日志走 docker logs / kubectl logs,而 journald 在宿主机上。

按时间窗口取日志

--since 和 --until 支持相对时间和绝对时间,两个配合可以把范围压到几分钟。

journalctl --since '2026-10-19 15:00' --until '2026-10-19 15:30' --no-pager
journalctl --since '30 min ago' -u nginx -p warning
grep '19/Oct/2026:15:2' /var/log/nginx/access.log | tail

时间不写时区时按本机时区解释;跨时区排查要显式带上偏移,比如 2026-10-19T07:00:00Z。

从后往前找报错

日志通常很长,先 tail 看尾部,再 grep 收窄关键词,最后用上下文确认。

tail -n 200 /var/log/app.log
grep -niE 'error|exception|timeout|refused' /var/log/app.log | tail -40
grep -n -B3 -A15 'Traceback' /var/log/app.log | tail -60

-B / -A 给出前后文,比单看错误行更容易判断真实原因。

实时跟踪与合并多文件

tail -F 能扛住日志轮转,-f 在多文件会加文件名前缀,排查集群时很方便。

tail -F /var/log/app.log
tail -f /var/log/app/app-*.log
journalctl -f -u nginx -u php-fpm

-F(大写)等于 --follow=name --retry,文件被 logrotate 换掉后仍能继续跟,-f 会跟丢。

统计而不是逐行读

先看分布,再决定要不要读细节。一分钟的错误分布往往直接指向问题。

awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
grep -c ' 500 ' /var/log/nginx/access.log
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

先统计再取样:先知道哪类错误最多、集中在哪个时间段,再 tail 那一段原文。