Linux 磁盘占满怎么查:从 df 到 lsof 的完整排查路径

举报
yd_282015185 发表于 2026/10/29 08:15:12 2026/10/29
【摘要】 Linux 磁盘空间排查的完整路径:先分清 df -h 空间满与 df -i inode 满,再逐层定位分区与目录,用 lsof +L1 处理已删除但未释放的文件,最后处理容器日志与镜像层占用。附六条真实踩坑和一份可直接执行的排查脚本。

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。差额去哪了?两个主要来源:

  1. 已删除但被进程占用的文件(deleted but still open),第五层细讲
  2. 被挂载点遮住的原目录内容(上面那种情况)

还有一个少见但真实的原因:文件系统预留块。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 单独分区

日志涨满不会拖垮根分区,数据库所在分区也能单独扩。

十、小结

回到最开始的问题:「磁盘满了怎么办」。正确的顺序是:

  1. df -h 和 df -i 一起看,先分清空间满还是 inode 满
  2. du -x -h --max-depth=1 逐层往下钻,-x 是必须的
  3. find -xdev -size +1G 抓散落的大文件
  4. lsof +L1 查已删除未释放,用 reload 或 : > /proc/pid/fd/n 处理
  5. 容器场景单独查 docker system df 和 *-json.log

绝大多数「删了没效果」「明明有空却写不进去」都是第 4 步和第 1 步没做。把这两步养成习惯,剩下的都是找目录的体力活。

最后一句:动手删之前先看清楚是谁在写。 一个正在被写入的目录被 rm -rf 之后,往往比磁盘满本身更难收拾。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。