磁盘与 inode:空间到底被谁吃了
一、现象:df 说满了,du 加起来却不够
凌晨收到告警:根分区使用率 100%。登上机器 df -h 一看,40G 的盘确实只剩几百 K;可用 du -sh /* 挨个加起来只有 22G。差的 18G 既不在 /var/log,也不在 /home。再补一句 df -i,inode 才用了 12%。
这种"账对不上"的情况在运维里非常常见。它几乎不是灵异事件,而是几个固定原因之一:文件被删掉但进程还握着句柄、目录被别的文件系统挂载盖住了、文件系统给 root 留了专用块、稀疏文件的表观大小和实际占用不是一回事、或者统计时没带 -x 把别的挂载点算重了。
把 df 和 du 各自在数什么弄清楚,剩下的就是按图索骥。
二、原理:df 数块,du 数目录树
df 读的是超级块。 它不关心文件叫什么、属于谁,只问文件系统:一共有多少块、分配出去多少、还剩多少。这个数字由内核维护,是"块"级别的统计,和目录里有没有对应的文件名无关。
du 走的是目录树。 它从给定路径开始递归 stat 每个文件,把 st_blocks 乘 512 字节累加。没有名字的文件它看不到,因为目录项里根本没有它。
两者天然会有差额,主要来自五处:
- 已删除但仍被打开的文件。
rm只是把目录项摘掉、把链接数减一;只要还有进程持有 fd,inode 和数据块就不会释放,df继续算它,du永远找不到它。 - 被挂载点遮住的目录。
/data上挂了新盘,原来写进/data的那几个 G 还在下层文件系统里占着块,但du /data看到的是新盘。 - 保留块。ext4 默认给 root 保留 5% 的空间,普通用户看到的
Avail已经扣掉了这部分,root 却还能写。 - 稀疏文件。
ls -l显示的是表观大小,du显示的是真实分配的块,两者可能差几个数量级。 - 硬链接与重复统计。同一次
du运行内会去重,分成多次跑就会重复计数。
还有一个容易被忽略的前提:inode 总数在格式化时就定死了(XFS 是动态分配,行为不同)。mkfs.ext4 默认按每 16K 一个 inode 估算,小文件密集的业务很容易先撞上 inode 上限而不是容量上限。这也解释了为什么"盘还有一半空,却写不进任何文件"。
三、分步操作
3.1 先分清是块满还是 inode 满
这两种"满"的处理方式完全不同,先花十秒判断。
df -hT
df -i
df -hT 看容量和文件系统类型,df -i 看 inode 使用率。如果 IUse% 到 100% 而容量还有富余,那就是小文件太多,删大文件没用,得去清目录数量。
3.2 用 -x 逐层收敛
从根开始,一层一层往下钻,不要一上来就全盘扫。
du -xhd1 / 2>/dev/null | sort -rh | head -20
du -xhd1 /var 2>/dev/null | sort -rh | head -20
du -xhd1 /var/lib 2>/dev/null | sort -rh | head -20
-x 表示不跨文件系统,避免把挂载进来的数据盘、tmpfs 算进来;-d1 只看一层;sort -rh 按人类可读的大小倒序。2>/dev/null 用来吞掉权限不足的报错——但这同时也是个陷阱,后面会讲。
想在终端里直接翻目录,可以装 ncdu,它把 du 的结果做成可交互的树,方向键进出、d 删除,比反复敲命令直观:
ncdu -x /var
-x 在 ncdu 里同样表示不跨文件系统,第一次扫全盘会慢,扫局部目录很快。
3.3 直接找大文件
目录很多时,先按大小捞一遍往往更快。
find / -xdev -type f -size +500M -printf '%s\t%p\n' 2>/dev/null | sort -nr | head -20
-xdev 同样限制在同一个文件系统内,-printf 输出字节数方便排序。日志、备份包、core dump、老的镜像 tar,基本都在这一步现形。
3.4 揪出"已删除但仍被占用"的文件
这是 df 与 du 差额最经典的原因,尤其是日志被 rm 掉、进程没重启的场景。
lsof +L1 2>/dev/null | head -20
lsof -nP 2>/dev/null | awk '/deleted/ {print $1, $2, $7, $9}' | sort -k3 -nr | head
没有 lsof 的机器可以直接读 /proc:
find /proc/*/fd -ls 2>/dev/null | grep '(deleted)' | awk '{print $7, $11, $12, $13}' | sort -nr | head -20
找到之后有两条路:重启对应进程,或者在不重启的前提下把文件截断。第二句要写对路径,别写成 > /proc/1234/fd/7,重定向会新建文件而不是清空原 inode:
truncate -s 0 /proc/1234/fd/7
执行完再 df -h 看一眼,几十 G 通常立刻就回来了。
3.5 核对保留块
ext4 默认保留 5% 给 root,一块 40G 的盘就是 2G。用 df 看到的 Size - Used - Avail 对不上时,先算这笔账。
tune2fs -l /dev/vda1 | grep -Ei 'block count|reserved block count|block size'
纯数据盘不需要给 root 留这么多,调到 1% 就够:
tune2fs -m 1 /dev/vda1
这一步会改变文件系统参数,生产环境上先确认盘上没有依赖这 5% 的应急空间。
3.6 稀疏文件与表观大小
数据库、镜像、预分配的日志文件经常是稀疏的。ls -lh 报 10G,实际可能只占了 200M。
ls -lsh /var/lib/app/data.img
du -h --apparent-size /var/lib/app/data.img
du -h /var/lib/app/data.img
第一列是实际占用,--apparent-size 才是表观大小。判断"是不是它吃掉了空间",要看 du 不带参数的结果。
3.7 inode 耗尽的排查
inode 满的典型症状是:df -h 有空间,但新建文件报 No space left on device。
df -i /var
find /var -xdev -printf '%h\n' 2>/dev/null | sort | uniq -c | sort -nr | head -15
第二条命令统计每个目录下直接子项的数量,输出第一列就是文件数。常见的元凶是 session 目录、邮件队列、/tmp 里的临时文件、以及没做轮转的容器日志。
3.8 清理与立即回收
删日志优先用截断而不是 rm,尤其是正在写的文件:
: > /var/log/app/error.log
truncate -s 0 /var/log/app/access.log
journalctl --vacuum-size=500M
logrotate -f /etc/logrotate.d/app
journalctl --vacuum-size 按总量回收,也可以用 --vacuum-time=7d 按时间。logrotate -f 强制走一遍轮转策略,比自己 rm 安全。
3.9 容器环境的 overlay 账本
容器的可写层和镜像层都在 /var/lib/docker 下,du 看到的是合并后的视图,容易低估。
docker system df -v
du -xhd1 /var/lib/docker/overlay2 2>/dev/null | sort -rh | head
docker system df -v 会分别列出镜像、容器、卷、构建缓存各自的占用,比盲删目录靠谱。清理用 docker image prune -a 或 docker builder prune,别直接 rm -rf overlay2。
3.10 挂载点遮住的目录
怀疑有数据被挂载盖住时,用 bind mount 把根重新挂到一个空目录下看:
findmnt -T /data
mkdir -p /mnt/rootview && mount --bind / /mnt/rootview
du -xhd1 /mnt/rootview/data 2>/dev/null | sort -rh | head
umount /mnt/rootview
如果下层确实有几十 G 的旧数据,确认无用后可以在维护窗口内清掉,再卸载重挂。
3.11 一个可以放进 cron 的巡检脚本
#!/usr/bin/env bash
set -u
TH=$1
for m in $(df -P -x tmpfs -x devtmpfs | awk 'NR>1{print $6}'); do
u=$(df -P "$m" | awk 'NR==2{gsub("%","",$5); print $5}')
i=$(df -Pi "$m" | awk 'NR==2{gsub("%","",$5); print $5}')
[ "${u:-0}" -ge "$TH" ] && echo "DISK $m ${u}%"
[ "${i:-0}" -ge "$TH" ] && echo "INODE $m ${i}%"
done
chmod +x /usr/local/bin/diskcheck.sh
/usr/local/bin/diskcheck.sh 85
输出为空就是正常。接到告警系统时,把 echo 换成实际的推送命令即可。
3.12 别放过备份与快照残留
前面几步都干净、差额却还在时,重点看备份目录和快照挂载点。tar 打包中断留下的半截文件、rsync 的临时隐藏文件、.snapshot 目录,都是常客。
ls -la /data | grep -E '^\-.*\.(tar|gz|zip|tmp)$'
find /data -xdev -name '.*.??????' -o -name '.~tmp*' 2>/dev/null | head
mount | grep -Ei 'snapshot|nfs|cifs'
最后一条尤其重要:NFS 或对象存储挂载失败后,如果本地还留着一个同名目录,写进去的数据会落到根分区上,几十 G 不知不觉就没了。先 umount 掉失效挂载再清理,顺序反了会把数据写到错误的地方。
四、常见坑
删了文件空间没回来。 rm 只减链接数,进程还开着 fd 就不释放。先 lsof +L1 再决定是重启还是 truncate。
du 不带 sudo。 普通用户读不了的目录会被静默跳过,2>/dev/null 又把报错吞了,最后得出一个偏小的结论。排查时一律用 root。
忘记 -x。 根目录下挂了数据盘,du -sh /* 会把数据盘的内容算进根分区,看起来"超了"。
把 ls -l 的大小当占用。 稀疏文件、预分配文件都会骗人,以 du 为准。
只盯容量不看 inode。 小文件写爆 inode 时,容量面板一片绿,业务却已经写不进任何东西。
在容器里 du 全盘。 overlay 的下层是只读的镜像层,清了也没用,还会把容器搞坏。用 docker system df 看账。
日志直接 rm。 进程持有 fd,空间不释放;而且新日志会写到同一个已删除的 inode 上,ls 看不到、df 一直在涨。用 truncate 或 logrotate。
大目录用 rm -rf 删。 几十万小文件时极慢,可以先用 rsync -a --delete empty/ target/ 清空再删目录,速度通常快一个数量级。
五、小结
磁盘排查的顺序应该是固定的:先 df -hT 和 df -i 分清是块满还是 inode 满;再 du -xhd1 逐层收敛定位目录;然后 find -size 找大文件、lsof +L1 找幽灵文件;最后核对保留块、稀疏文件和挂载点遮挡。
df 与 du 的差额不是 bug,而是两个统计口径的必然结果。能解释差额,就等于找到了空间。真正要防的是复发:日志走 logrotate、journal 设上限、容器定期 prune、巡检脚本盯住容量和 inode 两条线。这样下次告警响起时,你面对的是一个已知的、有边界的数字,而不是一个"莫名其妙就满了"的黑箱。
- 点赞
- 收藏
- 关注作者
评论(0)