df -h 还剩 40%,创建文件却报 No space left on device:inode 耗尽排查
报错是这样的:
$ df -h /data
Filesystem Size Used Avail Use% Mounted on
/dev/vdb1 500G 300G 200G 60% /data
$ touch /data/test
touch: cannot touch '/data/test': No space left on device
空间明明有 200G。这种时候第一个要看的不是 df -h,是 df -i。
$ df -i /data
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vdb1 32768000 32768000 0 100% /data
inode 满了。
inode 是什么
ext4/xfs 在格式化的时候就把 inode 数量定死了,每个文件(包括目录、符号链接、socket)占一个 inode。inode 里存的是元数据:权限、属主、时间戳、数据块指针,唯独不存文件名。文件名在目录项里。
所以"空间够但 inode 不够"意味着:文件太多,而不是文件太大。典型的 inode 杀手有这么几类:
- 会话文件。PHP 的 session、Java 的临时上传目录,一个请求一个文件。
- 邮件队列。postfix 的 queue/active、deferred 下面动辄百万级小文件。
- 日志按小时切分且不清理,一年就是 8760 个文件乘以目录数。
- 缓存目录。我们这次就是这个:一个图片处理服务把缩略图按 md5 前两位建了两级目录,每个目录下几十万个小文件。
- 还有一种最坑的:删了文件但进程还持有 fd,inode 不会释放(这个用 df -h 能看出来,用 df -i 也能看出来,但 lsof 才找得到是谁)。
这次的具体情况
$ find /data/cache -type f | wc -l
29814332
三千万个文件,平均大小 9KB,总共 260GB。业务上这些缓存本来有 7 天过期,清理脚本用的是:
find /data/cache -type f -mtime +7 -delete
看起来没问题,但有两个漏洞:
1. 只删文件不删空目录。每个 md5 前缀目录即使清空了也还在,目录本身也占 inode。三千万文件对应大约 6.5 万个目录,这部分不是主因,但会累积。
2. 更严重的是清理速度跟不上产生速度。业务高峰期每秒新建 300 个文件,清理脚本单线程每秒只能删 200 个,净增长。周末流量翻三倍,两天就攒了几百万。
怎么快速定位是哪个目录
# 按 inode 数量排序,找出贡献最大的目录
for d in /data/*/; do echo "$(find "$d" -xdev -type f 2>/dev/null | wc -l) $d"; done | sort -rn | head
这个命令在千万级文件上会很慢(我们跑了 20 分钟)。更快的办法是 du 的 --inodes:
du --inodes -d 2 /data 2>/dev/null | sort -rn | head -20
du --inodes 走的是目录遍历但不 stat 每个文件,快一个数量级。xfs 上不支持 --inodes,得用 xfs_quota 或者直接 find 分批统计。
第二种情况:文件删了 inode 没释放
如果 df -i 显示 IUsed 很高,但 find 数出来的文件数远小于 IUsed,那就是有进程持着已删除文件的 fd:
$ lsof +L1 | head
java 12345 app 312r REG 253,17 10485760 0 1234567 /data/logs/app.log (deleted)
这种情况 df -h 也会显示空间被占,两个指标一起异常。处理方式是把持有 fd 的进程重启,或者清空 /proc/<pid>/fd 里对应的句柄(不推荐,容易把进程搞崩)。我们之前遇到过一次是 logrotate 用了 copytruncate,Java 进程的 fd 还指着老 inode,日志一直往"已删除"的文件里写,直到重启才释放。
清理时要注意的三件事
1. 不要用 rm -rf 直接怼百万级目录。rm 是同步的,一个目录几百万文件要跑几个小时,期间 IO 打满。用 rsync 的空目录同步法更快:
mkdir /tmp/empty
rsync -a --delete /tmp/empty/ /data/cache/01/
rsync 的删除是批量 unlink,比 rm 快 3~5 倍。
2. 删之前确认没有进程在用。缓存目录一般没事,但如果是 tmp 目录,可能有正在上传的临时文件,删了会让业务报错。用 lsof +D 检查(慢,慎用)或者按文件 mtime 过滤。
3. 目录本身也要清。批量删完之后:
find /data/cache -type d -empty -delete
注意 -delete 对非空目录会失败并继续,是安全的。
根治
- 扩容 inode。ext4 可以在 mkfs 时用 -i 指定 bytes-per-inode,默认 16384(16KB 一个 inode),改成 4096 就是四倍 inode。但这是格式化时定的,线上改不了,只能换盘重建。xfs 是动态分配 inode 的,没有这个问题,所以新盘我们统一用 xfs。
- 换存储结构。三千万小文件不该放在本地文件系统。我们把缩略图迁到了 OBS,本地只留最近 24 小时的热数据,inode 占用从 100% 降到 12%,而且不再需要自己写清理脚本——桶上配个生命周期规则就行。
- 清理脚本改成多线程 + 限速,并且加一条自监控:每次跑完记录删除数量,如果连续两次删除数小于新建数就告警。
一个值得提前加的监控
node_exporter 的 node_filesystem_files_free 和 node_filesystem_files 两个指标一除就是 inode 使用率,告警阈值建议设在 80%。这个指标比磁盘使用率更早报警——我们这次磁盘才 60%,看起来非常健康,实际上已经写不进任何文件了。
顺带说一句,容器环境里 inode 更容易出问题:多个 Pod 共享节点上的同一个 overlay 文件系统,任何一个容器刷小文件都会把整台节点的 inode 吃光,表现是**同节点上所有 Pod 一起报 No space left**,但每个 Pod 自己看 df 都很正常。这种时候要去节点上执行 df -i,而不是进容器看。
- 点赞
- 收藏
- 关注作者
评论(0)