侧边栏壁纸
  • 累计撰写 135 篇文章
  • 累计收到 2 条评论

Linux 磁盘显示还有空间却报 No space left on device?我排查 inode 和已删句柄踩过的几个坑

2026-9-30 / 0 评论 / 8 阅读
AI 摘要由 AI 生成

生产环境中磁盘空间报满,实际剩余空间充足的问题常因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 的空间浪费往往来自四个隐蔽角落:

  1. 构建缓存(BuildKit cache):每次 docker build 产生的中间层未被回收。
  2. 悬空镜像(Dangling images):新镜像构建后,旧镜像名字变成了 <none>。
  3. 容器默认控制台日志:没有配置 max-size 的容器,所有往 stdout 打印的日志都会无节制写进 /var/lib/docker/containers/<id>/<id>-json.log,单文件跑出十几 G 并不罕见。
  4. 孤儿数据卷:容器被删除但没带 -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 的五步排查清单

下次在生产服务器上遇到磁盘报警,按下面这套流水线顺序排查,基本能在三分钟内定位病因:

  1. 看容量:执行 df -h,确认哪个分区占用达到 100%。如果满了,用 du -ahx / | sort -rh | head -n 20 抓出前二十名大文件。
  2. 看 inode:如果容量有空余,立刻执行 df -i。如果 IUse% 满了,用 find 统计小文件聚集的目录并用 -delete 安全移除。
  3. 看僵尸文件:执行 lsof +L1 | grep deleted,查看是否有大日志文件已被 rm 但仍被活动进程句柄持有,通过重定向清空对应的 /proc/<PID>/fd/<FD>。
  4. 看 Docker 占用:执行 docker system df,查看是否有堆积的未打标镜像、孤儿卷或膨胀的构建缓存。
  5. 看挂载覆盖:如果容量依然对不上,用 mount --bind / /mnt/test 检查挂载点底层是否压着未释放的历史旧数据。

系统不会无缘无故报错。跳过常规表象,把这几层底层机制查一遍,不管是 inode 告急还是句柄未释放,都能快速恢复服务。

评论一下?

OωO
取消