2871 字
约 9 分钟
16
S15-X08 · 磁盘与文件系统:df、du 与 inode 的三层诊断

"磁盘满了"是句含糊的话。它可能指块的空间用完,也可能指 inode 编号用完,还可能只是某个目录看着大、实际空间被已删除的文件攥着。三种情况的处理动作完全不同,混在一起查就会来回绕。

下面按三层展开:先确认是哪一层满了 → 再用对的工具定位到具体挂载点与目录 → 最后处理掉占用者。


1. 三层诊断的顺序

层 工具 回答的问题 满了之后的现象
块层 df -h 还剩多少字节 写入报 No space left on device
inode 层 df -i 还能创建多少文件 同样报 No space left,但 df -h 余量充足
目录层 du 谁吃掉了这些块 知道满了,但不知道是谁

顺序不能颠倒:先 df -h,余量正常转 df -i;两者都正常而应用仍写失败,才看配额、只读挂载或 cgroup 限制。

$ df -hT
Filesystem     Type   Size  Used Avail Use% Mounted on
/dev/vda1      ext4    99G   96G  1.1G  99% /
/dev/vdb1      xfs    500G  120G  380G  24% /data

$ df -i
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/vda1      6553600 6553600      0  100% /

这份输出是两种病:块快满(99%),同时 inode 已全满(100%)。只修一个不够。

df -T 用来确认文件系统类型。ext4 在格式化时一次性分配固定数量的 inode,xfs 动态分配。这决定了 inode 耗尽时你到底能不能"加 inode"。


2. df -h 与 df -i 分别看什么

列 含义 容易误读的地方
Size / Used / Avail 总块、已用块、可用块 Avail 已扣掉 root 保留块
Use% 已用 / 总量 分母含保留块,因此 100% 时 root 仍能写
IUsed / IFree 已用 / 剩余 inode 数 与文件大小无关,只看文件个数
IUse% inode 使用率 到 100% 时 df -h 可能还很空

ext4 默认给 root 保留 5%。Use% 显示 96% 时普通用户已写不进去,而 Avail 早把这块扣掉了,两个数字对不上是保留块造成的。

# 保留块从 5% 降到 1%,给普通用户多腾一点空间(对大分区有效)
$ tune2fs -m 1 /dev/vda1

inode 是另一回事:一个文件占一个 inode,与大小无关。


3. du 与 df 对不上的三种原因

最常见的困惑:df 说根分区用了 96G,du -sh / 加出 60G,剩下 36G 去哪了。

原因 特征 验证方式
已删除但被进程持有 du 不计入,空间直到进程关闭 fd 才释放 lsof +L1 或 /proc/<pid>/fd
挂载点嵌套重复统计 du 把子挂载点内容算进父目录,df 分开计 du -x 对比,findmnt 看挂载树
稀疏文件与预分配 逻辑大小远大于实际占用块 du 与 du --apparent-size 对比

第一种最隐蔽也最常见:rm 掉一个几 G 的日志,df 一点没降。原因是进程还开着这个 fd,目录项没了但数据块仍被 inode 引用。

$ lsof +L1 2>/dev/null | sort -k7 -nr | head -5
java     5123  app  4w  REG  253,1 19327352832  0 1234567 /app/logs/app.log (deleted)

+L1 表示"链接数小于 1",正好筛出已删除但仍被打开的文件;第 7 列是文件大小,按它倒序就能看到谁占得最凶。


4. du 的正确用法

# 只看根分区本层,不跨文件系统,深度 2,按大小倒序
$ du -x -h --max-depth=2 / 2>/dev/null | sort -rh | head -20

