生产环境中磁盘空间报满,实际剩余空间充足的问题常因inode耗尽或未释放的文件句柄导致。本文探讨了如何排查inode和文件句柄问题,包括定位产生大量小文件的目录、使用find命令安全删除文件以及解决删除大文件后空间未释放的情况。
生产环境里让人血压骤升的瞬间不多,磁盘突然报警绝对算一个。
某天下午核心接口突然大面积报 500,登录服务器看错误日志,清一色写着 OSError: [Errno 28] No space left on device。
第一反应就是敲个命令看磁盘占了多少:
df -h
屏幕上输出的结果却让人愣住了:根目录 /dev/vda1 挂载点使用率只有 56%,可用空间还有将近 30GB。
明明还剩几十个 G 的存储,系统为什么硬咬着说设备上没有空间?
这种磁盘明明显示有空余、写操作却直接撞墙的诡异情况,在 Linux 运维中其实相当常见。只是它的根因通常不在直观的字节容量上,而是隐藏在 inode 耗尽、未释放的已删除文件句柄、或者是挂载层被遮蔽等深水区。
把当时排查出来的几个关键暗坑理清楚,下次遇到类似报错两分钟就能定位。
空间还有几十个 G,但 inode 节点已经被小文件打满 100%
很多新手只知道磁盘按兆(MB)或者吉(GB)来计算大小,容易忽略 Linux 文件系统的另一个核心指标:inode。
在 ext4 或者 xfs 文件系统里,每个文件和目录在创建时不仅要在数据块里存实际内容,还需要消耗一个 inode 结构体来保存文件的权限、大小、所有者和物理块指针。
关键在于:磁盘格式化时分配的 inode 总量是固定的。哪怕一个文本文件里面只有一个字节,它也必须占掉一个独立的 inode。
敲一下这个命令:
df -i
输出立刻暴露了问题真相:
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vda1 3276800 3276800 0 100% /
IUse% 显示 100%,可用 inode 刚好归零。也就是说,磁盘上的数据块空间还剩一半多,但系统已经再也创建不出哪怕一个新文件的户口本了。
怎么揪出到底是哪个目录在疯狂生小文件?
通常都是应用无节制产生的临时缓存、未清理的 session 文件、或者系统定时任务发送内部邮件塞满了邮件队列。
用下面这行统计脚本,从根目录开始层层扫描目录下子文件最多的路径:
for d in /*; do
if [ -d "$d" ]; then
echo -n "$d: "
find "$d" -maxdepth 2 2>/dev/null | wc -l
fi
done | sort -k2 -nr
顺着数字最大的目录一层层追查下去,我最终定位到了 /var/spool/postfix/maildrop。因为服务器某个 crontab 脚本一直有标准输出,系统默认把每次执行结果都给本地 root 用户寄封邮件,偏偏机器又没配邮件服务,久而久之里面堆积了上百万个几字节大小的未送达邮件碎片。
清理海量小文件时千万别直接 rm -rf *
当一个目录里塞了几十万上百万个小文件时,如果你直接进目录执行 rm -rf *,shell 大概率会报错:
-bash: /bin/rm: Argument list too long
这是因为通配符 * 会在传递给 rm 之前由 bash 展开成全部文件名列表,直接超出了 Linux 单个命令行参数的内存上限(ARG_MAX)。
安全又迅速的清理姿势是用 find 配合内置的 -delete 指令:
find /var/spool/postfix/maildrop -type f -delete
这条命令不经过 shell 参数展开,直接由内核按目录项流式删除,几分钟就能平息百万级小文件灾难,inode 瞬间恢复健康。
用 rm 删了几个 G 的大日志,可用空间却纹丝不动
这是另一个极其经典的场景。
业务机器磁盘满了,运维上去找到应用的大日志文件 access.log,一看有 15GB,抬手就是一个:
rm -f /data/logs/access.log
删完之后立刻执行 df -h 准备交工,却发现磁盘使用率没有下降哪怕 1%。
这是因为 Linux 的文件删除机制包含两个计数器:硬链接数(i_nlink)和打开引用计数(i_count)。只有当这两个计数同时降为 0 时,内核才会真正释放底层数据块。
你用 rm 只是删掉了目录项里的文件名,把硬链接数减到了 0。但如果后端的 Java、Python 或者 Nginx 进程还在一直开着该文件进行写入,进程持有的文件描述符依然在占用空间。
找出吃着空间不放的僵尸句柄
执行这行命令,一秒钟抓出所有被标记为已删除但仍在被进程霸占的大文件:
lsof +L1 | grep deleted
或者:
lsof / | grep '(deleted)' | sort -k7 -nr | head -n 10
输出通常长这样:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
python3 4812 root 3w REG 253,1 15728640000 0 1835012 /data/logs/access.log (deleted)
可以看到 PID 为 4812 的进程依然牢牢抓着这 15GB 的日志。
怎么在不重启生产业务的前提下立刻释放空间?
很多新手以为只能暴力 kill -9 杀掉进程。如果这是线上高并发的核心服务,贸然重启可能引发请求震荡。
优雅的解决办法是直接清空该进程对应的文件描述符:
> /proc/4812/fd/3
这里的 4812 是进程 PID,3 是上面 lsof 输出里的文件描述符数字(FD)。通过重定向把空内容刷进去,内核会立即将底层数据块截断为 0,df -h 上的可用空间立刻释放,业务进程甚至不会感知到任何中断。
为了避免以后重蹈覆辙,清空正在写入的日志文件时,永远不要用 rm,直接使用截断命令:
> /data/logs/access.log
# 或者
truncate -s 0 /data/logs/access.log
Docker 的 overlay2 目录默默吞掉几十个 G 存储
用 Docker 跑微服务的服务器,如果长时间不维护,根目录往往会被 /var/lib/docker/overlay2 塞爆。
很多人习惯性以为只要容器没几个就不用管,实际上 Docker 的空间浪费往往来自四个隐蔽角落:
- 构建缓存(BuildKit cache):每次
docker build产生的中间层未被回收。 - 悬空镜像(Dangling images):新镜像构建后,旧镜像名字变成了
<none>。 - 容器默认控制台日志:没有配置
max-size的容器,所有往 stdout 打印的日志都会无节制写进/var/lib/docker/containers/<id>/<id>-json.log,单文件跑出十几 G 并不罕见。 - 孤儿数据卷:容器被删除但没带
-v参数,遗留下孤立数据卷。
排查 Docker 真实占用分布的第一步,是执行内置的诊断命令:
docker system df
它会把镜像、容器、本地数据卷以及构建缓存占用的空间清清楚楚列成表格,并标明可回收(RECLAIMABLE)的容量比例。
安全清理孤立资源:
# 清理所有悬空镜像和停止的容器
docker system prune -f
# 如果想连无用的构建缓存一起清理
docker builder prune -f
要从根源上防止容器日志把磁盘撑爆,在 /etc/docker/daemon.json 里加上全局日志轮转限制:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
配置后执行 systemctl reload docker,单个容器的控制台日志最多保留 3 个 50MB 的归档,再也不会野蛮生长。
挂载新盘覆盖了原目录,旧数据被隐身吃掉
这是一个极为隐蔽却经常坑苦老工程师的物理暗坑。
某天运维给服务器加了一块 500GB 的数据盘,把新盘挂载到了 /data 目录:
mount /dev/vdb1 /data
但很多时候大家没注意到,在挂载新盘之前,宿主机根分区原来的 /data 目录下其实早就由前面的开发人员解压了 20GB 的安装包或历史数据。
当新磁盘一挂载上去,文件系统就把原目录完全覆盖了。此时你敲 du -sh /data,查看到的只是新磁盘上的内容;但原本那 20GB 依然躺在根分区的磁盘块里,持续消耗着根分区的空间,任你在新目录里怎么翻找都找不到。
怎么透视被遮蔽的底层旧数据?
利用 Linux 提供的目录绑定挂载(bind mount),把根目录镜像到一个临时目录:
mkdir -p /mnt/root_inspect
mount --bind / /mnt/root_inspect
此时打开 /mnt/root_inspect/data,看到的就是未被新磁盘遮挡的原始目录结构。
如果里面确实有遗留的老旧垃圾文件,直接在临时挂载点里清理干净,最后解绑:
umount /mnt/root_inspect
rmdir /mnt/root_inspect
根分区被幽灵般占用的几十个 G 就这样原形毕露、轻松找回。
遇上 No space left on device 的五步排查清单
下次在生产服务器上遇到磁盘报警,按下面这套流水线顺序排查,基本能在三分钟内定位病因:
- 看容量:执行
df -h,确认哪个分区占用达到 100%。如果满了,用du -ahx / | sort -rh | head -n 20抓出前二十名大文件。 - 看 inode:如果容量有空余,立刻执行
df -i。如果IUse%满了,用find统计小文件聚集的目录并用-delete安全移除。 - 看僵尸文件:执行
lsof +L1 | grep deleted,查看是否有大日志文件已被rm但仍被活动进程句柄持有,通过重定向清空对应的/proc/<PID>/fd/<FD>。 - 看 Docker 占用:执行
docker system df,查看是否有堆积的未打标镜像、孤儿卷或膨胀的构建缓存。 - 看挂载覆盖:如果容量依然对不上,用
mount --bind / /mnt/test检查挂载点底层是否压着未释放的历史旧数据。
系统不会无缘无故报错。跳过常规表象,把这几层底层机制查一遍,不管是 inode 告急还是句柄未释放,都能快速恢复服务。
文章标题:Linux 磁盘显示还有空间却报 No space left on device?我排查 inode 和已删句柄踩过的几个坑
文章链接:https://llbbs.cn/jishujaocheng/196.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?