Linux 磁盘占满怎么查:从 df 到 lsof 的完整排查路径
Linux 磁盘占满怎么查:从 df 到 lsof 的完整排查路径
「磁盘满了」这四个字在告警里只是一条,但背后的原因能分成十几个完全不同的方向。有人上来就 rm -rf 删日志,删完 df 一看还是 100%;有人删了 20G 文件发现空间没释放;有人折腾半天才发现根本不是磁盘满,是 inode 用完。
这篇把这条排查路径拆成五层:先分清是「空间满」还是「inode 满」,再看是「哪个分区」、再定位「哪个目录」、再区分「文件还在不在被占用」、最后处理容器和已删除未释放这两类特殊情况。每一层给出具体命令和判断依据,最后附六个真实踩坑。
一、第一件事:分清空间满还是 inode 满
大部分人的第一反应是 df -h,看到 Use% 100% 就开始找大文件。但 df -h 只反映数据块占用,不反映 inode 占用。两者是不同的资源池,一个满另一个可能还很空。
所以第一组命令要一起跑:
df -h
df -i
df -h 看的是块设备容量,df -i 看的是 inode 数量。典型输出:
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 40G 40G 0 100% /
/dev/vdb1 200G 12G 178G 7% /data
$ df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vda1 2621440 262144 0 100% /
/dev/vdb1 13107200 20480 13086720 1% /data
判断依据
df -h |
df -i |
结论 |
|---|---|---|
| 100% | 正常 | 普通空间满,走第二层找大文件 |
| 正常 | 100% | inode 满,跟文件大小无关,是文件数量太多 |
| 100% | 100% | 两者都满,先解决 inode,因为 inode 满时很多工具会报 No space left on device 而无法创建临时文件 |
inode 满的表现很迷惑:df -h 显示还有几十 G 空闲,但 touch 新文件直接报 No space left on device,日志也写不进去,服务重启失败。这时候 du -sh 找大目录是白费力气,因为占用 inode 的可能是几百万个小文件,总大小加起来才几个 G。
inode 满的常见来源:
- 邮件队列(
/var/spool/postfix/maildrop)堆积几十万个小文件 - PHP
/tmp或 session 目录没清理 - 日志按小时切分且从不删除,几年下来几百万个文件
- Docker 的 overlay2 层目录、或者某个应用写了几百万个缓存小文件
- Git 仓库里
objects目录(大量小文件)
注意:inode 数量是格式化时定死的,不能在线扩容。 ext4 想加 inode 只能重建文件系统(或者用 resize2fs 前提是文件系统还没满且预留了空间,实际很难)。所以 inode 满了基本只能删文件,没有第二条路。这也是为什么新机器分区时不能把 inode 比例设得太小。
二、定位是哪个分区
如果 df 只显示根分区满,/data 之类的挂载点还有空间,那问题范围就缩小了。但要注意两种会让 df 结果误导人的情况。
情况一:挂载点被覆盖
如果一个目录本来是根分区的一部分,后来在上面挂了新分区,而挂载前里面已经有文件,那些文件会被「藏起来」——它们仍占着根分区的块,但通过新挂载点看不到。
典型场景:先往 /data 写了几十 G 数据,然后格式化 /dev/vdb1 挂到 /data。原来的数据还占着 / 的空间,但 du -sh /data 只统计新分区的新内容。
处理方式是把根分区挂到临时目录再看:
mkdir -p /mnt/root
mount --bind / /mnt/root
du -sh /mnt/root/* 2>/dev/null | sort -rh | head -20
umount /mnt/root
情况二:df 和 du 对不上
这是最经典的现象:df 说用了 40G,du -sh / 加起来只有 15G。差额去哪了?两个主要来源:
- 已删除但被进程占用的文件(deleted but still open),第五层细讲
- 被挂载点遮住的原目录内容(上面那种情况)
还有一个少见但真实的原因:文件系统预留块。ext4 默认给 root 预留 5% 的空间,df 的 Used% 是按可用空间算的,可能显示 100% 但实际还有 5% 没被普通用户使用。tune2fs -m 1 /dev/vda1 可以把预留比例降到 1%,但这只是缓解,不是解决。
三、定位是哪个目录
确认了分区,接下来缩小到目录。这条命令几乎适用于所有场景:
du -x -h --max-depth=1 / 2>/dev/null | sort -rh | head -20
关键在 -x:不跨越文件系统边界。不加 -x,du 会跑进 /proc、/sys,也会跨进挂载的其他分区,结果既慢又乱(同一个文件被算两次,或者算到别的分区头上)。
一级目录锁定了,继续往下钻:
du -x -h --max-depth=1 /var 2>/dev/null | sort -rh | head -20
du -x -h --max-depth=1 /var/log 2>/dev/null | sort -rh | head -20
按文件找,而不是按目录找
有时候最大的不是某个目录,而是散落在各处的几个大文件。直接按大小排全盘文件:
find / -xdev -type f -size +500M -exec ls -lh {} \; 2>/dev/null | awk '{print $5, $9}' | sort -rh | head -20
-xdev 等价于 du 的 -x,不跨文件系统。-size +500M 可以按需调整,找 1G 以上改成 +1G。
常见的大户位置
排查多了会发现,90% 的情况集中在这几个地方:
| 路径 | 常见原因 |
|---|---|
/var/log |
日志没轮转,或轮转配置了但 rotate 数量太大 |
/var/lib/docker |
镜像层、容器日志(*-json.log 不轮转会无限涨) |
/var/lib/mysql 或 /data/mysql |
binlog 没设过期时间 |
/root 或 /home |
下载的大文件、备份包、core dump |
/tmp |
应用临时文件从不清理 |
/var/cache |
包管理器缓存(apt / yum 的包) |
几个针对性的快查命令:
# 容器日志大小(Docker 默认 json-file 驱动不限制大小)
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -rh | head
# 包管理器缓存
du -sh /var/cache/apt /var/cache/yum /var/cache/dnf 2>/dev/null
# MySQL binlog
du -sh /var/lib/mysql/*bin* 2>/dev/null
# core dump
find / -xdev -name "core.*" -o -name "*.core" 2>/dev/null | head
四、ls 看到的和 du 算出来的不一致
有时候 du -sh 说某个目录 20G,但 ls -lh 把里面所有文件加起来才 200M。这种「对不上」几乎总是同一个原因:目录里有大量被删除但仍被进程持有的文件,或者有稀疏文件。
稀疏文件(sparse file)是 ls 显示的逻辑大小和 du 显示的实占块数不一致的另一个来源。数据库、虚拟机镜像常用稀疏文件,ls -lh 看到 100G,du -h 可能只有 2G。这种情况下不要被 ls 吓到,以 du 为准。
对于「已删除未释放」,直接进第五层。
五、已删除但未释放:为什么删了文件空间没回来
这是最让人抓狂的一类。rm 了一个 20G 的日志文件,ls 里也没了,df 一点没变。
原因是 Linux 的文件删除语义:rm 只是把目录项摘掉、把 inode 的链接数减 1。只要还有进程持有这个文件的打开句柄,数据块就不会释放。 持有句柄的进程可能是:
- 正在写日志的应用(最常见:
java、nginx、python的 logging) tail -f之类还在读的进程- 被
rsync/scp正在传输的文件 - 容器里的进程(句柄在宿主机上)
定位
lsof +L1 2>/dev/null | head -30
+L1 表示「链接数小于 1」,也就是已经没有任何目录项指向它。输出里的 SIZE/OFF 列就是文件大小。按大小排序:
lsof +L1 2>/dev/null | awk 'NR>1 {print $7, $1, $2, $NF}' | sort -rn | head -20
没有 lsof 的环境(很多精简镜像不带),用 procfs 直接查:
ls -l /proc/*/fd/* 2>/dev/null | grep deleted | head -30
或者按进程聚合看谁占得多:
for p in /proc/[0-9]*; do
pid=${p#/proc/}
sz=$(ls -l $p/fd/* 2>/dev/null | grep -c deleted)
[ "$sz" -gt 0 ] && echo "$pid $(ls -l $p/fd/* 2>/dev/null | grep deleted | awk '{s+=$5} END {print s/1024/1024" MB"}') $(cat $p/comm 2>/dev/null)"
done | sort -k2 -rn | head
处理
不要 kill -9。 对写日志的进程用 kill -9 可能导致数据丢失、连接异常断开。正确做法有两种:
方式一:重启或 reload 进程(推荐)
systemctl reload nginx # 或 restart
reload 会让进程重新打开日志文件,旧句柄关闭,空间立刻释放。
方式二:不清空 inode 但截断它
如果不想重启进程,可以清空文件内容但保留句柄:
: > /proc/<pid>/fd/<fd_num>
这条命令不删除 inode,只是把文件截断到 0 字节,进程继续往里写也不会出错。前提是你确认这个 fd 就是以写模式打开的日志文件,不要对数据库文件、socket 做这个操作。
生产上更稳的做法是配合日志轮转:logrotate 用 copytruncate 而不是 create,就能避免这个坑(代价是轮转瞬间可能丢几行日志)。
六、容器场景的特殊处理
容器把这个问题复杂化了一层:文件在容器里删了,占用算在宿主机上。
容器日志
Docker 默认 json-file 驱动不限制大小,一个不停打日志的容器几天就能吃掉几十 G。
# 找出最大的容器日志
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -rh | head -5
# 定位是哪个容器
for f in /var/lib/docker/containers/*/*-json.log; do
id=$(basename $(dirname $f))
echo "$(du -h $f | cut -f1) $id $(docker inspect --format '{{.Name}}' $id 2>/dev/null)"
done | sort -rh | head
处理:
# 临时清理(不重启容器,直接截断)
truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log
永久方案是在 daemon 配置里加限制(/etc/docker/daemon.json):
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
改完需要重启 Docker daemon,且只对新建的容器生效,已有容器要重建。
容器内的删除
如果容器里 rm 了一个文件但空间没释放,原因同第五层——是容器内进程持有句柄。在宿主机上 lsof +L1 也能看到(COMMAND 列可能显示为容器内的进程名),处理方式是 docker restart <container>。
镜像和悬空层
docker system df
docker system df 会分四类列出:Images、Containers、Local Volumes、Build Cache。经常是 Build Cache 占了几十 G 而没人注意。
docker system prune -a --volumes # 危险,先看清楚再执行
这条命令会删掉所有未使用的镜像、容器、网络、卷。 卷(volume)里可能有数据库数据,--volumes 尤其危险。建议分开执行,先 docker image prune -a,再 docker builder prune,最后才考虑卷。
七、六个真实踩坑
坑 1:du 不带 -x,跑到 /proc 里卡死
du -sh / # 错误:会进 /proc、/sys、/dev,可能跑几十分钟
du -shx / # 正确:不跨文件系统
/proc 里的文件大小显示为 0 但读取会触发内核行为,某些情况下 du 会卡住。养成习惯:任何全盘 du 都带 -x。
坑 2:日志轮转配了但没生效
/etc/logrotate.d/ 里明明有配置文件,日志还是涨到几十 G。三个常见原因:
logrotate的 cron 任务没跑(检查/etc/cron.daily/logrotate是否存在、是否可执行)- 配置里写了
daily但size条件没满足,或者maxsize没设 rotate数量设成 52 但compress没开,52 个未压缩大文件加起来更占空间
验证方式:
logrotate -d /etc/logrotate.d/your-app # 干跑,看它认为该做什么
cat /var/lib/logrotate/status # 看上次轮转时间
坑 3:df 显示 100% 但 du 只有一半——预留块
ext4 默认 5% 预留。40G 的盘预留 2G,df 的 Used% 按可用容量算,可能提前显示满。降预留:
tune2fs -m 1 /dev/vda1
但这只是把 4% 还给用户,属于治标。真正要紧的是先找到占用大头。
坑 4:删了文件但目录还在被挂载遮住
前面第二层讲过。现象是 du -sh / 比 df 的一 Used 小很多,且 lsof +L1 查不出东西。用 mount --bind 把根分区单独挂出来看才能发现。
坑 5:inode 满时什么都做不了
inode 为 0 时,连 touch 都报 No space left on device,vim 打不开文件(要写 swp),很多清理脚本也会因为写临时文件失败而中断。
应急手段:
# 先腾出一个 inode
rm -f /var/spool/postfix/maildrop/* 2>/dev/null
# 或清空一个已知的大目录
find /tmp -type f -delete 2>/dev/null
腾出空间后再从容处理。所以监控一定要同时盯 inode,不能只看磁盘使用率。
坑 6:rm -rf 正在被写入的目录
对持续写入的目录(日志、缓存、上传临时区)直接 rm -rf 会导致:进程报 ENOENT 但继续持有句柄、空间不释放、应用异常。正确做法是先停写入(reload 或改配置指向新目录),再删旧目录。
八、一份可以直接用的排查脚本
把上面的路径串起来,写成一条能在故障时直接跑的脚本:
#!/usr/bin/env bash
# disk-check.sh —— 磁盘/inode 占用快速排查
set -uo pipefail
echo "===== 1. 分区与 inode ====="
df -hT | awk 'NR==1 || $6+0>=80'
df -i | awk 'NR==1 || $5+0>=80'
echo
echo "===== 2. 各分区 Top 目录(不跨文件系统) ====="
for mp in $(df -P | awk 'NR>1 && $6 ~ /^\// {print $6}' | sort -u); do
echo "--- $mp ---"
du -x -h --max-depth=1 "$mp" 2>/dev/null | sort -rh | head -8
done
echo
echo "===== 3. 大于 1G 的文件 ====="
find / -xdev -type f -size +1G -printf '%s\t%p\n' 2>/dev/null \
| sort -rn | head -15 \
| awk -F'\t' '{printf "%.1fG\t%s\n", $1/1073741824, $2}'
echo
echo "===== 4. 已删除未释放文件 ====="
if command -v lsof >/dev/null 2>&1; then
lsof +L1 2>/dev/null | awk 'NR==1 || $7+0 > 104857600' \
| awk 'NR==1 {print; next} {printf "%.1fG\t%s\tpid=%s\t%s\n", $7/1073741824, $1, $2, $NF}'
else
echo "(未安装 lsof,改用 /proc)"
ls -l /proc/*/fd/* 2>/dev/null | grep deleted | awk '{print $5, $9, $11}' | head -15
fi
echo
echo "===== 5. 容器相关 ====="
if command -v docker >/dev/null 2>&1; then
docker system df 2>/dev/null
echo "--- 最大容器日志 ---"
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -rh | head -5
fi
echo
echo "===== 6. 日志目录 ====="
du -sh /var/log 2>/dev/null
find /var/log -type f -size +100M -printf '%s\t%p\n' 2>/dev/null \
| sort -rn | head -10 \
| awk -F'\t' '{printf "%.0fM\t%s\n", $1/1048576, $2}'
用起来就是把输出贴进故障单,一眼就能看出卡在哪一层。
九、日常预防
排查是被动的,真正省事的是把几件事配好:
1. 日志限制大小
- Docker:
daemon.json里设max-size+max-file - 系统日志:
/etc/systemd/journald.conf里设SystemMaxUse=2G - 应用日志:
logrotate配maxsize+rotate+compress+copytruncate
2. 数据库
- MySQL:
binlog_expire_logs_seconds=604800(7 天) - PostgreSQL:
wal_keep_size配合归档策略
3. 监控同时盯两个指标
磁盘使用率 和 inode 使用率。只盯前者会在 inode 满时完全瞎掉。
4. 定期清理
# 系统日志超 7 天清理
journalctl --vacuum-time=7d
# apt 缓存
apt-get clean
# docker 悬空镜像
docker image prune -a -f
5. 分区规划时给 /var 和 /data 单独分区
日志涨满不会拖垮根分区,数据库所在分区也能单独扩。
十、小结
回到最开始的问题:「磁盘满了怎么办」。正确的顺序是:
df -h和df -i一起看,先分清空间满还是 inode 满du -x -h --max-depth=1逐层往下钻,-x是必须的find -xdev -size +1G抓散落的大文件lsof +L1查已删除未释放,用 reload 或: > /proc/pid/fd/n处理- 容器场景单独查
docker system df和*-json.log
绝大多数「删了没效果」「明明有空却写不进去」都是第 4 步和第 1 步没做。把这两步养成习惯,剩下的都是找目录的体力活。
最后一句:动手删之前先看清楚是谁在写。 一个正在被写入的目录被 rm -rf 之后,往往比磁盘满本身更难收拾。
- 点赞
- 收藏
- 关注作者
评论(0)