# 只出每个一级目录的总量
$ du -x -sh /* 2>/dev/null | sort -rh | head

# 逻辑大小 vs 实际占用:稀疏文件、预分配文件会差很多
$ du -sh --apparent-size /var/lib/mysql/ibdata1
参数 作用 什么时候必须加
-x 不跨文件系统 查 / 时,否则把 /data 的内容重复算进来
--max-depth=N 限制递归深度 先看两层定方向,再逐层下钻
-s 只输出汇总 批量比较多个目录
-h 人类可读单位 与 sort -h 配合
--apparent-size 按逻辑大小而非占用块 判断是不是稀疏文件/预分配

du 不做缓存,重复跑就是重复读元数据,大目录上很费时间。


5. 定位被删除但仍占空间的文件

# 路一:直接进进程的 fd 目录
$ ls -l /proc/5123/fd | grep deleted
l-wx------ 1 app app 64 Nov  9 03:12 7 -> /app/logs/app.log (deleted)

找到之后有两种处理方式,代价完全不同:

方式 操作 影响 适用
重启进程 systemctl restart app 服务中断,连接断开 可以安排停机窗口时
清空句柄 truncate -s 0 /proc/<pid>/fd/<n> 不中断,要求该 fd 以写方式打开 线上应急首选
$ truncate -s 0 /proc/5123/fd/7
$ df -h /    # 回看余量

truncate 走进程自己的 fd。对按行追加的日志没问题;对自记录 offset 的程序,清空后可能留一段空洞(\0)。


6. inode 耗尽:谁在造小文件

df -i 到 100% 时任何写操作都失败,包括 touch,但 df -h 可能还有几十 G。先按目录数文件个数:

# 统计各一级目录下的文件数(只数文件)
$ for d in /*; do printf "%-24s %s\n" "$d" "$(find "$d" -xdev -type f 2>/dev/null | wc -l)"; done | sort -k2 -nr | head

# 定位文件数超过 10 万的目录
$ find /var -xdev -type f 2>/dev/null | sed 's#/[^/]*$##' | sort | uniq -c | sort -nr | head
成因 典型路径 处理
海量小文件 上传目录、缓存目录 按天分目录 + 定期归档,不要同目录堆几十万文件
session 文件 /var/lib/php/sessions、自实现的 file session 改 Redis 存 session,或加 GC 脚本
日志分片 按请求生成的小日志、轮转配置不删旧文件 检查 rotate 与 maxage,补 compress
临时文件未清 /tmp、应用自己的 tmp 目录 systemd-tmpfiles 或 cron 清理

一次删太多小文件本身就会打满 IO:几十万文件用 find ... -delete 分批删,比 rm -rf 可控。


7. 日志爆盘的治理链路

日志是最常见的块层占用者。治理分三段:轮转 → 压缩 → 上限。

# /etc/logrotate.d/app
/app/logs/*.log {
    daily
    rotate 14
    maxsize 512M
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}

copytruncate 和 create 的取舍是这段配置唯一需要动脑的地方:

选项 行为 优点 代价
create 改名后新建空文件,进程需重开日志 无数据丢失,轮转干净 必须 postrotate 里 reload 进程,否则继续写旧 inode
copytruncate 先复制再截断原文件 不用 reload,适合不会重开日志的程序 复制与截断之间有极短窗口会丢日志
maxsize 达到大小也触发轮转 防止一天内写爆 与 daily 同配时按先到者触发

Docker 的默认日志驱动 json-file 不轮转,一个容器写满磁盘只是时间问题。宿主机层面统一设上限:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "50m", "max-file": "5" }
}

写进 /etc/docker/daemon.json 只对新容器生效,旧日志在 /var/lib/docker/containers/<id>/。


8. 文件系统层的检查

$ df -T                        # 确认类型与挂载参数
$ findmnt -T /data             # 挂载源与选项(ro/noatime/quota)
$ tune2fs -l /dev/vda1 | egrep -i "inode count|free inode|block count|free blocks|reserved"
$ xfs_info /data               # xfs 的块大小与 inode 分配策略
检查项 命令 能看出什么
是否只读挂载 findmnt -T <path> ro 说明内核因 IO 错误把文件系统降级为只读
ext4 inode 总量 tune2fs -l 的 Inode count 格式化时的 -N/-i 参数是否偏小
xfs 空间分布 xfs_info 块大小、AG 分布与预分配行为

ext4 的 inode 数在 mkfs 时定死,事后无法增加,只能备份重建;xfs 动态分配。


9. 排错与常见坑

排错表:

现象 原因 处理
df -h 99% 但 du 加起来差几十 G 已删除文件被进程持有 lsof +L1 定位,truncate 或重启
No space left 但 df -h 有空 inode 用完 df -i,再按目录数文件数清理
du -sh / 明显大于 df 跨文件系统重复统计 加 -x
同一文件 du 与 ls -l 差很多 稀疏文件/预分配 用 --apparent-size 对比确认
文件系统变只读 IO 错误后内核降级 dmesg 找错误,mount -o remount,rw 只能临时救
容器里 df 是宿主机的数 未配存储配额,看到的是同一层 查 docker inspect 的 UpperDir

常见坑单独列一遍,每条都真实踩过:

坑 表现 正确做法
直接 rm 大日志 空间不释放 先清空句柄或重开日志,再 rm
du 卡在某个目录 无输出、CPU 不高 加 -x,先 --max-depth=1 缩小范围
把 tmpfs 当持久盘 重启后数据消失、内存被吃光 /dev/shm、/run 只放临时数据
容器内外口径混淆 容器内清日志,宿主机 df 不降 在宿主机按 UpperDir 路径清,或设日志驱动上限
用 find / -name 找大文件 遍历全盘、把 IO 打满 先 du --max-depth=2 定位目录
只清日志不查来源 几小时后再次爆盘 同时加 maxsize 上限与余量告警

10. 边界:这些情况别硬用 df/du

场景 首选工具 理由
按大小排序找 Top N du --max-depth + sort -rh find -size 不告诉你总量
想知道某进程写了多少 /proc/<pid>/io 的 write_bytes du 只看结果不看来源
想抓增长速度 df -h 定时采样写入时序库 单次读数无法判断趋势
文件系统损坏 fsck / xfs_repair df 读超级块,损坏时数字不可信
需要目录级配额 XFS project quota、quota 块层满了才知道已经晚了

df 与 du 都是事后工具。省事的是事前设上限:日志按大小轮转、容器日志驱动设 max-size、上传目录按天归档。


11. 自测

  1. df -h 显示根分区 99%,但 du -xsh / 只算出来 60G,剩下 36G 最可能在哪?
  2. Use% 到了 96% 而普通用户已经写不进去,为什么 root 还能写?
  3. lsof +L1 里的 +L1 是什么意思,它筛出哪一类文件?
  4. 查 / 的目录占用时为什么必须加 -x?
  5. ext4 的 inode 用完了,能不能直接扩容 inode 数量?
  6. logrotate 的 create 与 copytruncate 分别要求应用做什么配合,选错了会丢什么?
S15-X08 · 磁盘与文件系统:df、du 与 inode 的三层诊断
https://www.clxhxhhr.top/posts/5807/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。