2024 年的一个下午,告警炸了:一台服务器磁盘满了,服务开始疯狂报 No space left on device,日志写不进、临时文件建不了。我 ssh 上去,df -h 一看,根分区 / 赫然写着 100%——确实满了。我心想这好办,找出那个占地方的大文件删掉就行。于是我从根目录开始,一层层 du -sh 往下找,把每个目录的占用加起来……结果让我懵了:我能找到的所有文件加在一起,连磁盘总量的一半都不到。磁盘明明显示满了,可那些空间,我【就是找不到】。一块磁盘的空间不会凭空消失。这件事逼着我把 Linux 的磁盘空间、df 与 du、inode、被删文件这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,根分区 / 共 50GB
事故现象:
- 告警:磁盘满,服务报 No space left on device
- df -h 显示 / 用了 100%
- 但 du 把所有文件加起来,只有 20 多 GB —— 对不上
现场排查:
# 1. 确认磁盘真的满了
$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 50G 0 100% /
# —— 确实 100%,一点空间都不剩
# 2. 从根目录找大目录
$ du -sh /* 2>/dev/null | sort -rh | head
8.2G /var
6.1G /usr
4.3G /home
2.0G /opt
# —— 全部加起来才 20 多 GB,可 df 说用了 50GB
# ★ 谜题:剩下将近 30GB 的空间,去哪了?
# 3. 找"被删除但仍被进程占用"的文件
$ lsof -nP | grep '(deleted)' | sort -k7 -rn | head
java 6500 root 3w REG 253,1 29200000000 /var/log/app/debug.log (deleted)
# ★ 真相:一个 29GB 的 debug.log 已经被 rm 删了,
# 但 java 进程(PID 6500)还开着它 —— 空间根本没释放!
根因(后来想清楚的):
1. 之前有人发现 debug.log 太大,直接 rm 删掉了它。
rm 之后 ls 确实看不到这个文件了,以为空间腾出来了。
2. ★ 但删文件的真相是:rm 只是把这个文件名(目录项)
从目录里抹掉,并【不等于】立刻释放磁盘空间。
3. 一个文件占的磁盘空间,只有在【没有任何目录项指向它,
且没有任何进程打开着它】时,才会被真正回收。
4. 那个 java 进程一直开着 debug.log 的文件描述符在写,
文件名虽然没了,但进程手里的 fd 还"攥着"这个文件——
于是这 29GB 空间一直挂在那里,不删不还。
5. du 是按【文件名】遍历的,文件名没了它就数不到;
df 是问内核【文件系统整体】用了多少,它知道真相。
两者一对不上,答案往往就是"被删文件仍被占用"。
磁盘空间没凭空消失,它被一个"看不见的文件"占着。
修复 1:df 与 du——两个命令看的根本不是一回事
# === df:从【文件系统】的角度看空间 ===
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 50G 0 100% /
# df 直接问内核:这个文件系统,总共多大、用了多少、还剩多少。
# ★ 它统计的是【块的分配情况】,不在乎这些块属于哪个文件。
# 被删但仍被进程占用的文件,占的块,df 【算在内】。
# === du:从【文件/目录】的角度看空间 ===
$ du -sh /var
8.2G /var
# du 的做法是:从一个目录出发,挨个遍历它下面的文件,
# 把每个文件的大小累加起来。
# ★ 它统计的是【能通过文件名访问到的文件】。
# 一个被 rm 掉、没有文件名的文件,du 【根本遍历不到】。
# === ★ 所以 df 和 du 对不上,是这次的核心线索 ===
# df 用量 > du 累加 -> 有"被删却仍被占用"的文件(本次)
# df 用量 ≈ du 累加 -> 正常
# du 比 df 还大 -> 通常是 du 重复算了硬链接,或跨了挂载点
# === 一个常见误判:挂载点被盖住 ===
# 如果某个目录(比如 /data)上挂载了别的磁盘,
# 而你在挂载【之前】往 /data 写过文件 ——
# 那些老文件被挂载"盖住"了:du 数不到、空间却占着。
$ mount | column -t # 看清楚挂载结构
$ df -h # 每个挂载点单独看用量
# === 看某个目录究竟挂载没挂载 ===
$ findmnt /data
$ stat -f /data # 看它属于哪个文件系统
修复 2:du 找大文件、大目录的正确姿势
# === 第一步:定位是哪个【顶层目录】在吃空间 ===
$ du -sh /* 2>/dev/null | sort -rh | head
# -s 只输出汇总 -h 人类可读 2>/dev/null 忽略没权限的报错
# sort -rh 按人类可读的大小【倒序】排 —— 一眼看到最大的
# === 第二步:钻进可疑目录,逐层往下 ===
$ du -h --max-depth=1 /var | sort -rh | head
4.9G /var/log # ★ 顺着最大的往下钻
3.0G /var/lib
$ du -h --max-depth=1 /var/log | sort -rh | head
4.6G /var/log/app # —— 一路钻到底,锁定目录
# === 第三步:在目标目录里找【单个大文件】===
$ find /var/log -type f -size +500M -exec ls -lh {} \;
# 找出所有大于 500MB 的文件
$ ls -lhS /var/log/app | head # -S 按大小排序,最大的在最上
# === 一行直接揪出全盘最大的 20 个文件 ===
$ find / -xdev -type f -printf '%s %p\n' 2>/dev/null \
| sort -rn | head -20 | awk '{printf "%.1fM\t%s\n",$1/1048576,$2}'
# ★ -xdev 关键:只在【当前文件系统】里找,不跨到别的挂载磁盘,
# 否则会把别的盘也遍历进来,结果就乱了。
# === 找最近被改动、且很大的文件(往往就是事故源头)===
$ find / -xdev -type f -size +100M -mtime -3 2>/dev/null
# -mtime -3 = 最近 3 天内修改过 —— 突发的磁盘满,八成是新写的
# === ncdu:交互式地浏览磁盘占用(强烈推荐)===
$ ncdu -x / # -x 同样表示不跨文件系统
# 方向键进出目录,d 键直接删 —— 比反复敲 du 高效得多
修复 3:被删文件仍占空间——这次的根因
# === ★ 先理解:rm 一个文件,到底删掉了什么 ===
# Linux 里,一个文件由两部分构成:
# - 目录项(文件名) —— "/var/log/app/debug.log" 这个名字
# - inode + 数据块 —— 文件的元信息和实际内容,真正占空间的
# rm 命令做的事,仅仅是【删掉目录项】(那个名字)。
# 至于 inode 和数据块那块空间,内核会维护一个【引用计数】:
# 引用计数 = 指向它的目录项数 + 正打开它的进程数
# 只有当这个计数【归零】时,空间才被真正回收。
# === 所以"被删却仍占空间"是这么发生的 ===
# 1. 进程 java 打开了 debug.log 在写 —— 它持有一个 fd。
# 2. 有人 rm debug.log —— 目录项没了,ls 看不到它了。
# 3. 但 java 的那个 fd 还在 —— 引用计数还是 1,不为零。
# 4. 于是空间【不释放】。文件成了一个"没有名字的幽灵",
# 只有那个 java 进程的 fd 还连着它。
# ★ 它会一直占着空间,直到 java 关掉 fd,或者进程退出。
# === ★ 揪出所有这种"幽灵文件" ===
$ lsof -nP | grep '(deleted)'
COMMAND PID USER FD TYPE SIZE/OFF NAME
java 6500 root 3w REG 29200000000 /var/log/app/debug.log (deleted)
# lsof 列出所有打开的文件;标着 (deleted) 的,
# 就是"文件名已删、但仍被进程占着"的幽灵。
# SIZE/OFF 那一列,就是它还霸占着的字节数。
# === 按占用大小排序,先解决最大的 ===
$ lsof -nP | grep '(deleted)' | awk '{print $7, $1, $2, $9}' | sort -rn | head
# === 处理办法 1(推荐):重启 / 优雅地让进程放掉 fd ===
$ systemctl restart myapp
# 进程一退出,它持有的 fd 全部关闭,引用计数归零,
# 那 29GB 空间【立刻】被释放。df 马上就降下来。
# === 处理办法 2(不能重启时):清空那个 fd 指向的内容 ===
# 进程还不能停?那就通过 /proc 把那个文件的内容【截断成 0】:
$ ls -l /proc/6500/fd | grep deleted
l-wx------ 1 root root 64 ... 3 -> /var/log/app/debug.log (deleted)
$ : > /proc/6500/fd/3 # ★ 把 fd 3 指向的文件内容清空
# 文件还在(进程还开着),但内容清零,空间被回收。
# ★ 注意:这只对"还在被追加写的日志"这类文件安全。
# === 验证空间回来了 ===
$ df -h / # Use% 应该明显下降
修复 4:inode 耗尽——另一种"磁盘满"
# === ★ 还有一种 No space left on device,df -h 却显示有空间 ===
# 文件系统里,除了"数据块"有限,"inode"也是有限的。
# 每创建一个文件(哪怕是 0 字节),就消耗【一个 inode】。
# inode 一旦用光,就再也建不了新文件 —— 哪怕磁盘还很空。
# === 用 df -i 看 inode 的使用情况 ===
$ df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vda1 3276800 3276790 10 100% /
# ★ IUse% 100% —— inode 用光了!这才是这种情况的真凶。
# 此时 df -h 可能还显示有空间,但就是建不了文件。
# === 定位:是哪个目录塞了【海量小文件】===
# inode 耗尽,几乎总是因为某个目录下堆了几百万个小文件。
$ for d in /var/* /tmp/* /home/*; do \
echo "$(find "$d" 2>/dev/null | wc -l) $d"; \
done | sort -rn | head
# 哪个目录的文件【数量】最多,就往哪里钻。
# === 常见元凶 ===
# - 某个程序狂建临时文件却不删(/tmp 塞满)
# - 邮件队列、session 文件堆积
# - 没配清理的缓存目录
# === 处理:删掉那堆小文件 ===
# ★ 文件多到 rm * 都会报 "Argument list too long",用 find:
$ find /tmp/junkdir -type f -delete
# 或者文件数实在太多,find 配合 xargs 分批删:
$ find /tmp/junkdir -type f -print0 | xargs -0 rm -f
# === 看单个目录用了多少 inode ===
$ find /some/dir -xdev -printf '.' 2>/dev/null | wc -c
# 输出的数字 = 这个目录树下的文件总数 ≈ 占用的 inode 数
修复 5:清理与预防——别再让磁盘满第二次
# === 应急清理:能安全删的几类东西 ===
$ journalctl --vacuum-size=200M # systemd 日志只留 200M
$ yum clean all # 清 yum 缓存(CentOS)
$ du -sh /var/cache/* # 看缓存目录,按需清
$ find /tmp -type f -atime +7 -delete # 删 7 天没访问的临时文件
# === ★ 治本一:日志必须配 logrotate,别让它无限长 ===
$ vim /etc/logrotate.d/myapp
/var/log/app/*.log {
daily # 每天轮转
rotate 7 # 只保留 7 份历史
size 100M # 或者超过 100M 就轮转
compress # 旧日志压缩
missingok
notifempty
copytruncate # ★ 关键:复制后【就地清空】原文件
}
# ★ copytruncate 的意义:它不是删掉日志再建新的,
# 而是把内容复制走、再把原文件【truncate 成 0】——
# 原文件的 inode 不变,进程的 fd 依然有效,
# 完美避开这次"rm 删日志、空间却不释放"的坑。
$ logrotate -d /etc/logrotate.d/myapp # -d 先演练,不真执行
# === ★ 治本二:清理大日志,用 truncate,不要用 rm ===
# 想清空一个正在被写的大日志:
$ truncate -s 0 /var/log/app/debug.log # 就地清零,空间立即回收
$ : > /var/log/app/debug.log # 同效果(shell 重定向)
# ★ 千万别 rm 一个正在被进程写的日志 —— 这次的坑就是这么踩的。
# rm 之后空间不还,你还得去重启进程才能要回来。
# === 治本三:磁盘用量接进监控,别等满了才知道 ===
$ df -h --output=pcent,target | awk 'NR>1 && $1+0 > 85 {print "磁盘告警:", $0}'
# 用量超过 85% 就告警,留出充足的处理时间。
# inode 也要一起监控:
$ df -i --output=ipcent,target | awk 'NR>1 && $1+0 > 85 {print "inode告警:", $0}'
# === 预防:给可能写爆的目录【单独挂一块盘】===
# 把 /var/log、/tmp 这种"高风险写入区"挂到独立分区 ——
# 它就算被写爆,撑爆的是它自己,不会连累根分区 / 。
修复 6:磁盘排查纪律
# === 这次事故暴露的磁盘认知盲区,定几条纪律 ===
# === 1. ★ df 和 du 对不上,第一反应查"被删文件" ===
$ lsof -nP | grep '(deleted)'
# df 用量明显大于 du 累加 —— 几乎就是有进程占着已删文件。
# === 2. No space left on device,要同时查空间和 inode ===
$ df -h # 看数据块满没满
$ df -i # ★ 看 inode 满没满 —— 别只看 -h
# 两者任一满了,都会报这个错。
# === 3. ★ 清空被进程写的大文件,用 truncate,绝不用 rm ===
$ truncate -s 0 大日志文件
# rm 一个被打开的文件,空间不会还给你,反而要去重启进程。
# === 4. du 找文件,记得加 -x 不跨文件系统 ===
$ du -shx /*
# 不加 -x,会把别的挂载盘也算进来,数字就乱了。
# === 5. 警惕"挂载点盖住老文件" ===
# 怀疑空间对不上,把某个挂载点临时卸载,看看下面藏没藏老文件。
# === 6. 大日志靠 logrotate 自动轮转,别靠人工 rm ===
# logrotate + copytruncate,从机制上杜绝这次的坑。
# === 7. 磁盘排查的命令链 ===
$ df -h # ① 哪个分区满了
$ df -i # ② 是不是 inode 满了
$ du -shx /* 2>/dev/null | sort -rh # ③ 空间花在哪个目录
$ lsof -nP | grep '(deleted)' # ④ 有没有被删却被占的幽灵文件
$ find / -xdev -size +500M 2>/dev/null # ⑤ 揪出超大文件
# 按这个顺序,磁盘问题基本能定位。
命令速查
需求 命令
=============================================================
看磁盘空间用量 df -h
看 inode 用量 df -i
找大目录(逐层) du -h --max-depth=1 目录 | sort -rh
找大文件 find / -xdev -type f -size +500M
找被删却被占用的文件 lsof -nP | grep '(deleted)'
交互式浏览磁盘占用 ncdu -x /
清空被进程写的大文件 truncate -s 0 文件
清空某进程的某个 fd : > /proc/PID/fd/N
看 systemd 日志占用并清理 journalctl --vacuum-size=200M
看挂载结构 mount | column -t / findmnt
口诀:满了先 df -h 和 df -i 都看 -> du 找大目录
df 比 du 大就 lsof grep deleted -> 清空用 truncate 别用 rm
避坑清单
- df 看文件系统整体用量,du 按文件名累加,两者口径不同会对不上
- df 用量远大于 du 累加,几乎一定是有进程占着已被删除的文件
- rm 只删文件名,空间要等引用计数(目录项数+打开它的进程数)归零才释放
- 用 lsof | grep deleted 揪出"幽灵文件",重启进程或清空 /proc/PID/fd/N
- 清空正在被写的大日志要用 truncate -s 0,绝不能用 rm
- No space left 也可能是 inode 耗尽,必须用 df -i 单独确认
- inode 耗尽通常是某目录堆了海量小文件,用 find -delete 清理
- du 找文件加 -x、find 加 -xdev,避免跨文件系统把别的盘也算进来
- 挂载点会盖住挂载前写入的老文件,du 数不到但空间真占着
- 大日志用 logrotate + copytruncate 自动轮转,从机制上杜绝人工 rm 的坑
总结
这次"磁盘满了、空间却找不到"的事故,纠正了我对"删除文件"这个再日常不过的操作一个根深蒂固的误解。在这次之前,我心里的等式简单而想当然:rm 掉一个文件,就等于把它占的磁盘空间还了回去——文件没了,空间自然就回来了。正是这个错误的等式,让我在排查时彻底迷失:我从根目录出发,一层一层地 du,把能找到的每一个文件、每一个目录的大小都加了起来,可这个总和,和 df 报出来的"已用 50GB"之间,差了将近 30GB——这 30GB 像是凭空蒸发了。复盘到根上,我才真正搞懂了 Linux 里"一个文件"到底是什么,以及 rm 究竟做了什么。在 Linux 的文件系统里,我们平时说的"文件",其实是两样东西的组合:一样是"目录项",也就是挂在某个目录下的那个文件名,比如 /var/log/app/debug.log 这个字符串;另一样是 inode 连同它底下的数据块,这才是文件的元信息和真正占据磁盘空间的实体。而 rm 这个命令,它做的事情远比我想象的要小——它仅仅是把那个"目录项",也就是那个文件名,从目录里抹掉而已。至于 inode 和数据块那片真正占空间的实体,内核并不会在 rm 的那一刻就回收它。内核给每个文件实体维护着一个"引用计数",这个计数等于指向它的目录项数量,加上当前正打开着它的进程数量;只有当这个计数彻底归零的时候,那片磁盘空间才会被真正释放。这就解释了我那台服务器上发生的一切:之前有人嫌 debug.log 太大,顺手 rm 掉了它,ls 里确实再也看不到这个文件,大家便都以为空间腾出来了。可就在这个文件被 rm 的时候,那个 java 进程,正持有着这个文件的一个文件描述符,还在源源不断地往里写日志。rm 抹掉了文件名,目录项的引用归零了,但进程那一边的引用还在——引用计数停在了 1,没有归零。于是这个 29GB 的文件,就变成了一个没有名字的"幽灵":它在任何目录里都查不到,du 靠遍历文件名来统计,自然永远数不到它;可它的数据块,实实在在地、一个不少地占据着磁盘,而 df 是直接向内核询问文件系统整体的块分配情况的,它把这片幽灵占据的空间,清清楚楚地算在了"已用"里。df 和 du 之间那 30GB 的鸿沟,正是这个幽灵文件的大小。想通了这一层,后面的一切就顺理成章了:我用 lsof 配合 grep '(deleted)',一下就把这个被 java 进程攥在手里的、标着 (deleted) 的幽灵文件揪了出来;接着只要重启那个 java 进程,它一退出,手里的文件描述符全部关闭,引用计数归零,那 29GB 空间在一瞬间就回到了 df 的可用栏里。这次事故之后,我给自己立下了一条铁律:要清空一个正在被进程写入的大日志,永远用 truncate -s 0 去就地把它的内容清零,而绝不能用 rm 去删它。因为 truncate 不动文件名、不动 inode,进程手里的文件描述符依然有效,空间却能立刻回收;而 rm 只会制造出又一个吞噬着空间、却谁也看不见的幽灵。这次从一个对不上账的磁盘出发,我最大的收获,是终于明白了一件事:在 Linux 里,删掉一个文件的名字,和释放它占的空间,从来就不是同一件事。
—— 别看了 · 2026