Linux运维12 min read次阅读

磁盘显示没满却写不进去:df 和 du 对不上的三种真实场景

凌晨两点报警电话响:某台接口机磁盘使用率 100%,写入日志开始失败。我 SSH 上去,df -h 确实满格,可 du -sh /* 一层层加下来,明明只有六十几 G,磁盘是 200G 的。差出来的那一百多 G 去哪了?这就是典型的「df 和 du 对不上」。

这种问题不稀奇,但每次第一反应都容易跑偏——有人直接去删日志,删了半天 df 纹丝不动,心里更慌。先把三件事分清楚:空间到底被谁占着、是块用完了还是 inode 用完了、以及文件是不是被「藏」在了某个挂载点下面。

先确认满的是「块」还是「inode」

df -h 看的是数据块,df -i 看的是 inode。两件事不是一个维度。inode 是文件系统里记录文件元信息的结构,每个小文件都要消耗一个。一台机器如果塞了几百万个几字节的 session 文件、mail spool 碎片或者缓存小图,inode 会先被吃光,而数据块还闲着一大半——这时候 df -h 显示还有空间,应用写文件却照样报 No space left on device

df -h /            # 块使用率
df -i /            # inode 使用率,重点看 IUse% 这一列

如果 df -i 的 IUse% 到了 100%,那问题就是小文件太多,下面专门说怎么查。如果块满但 du 对不上,往「幽灵文件」和「挂载点盖文件」这两个方向走。

场景一:文件删了,空间没回来(幽灵文件)

这是半夜最常被坑的一种。某个进程(常见的是日志轮子、Java 应用、Nginx)一直开着文件句柄在写,运维同学 rm 把文件删了,文件名确实没了,ls 看不到了,但进程还攥着那个 fd,磁盘块一直到进程关闭句柄才会真正释放。于是 df 一直是满的,可文件系统里已经找不到那个大文件。

lsof 能直接把这些「已删除但仍被打开」的文件捞出来——判断依据是被删文件的链接数(Link count)掉了,但 SIZE/OFF 还很大:

lsof +L1 | grep -i deleted
# 只关心 REG 类型且大小不为 0 的
lsof +L1 | awk '$5=="REG" && $7>0 {print $1, $2, $7, $9}'

输出大概长这样:

java      1234  root  7w  REG  8,1  12345678901  0   /var/log/app/access.log (deleted)

1234 是 PID,7w 是文件描述符(写模式),那一长串数字是文件当前大小。知道了 PID 和 fd,不用重启进程也能把空间腾出来——做法是直接把这个 fd 截断成 0 字节:

# 1234 是 PID,7 是 fd
: > /proc/1234/fd/7
# 等价写法
truncate -s 0 /proc/1234/fd/7

截断之后立刻 df -h 看,空间就回来了。等白天业务低峰再决定是重启这个进程(让它重新打开正常日志文件)还是修正它的日志轮转配置。这里有个坑:用 echo "" > /proc/.../fd/7 会写入一个空行而不是截断,效果不同,截断请用 : >truncate -s 0

如果一时找不到是哪个进程,又想快速确认到底有多少空间被这种幽灵文件占着,可以估算:

lsof +L1 | awk '$5=="REG" {sum+=$7} END {print sum/1024/1024/1024 " GB"}'

场景二:df 和 du 差一大截,但 lsof 里没有 deleted

lsof +L1 干干净净,可 df -hdu -sh / 还是对不上。这时候多半是「文件被挂载点盖住了」。举个例子:机器刚装好时,有人在 /var/log 底下写了几个大文件,后来把一块独立磁盘 mount 到了 /var/log。挂载之后,原来底层文件系统上那几个文件还在,只是被新挂载的文件系统盖住了,ls /var/log 看到的是新盘里的内容,旧文件看不见也数不进 du,但它确实占着底层盘的空间。

du 默认不跨文件系统统计,但可以通过 mount --bind 把根挂到别处,绕开挂载层去看底层真实内容:

mkdir -p /mnt/rootbind
mount --bind / /mnt/rootbind
# 现在去 /mnt/rootbind/var/log 底下找,能看到被盖住的老文件
du -sh /mnt/rootbind/var/log/* 2>/dev/null | sort -h

找到之后千万别在 /mnt/rootbind 里直接删——那是同一个底层文件系统,删了就是真删。正确的做法:先 umount /mnt/rootbind,然后在原来没被挂载盖住的视角(比如单用户模式、或从 LiveCD 启动)去处理,或者更稳妥的是确认文件确实没用后,进 /mnt/rootbind 删除再 umount。实际操作中我一般先确认文件创建时间早于挂载时间,比对 /proc/mounts 里的挂载记录,再下手。

还有一种更隐蔽的:用了 --bind 挂载或容器 volume,宿主机上 du 看到的是容器内视角,要去宿主机真实路径查。Docker 的话先看总量:

docker system df
# 大概率是 dangling 镜像 / 停止容器的 volume 占着
docker system prune -a --volumes

这一句会清掉所有没被容器引用的镜像、网络和 volume,生产环境跑之前先确认没有「停了但准备重启」的容器,否则连数据一起清掉。

场景三:inode 耗尽——满屏小文件

前面说过,df -h 还有空间却写不进文件,十有八九是 inode 没了。定位谁在造小文件:

# 按顶层目录统计文件数量
for d in /*; do
  c=$(find "$d" -xdev -type f 2>/dev/null | wc -l)
  echo "$c  $d"
done | sort -n

-xdev 很关键,它不让 find 钻进其他挂载的文件系统,否则 /proc/sys 会把统计带歪。找出数量异常多的目录后,再往下一层层 find ... | wc -l,定位到具体那批小文件。常见窝点:PHP 的 session 目录、邮件队列 /var/spool/mqueue、应用的本地缓存、CI 流水线的临时产物。

inode 这东西在 mkfs 时就定死了,ext4 和 xfs 都不能在文件系统建好之后「扩容」inode 数量。所以根治办法只有两条:删掉多余小文件,或者把那批数据迁到一个用更大 inode 密度重新格式化(ext4 用 -i 调小 bytes-per-inode,xfs 用 -i size=-d agcount)的文件系统。临时的止血可以用 find 按时间批量清:

# 删掉 7 天前的cache小文件,先 -print 确认再换 -delete
find /var/cache/app -type f -mtime +7 -print
find /var/cache/app -type f -mtime +7 -delete

-delete 之前一定先用 -print 看一遍,我就见过有人把 -mtime +7 写成 -mtime -7 把当天热数据全清了的。

一个容易被忽略的:保留块

ext2/3/4 默认给 root 留 5% 的空间(-m 参数),df 统计的是包含这 5% 的总量。普通用户写满到 95% 就报错了,但 root 还能继续写。有时候你以普通用户身份看 df 满格,切到 root 却还能写,就是这个原因。看看保留了多少:

dumpe2fs -h /dev/sda1 2>/dev/null | grep -i "reserved block"
# 大容量数据盘通常没必要留 5%,可以降到 1% 或 0
tune2fs -m 1 /dev/sda1

/dev/sda1 换成你实际的设备名。这条只对 ext 系列有效,xfs 没有保留块概念。降保留块能立刻「变」出一部分可用空间,但它本质是拿文件系统健壮性换容量——保留块原本是给 fsck 和 root 应急用的,数据盘可以降,系统盘别动。

稀疏文件也会让 du 和 ls 打架

还有一种对不上,不是故障,是稀疏文件(sparse file)的锅。数据库、虚拟机镜像、Kafka 的 log segment 常用稀疏文件预分配空间,逻辑上很大,实际只占用了写过的块。ls -l 显示的是逻辑大小,du 显示的是真实占用:

ls -l bigfile        # 比如显示 100G
du -h bigfile        # 可能只有 2G
du --apparent-size -h bigfile   # 加上这个才等于 ls 看到的逻辑大小

这种不是空间泄漏,别去「修」。只是提醒你:算容量规划时,稀疏文件和普通文件要分开看,否则容易对不上账。

排查顺序我一般这么走

真接到「磁盘满」报警,我不会上来就删东西,按这个顺序能少踩坑:

  1. df -hdf -i 一起看,先定是块满还是 inode 满。
  2. 块满 → lsof +L1 | grep deleted,有结果就截断对应 fd,空间秒回。
  3. 没有 deleted 文件 → mount --bind / /mnt/rootbind 看是不是被挂载点盖住。
  4. inode 满 → for d in /* 那个循环定位小文件窝点,按时间批量清。
  5. Docker 机器顺手 docker system df 看一眼 dangling。
  6. 最后才考虑保留块和稀疏文件这种「看着满其实没满」的情况。

这套流程跑下来,绝大多数「写不进去」都能在十分钟以内定位并止血。真正的教训是:删文件之前先搞清楚空间到底被谁占着,rm 一个被进程攥着的文件,除了让你心慌,什么用都没有。

定期巡检也比等报警强——我在一台核心机上挂了 df -i 的监控,inode 到 80% 就提前告警,比等它 100% 写挂了再半夜爬起来舒服得多。

分享:

相关文章

评论区