5个经典Linux深度问题:表面一个样,根因各不同
做运维这些年,最怕的不是报错,是"报错看着眼熟、按老经验一通操作、结果越搞越砸"。下面这 5 个问题,新手和老手都踩过,但真能讲清机制的没几个。每一个我都附了真实场景、排查命令、根因、止血和治本,以及怎么提前避开。
一、文件删了,磁盘还是满的:进程还攥着句柄
场景
有天晚上磁盘告警,/ 分区用到 95%。我 df -h / 一看用了 90G,可 du -sh /* 把所有目录加起来才 50G 出头。差的 40G 凭空消失了?
# df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/sda2 100G 90G 10G 90%
# du -shx /* 2>/dev/null
24G /data
12G /var
8G /usr
... 全加起来不到 50G
df 和 du 对不上,这就是第一信号:有"看不见的文件"还占着空间。
排查
df 读的是文件系统超级块里的"已用块计数",实时更新;du 是顺着目录树一个个文件统计。两边不一致,基本就是"文件被删了,但还有进程开着它"。一句话定位:
lsof +L1 | head
# 或
lsof | grep deleted
+L1 意思是"link count 小于 1 的打开文件"——也就是目录项已经没了、但 fd 还开着的。果然抓到一个 java 进程:
java 18234 root 15w REG 8,2 38765432100 /data/app/access.log (deleted)
一个 38G 的 access.log,早被 rm 了,但 java 还写着,空间一天没还。
根因(机制)
删除文件在 Linux 里只是 unlink:把目录项拿掉,inode 的硬链接数减 1。文件系统只在「硬链接数=0 且 打开计数=0」时才回收数据块。进程还开着 fd,打开计数 > 0,块就不还。df 按超级块记的已用块走(实时),du 顺着目录走(目录项没了,它数不到),所以 df 比 du 大。
解决
-
- 止血(不重启):找到 fd 号,直接截断,空间立刻释放,进程继续往新位置写:
```
# 上面 lsof 输出里 15w 的 15 就是 fd 号
/proc/18234/fd/15
``
或者用truncate -s 0 /proc/18234/fd/15`。这一下 38G 就回来了。
-
- 治本:别对正在被写的日志直接
rm。用 logrotate 配 copytruncate(先拷再清空原文件,不需要 reload),或者让应用响应信号重新开文件(nginx -s reopen、Java 的 Log4j2 滚动)。 -
避坑
-
- 删大文件前先
lsof +L1 确认没人开着; -
- 监控用
df 不要只盯 du; -
- 日志滚动一定要走应用能感知的方式,纯
rm 是给自己埋雷。 -
二、df 还有 200G,cp 却报 No space left on device:inode 耗尽了
场景
往 /data 拷个文件,直接炸:
$ cp app.tar /data/
cp: cannot create regular file '/data/app.tar': No space left on device
$ df -h /data
Filesystem Size Used Avail Use% Mounted on
/dev/sdb1 500G 300G 200G 60% # 明明还有 200G!
空间够,却写不进。新手第一反应是把磁盘扩了——纯浪费钱。
排查
df -i /data
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sdb1 3276800 3276800 0 100% # inode 满了
df -i 一打,inode 100% 用完。块还有的是,但 inode 一个不剩,新文件建不出来。
根因(机制)
ext4/xfs 在 mkfs 时按磁盘大小和 inode ratio(ext4 默认每 16KB 一个 inode)预先分配固定数量的 inode。每个文件——哪怕内容只有 1 字节——都要吃掉一个 inode,但只占极少的 block。所以"海量小文件"能把 inode 池抽干,而磁盘块还空着一大半。我见过最典型的是 /var/lib/php/session/ 没人清理、邮件 spool、日志切割后残留的碎片文件。
解决
-
- 定位谁占的 inode:
for d in /data/*/; do
echo "$(find "$d" -xdev -type f 2>/dev/null | wc -l) $d"
done | sort -rn | head
或者按一级目录聚合:
find /data -xdev -type f 2>/dev/null | awk -F/ '{print $2}' | sort | uniq -c | sort -rn | head -
- 止血:把确认无用的海量小文件批量删掉,inode 立刻回笼。
-
- 治本:inode 数量是
mkfs 时定死的,扩容磁盘不会加 inode。要么重建分区时调小 inode ratio(mkfs.ext4 -i 8192 每 8KB 一个,小文件多就调密),要么——更现实——小文件别堆本地磁盘:session 放 Redis,附件放对象存储(FastDFS/MinIO),日志切割按大小并且删旧。 -
避坑
-
- 监控要加一条
df -i,光看 df -h 发现不了; -
- session、缓存、临时小文件必须有过期清理(tmpwatch/tmpreaper、PHP gc);
-
- 存储大量小文件的场景,分区时就把 inode ratio 算进去。
-
三、TIME_WAIT 几万个,连接建不上:你乱调内核参数反而更惨
场景
一个高并发短连接服务,每秒新建几千连接。跑着跑着客户端开始连不上,ss -ant | grep -c TIME_WAIT 一数几万。新手搜一圈,照着帖子把 net.ipv4.tcp_tw_recycle=1 开了——然后 NAT/容器后面的客户连接随机大面积超时,比之前还糟。
根因(机制)
TIME_WAIT 是主动关闭方进入的状态,固定持续 2MSL(Linux 默认 60 秒)。它不是 bug,是 TCP 正确性的两道保险:
-
- 保证最后那个 ACK 可靠到达——万一对方 FIN 重传,你能响应,连接干净关闭;
-
- 让网络里滞留的旧报文段在新连接建立前彻底消亡,避免串数据。
-
所以 TIME_WAIT 多,本质是"你主动关连接太频繁",比如 PHP 每次请求 new 一个 MySQL/Redis 连接不复用、HTTP 客户端不用连接池。
至于 tcp_tw_recycle 为什么是坑:它靠 PAWS(时间戳防回绕)快速回收 TIME_WAIT,但 NAT 后面所有客户端共用一个公网 IP、各自 timestamp 乱序,服务端一算对不上直接丢连接。4.12 之后内核干脆把它删了。开着它,客户随机掉线查半年查不到。
解决
-
- 治本(唯一正解):连接复用。MySQL/Redis 用连接池、HTTP 用 keep-alive、PHP 用 pconnect。把短连接变长连接,TIME_WAIT 从源头减少。
-
- 客户端安全参数:
net.ipv4.tcp_tw_reuse=1——只对出向连接复用 TIME_WAIT 槽,安全,不影响入向。 -
- 端口不够用:扩
net.ipv4.ip_local_port_range="1024 65535",让客户端有更多本地端口可分配。 -
- 别动:
tcp_tw_recycle(已移除且危险)、tcp_max_tw_buckets 调大只是延缓症状。 -
避坑
-
- 永远别在生产碰
tcp_tw_recycle; -
tcp_tw_reuse=1 只对 client 安全,服务端别指望它;-
- 真问题是连接模式,调参只是给破车换轮胎;
-
- 监控 TIME_WAIT 数量趋势,暴涨就去看是不是谁在狂建短连接。
-
四、进程 kill -9 都杀不死,系统假死:D 状态卡在 I/O
场景
一台机器突然"假死":SSH 能连,但命令卡住半天不出。top 里有个进程 STAT 列显示 D,kill -9 发过去毫无反应,它还在那。
根因(机制)
D = uninterruptible sleep(不可中断睡眠,TASK_UNINTERRUPTIBLE)。进程卡在内核态等一个不能中断的 I/O:通常是块设备(磁盘坏道)、或者网络文件系统 NFS 的服务端没响应。信号(包括 SIGKILL)只在进程回到可运行/可中断态才被处理,D 态期间内核不理你,所以 kill -9 无效。常见触发:NFS 服务端 hang、网络存储超时配置不当、RAID 卡卡住、底层存储抽风。
排查
# 看进程卡在哪个内核函数
cat /proc/<pid>/stack
# 开 sysrq 后打印所有被阻塞的任务栈
sysctl kernel.sysrq=1
echo w > /proc/sysrq-trigger
dmesg | grep -A20 "Show Blocked State"
# 看是不是 NFS / I/O error
dmesg | grep -i "nfs\|I/O error\|task hung\|blocked"
# iowait 高不高
top # 看 wa% 一列
dmesg 里要是看到 nfs: server xxx not responding, timed out 或者 task xxx blocked for more than 120 seconds,基本就坐实了。
解决
-
- D 态进程只能等 I/O 回来或重启,信号杀不掉。
-
- NFS 场景:把挂载选项从
hard 改成 soft,intr,timeo=30,retrans=3,超时后返回错误而不是无限等;卡死的挂载 umount -f 或 umount -l(lazy,等忙完卸)。 -
- 磁盘场景:
smartctl -a /dev/sdX 看坏道,该换盘换盘。 -
- 最狠:真卡死连 reboot 都挂,只能
echo b > /proc/sysrq-trigger 强制重启(有数据风险,最后才用)。 -
避坑
-
- 关键路径(数据库、高并发写日志)别挂不可靠的网络存储;
-
- NFS 一律
soft + 明确 timeo/retrans,禁止 hard 无超时; -
- 监控
iowait 和 D 状态进程数,比等假死强。 -
五、Too many open files,但 ulimit -n 明明是 65535:systemd 的坑
场景
服务跑几天开始刷 Too many open files,新连接拒绝。你 ulimit -n 一查是 65535,够啊?改了 /etc/security/limits.conf 重启服务,还是 1024。
根因(机制)——三层限制,你改错层了
-
- 进程级:
/proc/<PID>/limits 里的 Max open files,由启动它的 shell 的 ulimit -n 或 systemd 的 LimitNOFILE= 决定; -
- 系统级:
fs.file-max,内核全局上限(sysctl fs.file-max); -
- systemd 经典坑:你在
limits.conf 写了 * soft nofile 65535,但服务是 systemd 起的——systemd 不走 PAM,根本不读 limits.conf,默认值还是 1024。必须在 .service 文件里加 LimitNOFILE=65535 才生效。这是 90% 人卡住的地方。 -
更深的坑:多数时候是 fd 泄漏——代码里打开文件/socket 没关(没放 finally/with),或者日志 handler 不释放。fd 只增不减,几天涨到上限。
排查
# 进程实际开了多少 fd
ls /proc/<PID>/fd | wc -l
# 它的上限是多少(看 Max open files 那行)
cat /proc/<PID>/limits | grep "Max open files"
# 系统级已分配/上限
cat /proc/sys/fs/file-nr
# 慢但全:lsof -p <PID> | wc -l
如果 ls /proc/PID/fd | wc -l 一直涨不回落,那就是泄漏,不是限额小。
解决
-
- 止血(不改代码):
prlimit --pid <PID> --nofile=65535:65535 临时把进程上限拉大;或者重启进程。 -
- 治本:修代码关 fd(用
with/try-finally);把三层限制都改对——尤其 systemd 服务加 LimitNOFILE=65535;系统级 sysctl -w fs.file-max=1000000 写进 /etc/sysctl.conf。 -
避坑
-
- 连接/文件用
with 或 try-finally 关,别指望 GC; -
- 监控 fd 数趋势,暴涨就是泄漏前兆;
-
- 改完 limits 一定
cat /proc/PID/limits 验证生效,别只看 ulimit -n; -
- systemd 起的服务,limits.conf 没用,认准
LimitNOFILE。 -
速查表
| 问题 |
一眼信号 |
关键命令 |
根因一句话 |
| 删文件空间不释放 |
df > du |
lsof +L1 |
进程还开着已删文件的 fd |
| inode 耗尽 |
df -h 有空间,df -i 满 |
df -i / find ... \| wc -l |
海量小文件抽干 inode 池 |
| TIME_WAIT 堆积 |
连接建不上,ss 几万 TW |
ss -ant \| grep -c TIME_WAIT |
短连接太频繁,不是参数问题 |
| D 状态假死 |
kill -9 无效,STAT=D |
cat /proc/PID/stack / dmesg |
卡在不可中断的 I/O(NFS/磁盘) |
| fd 耗尽 |
Too many open files |
ls /proc/PID/fd \| wc -l |
fd 泄漏 + systemd 不读 limits.conf |
这 5 个的共同点:报错都像常识,根因都在机制层。能讲清"为什么"的人,排查才快;只会背命令的,迟早在第三个问题上把生产搞挂。