"磁盘满了"是句含糊的话。它可能指块的空间用完,也可能指 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. 自测
df -h显示根分区 99%,但du -xsh /只算出来 60G,剩下 36G 最可能在哪?Use%到了 96% 而普通用户已经写不进去,为什么 root 还能写?lsof +L1里的+L1是什么意思,它筛出哪一类文件?- 查
/的目录占用时为什么必须加-x? - ext4 的 inode 用完了,能不能直接扩容 inode 数量?
logrotate的create与copytruncate分别要求应用做什么配合,选错了会丢什么?