服务器入坑指北:磁盘还有空间却写不进去

举报
Tulpartner 发表于 2026/10/19 09:44:05 2026/10/19
【摘要】 本文针对“磁盘还有空间却报 No space left on device”给出排障指北。核心是先分流再动手:用 df -h、df -i、findmnt -T、lsof +L1、tune2fs -l 等只读命令,判断是数据块、inode、已删除仍被持有、ext/XFS 保留块、挂载点或 tmpfs、inotify 上限,还是配额 EDQUOT 等原因。确认后按日志、包缓存、应用缓存、业务数据的顺序

服务半夜开始报错,日志里躺着同一句 No space left on device。登上去 df -h 一看,根分区还剩几十个 G——于是多数人的下一步是翻大文件删。删完再试,还是报错;有时更邪门:空间确实回来了一点,过一会儿又满了。
这句报错的麻烦之处在于,它是内核在"这个写操作没法满足"时给出的一句话,而"没法满足"的原因至少有七种:数据块用完、inode 用完、文件早被删掉但还被人拿着、给 root 留的保留块在起作用、报错路径根本不在你以为的那个盘上、路径落在按内存算额度的 tmpfs 上,以及跟磁盘完全无关的 inotify watch 上限。
这篇的顺序是先分流再动手:先用只读命令判断属于哪一类,再决定清什么、清哪里,最后用一张清单确认真的解决了。文中的数值都是示例,保留块比例这类值请以 tune2fs -l 现读为准。

一、先分流:七种成因的判定顺序

write() 失败时返回的 ENOSPC(errno 28)只是一个结果码,内核不会告诉你缺的是哪一类资源。所以顺序很重要——先判断是哪一类,再动手。

顺序 怀疑对象 命令(只读) 判据 命中后去哪
1 数据块耗尽 df -h <报错路径> Use% 到 100%,或 Avail 为 0 第二节、第八节
2 inode 耗尽 df -i <报错路径> IUse% 到 100%,IFree 为 0 第二节
3 已删除但仍被持有 lsof +L1(或读 /proc/<pid>/fd) 出现体积很大、指向 (deleted) 的项 第三节
4 保留块 / 权限差异 tune2fs -l <设备> | grep -i reserved df 报满,root 还能写、普通账号不能 第四节
5 挂载点选错 df <报错路径>、findmnt -T <报错路径> 该路径所在的文件系统不是你以为的那个 第五节
6 tmpfs 额度 findmnt -T <报错路径> 文件系统类型是 tmpfs 第五节
7 不是盘的 ENOSPC cat /proc/sys/fs/inotify/max_user_watches df 与 df -i 全绿,报错出现在保存文件/热重载时 第六节

顺序反了要付什么代价:直接去删大文件,如果真因是 inode,你腾出了数据块,但建新文件依旧失败(每个新文件都要占一个 inode);如果真因是"已删除但仍被持有",你删掉的是别的文件,被占住的空间一点没还;如果真因是挂载点选错,你删的是另一块盘上的数据,报错的盘毫无变化。三种情况都会让你在错误的方向上耗掉整个故障窗口,而删除是不可逆的——恢复只能靠备份。

二、df -h 与 df -i:块和 inode 是两件事

一个文件系统有两类彼此独立的资源:数据块(存文件内容)和 inode(存"文件的元信息,以及指向数据块的指针")。每创建一个新文件,至少消耗一个 inode;两者各自有上限。
所以 inode 先耗尽时,会出现最难理解的现象:df -h 显示还有几十 G,但新文件一个也建不出来,报错文字与块耗尽一模一样。此时 df -i 的 IUse% 已经是 100%。
典型制造者是"文件很多但都很小"的场景:会话文件目录、邮件队列、应用缓存、依赖树(一个前端项目几十万个文件很常见)、缩略图目录。定位分两步看:

  • df -i <报错路径> 确认 inode 是否真的到顶;
  • find <目录> -xdev -type f | wc -l 数文件个数(-xdev 避免跨到别的文件系统),配合 du -x -d1 <目录> | sort -h 看体积排名。注意 du 对"海量小文件"不敏感——它们体积小、个数多,必须单独数。
    处置方向是减少文件个数:轮转、归档、清理可重建的缓存。这里有个与下一节相同的坑:删掉一个已被打开的文件,inode 也不会立刻回收。

三、删了文件,空间为什么没回来?

机制在两层:unlink 做的事只是把目录项摘掉、把 inode 的链接计数减到 0;inode 与它指向的数据块,要等最后一个打开的 fd 关闭之后才被回收。只要还有进程拿着那个句柄(正在写的日志、打开的数据库文件、被 tail -f 盯着的文件),空间就一直被占着,df 也不会变。
取证(都是只读的):

  • 装了 lsof:lsof +L1 会列出链接计数小于 1 的已打开文件——正是"已经 unlink、但仍被持有"的那一批;
  • 没装 lsof:直接读 /proc,ls -l /proc/*/fd 2>/dev/null | grep deleted,或按链接找 find /proc/*/fd -lname '* (deleted)'。findmnt、df 与 /proc 都不需要额外装包,所以装 lsof 是可选项,不是必需项。
    判据:lsof +L1 里出现 SIZE 很大的项,且它所属的进程正是你怀疑的那个服务。
    取舍必须说清楚:删文件 ≠ 释放空间。正确的动作是让持有者重新打开文件或正常退出——日志类场景先做日志轮转,再让服务 reload(重新打开日志句柄);数据库类场景按它自己的维护方式处理。重启整机是最后手段:它会释放句柄,但也把现场(谁拿着、拿了多久)一起抹掉,下次复发你还得从零查。

四、为什么 root 还能写:保留块(ext)与 XFS 的另一套机制

df 报 100%,root 却能写进去——这是 ext 家族文件系统的保留块在起作用。mkfs.ext4 的 -m 选项设置"预留给特权进程的数据块百分比",默认是 5%,设计目的是让 root 拥有的守护进程在普通账号写不进去之后仍能正常工作。查看与调整(都在 e2fsprogs 里):

  • 查看:tune2fs -l <设备> | grep -i reserved,关注 Reserved block count: 与 Reserved blocks uid/gid;
  • 调整:tune2fs -m <百分比> <设备>。
    关键点是 df 不计入保留块,所以它的 Size 与 Used 之间会留出一段差值,表现为"账面上 100%,实际还能写一点"。别照抄 5% 这个数——现读你机器上的实际值;它随文件系统大小与建立方式而变。
    XFS 不是这个机制,千万不要套用。XFS 没有"按百分比留给 super-user"的旋钮,它的对应机制是特权事务预留:小于 640 MB 的文件系统最多预留 5% 的数据块,640 MB 及以上固定预留 8192 块;此外,如果启用了可选特性(reflink、rmapbt 之类),这些特性的 btree 还会另外预留空间。所以在一台 XFS 机器上看到"df 和 ls 的账对不上、新建文件比预期更早失败",应当按"固定块数的预留 + 可选特性预留"去解释。
    判据:先用 findmnt -T <路径> 看清文件系统类型,再决定用哪一套解释。处置方向上,这段余量是设计上的安全垫,通常不该动;确实要调整,先把"root 在极端情况下还需要多少余量"算清楚,并放在维护窗口做——这是会改变系统兜底能力的操作。

五、你删的可能不是你以为的那个盘:挂载点与 tmpfs

df -h 给出的单位是文件系统,不是目录。同一台机器上,根分区、数据盘、/var 独立分区、/tmp 各有各的可用空间;报错路径落在哪一个上,必须现查:

  • df <报错路径>:看这条路径所在的文件系统与可用量;
  • findmnt -T <报错路径>:看挂载点与文件系统类型。
    判据很直接:如果 df <报错路径> 与 df / 输出的不是同一行,那么"根分区还有空间"这个结论跟本次报错无关——你该看的是那一行。
    容器与命名空间里还有一个额外陷阱:容器内看到的设备与可用量,与宿主上看到的往往不是一个口径;容器里那块可写层的余量,取决于它底层实际落在哪个文件系统上。这里只讲"口径不同、要各自查",容器侧的配置项不在这篇的范围内(docker 配置优化那篇写过)。
    tmpfs 是最容易被忽略的一类:/tmp 与 /dev/shm 在很多发行版上是 tmpfs,容量按内存计而不是按盘算,所以"盘还很空"对它毫无意义。判据:findmnt -T <报错路径> 的 FSTYPE 是 tmpfs;容器里的 /dev/shm 默认额度很小(64 MB 量级),共享内存不够时同样报这句错。定位到这一层之后,要处理的是"谁把这块内存文件系统写满了",而不是去清磁盘。

六、两种"不是盘满"的 ENOSPC

配额超限是另一个 errno。 账号、组、项目三类配额超限时返回的是 EDQUOT,文案是 Disk quota exceeded,与 ENOSPC(No space left on device)是两个不同的错误码。这个边界很重要:配额满了去删文件,等于对着错误的资源使劲,而且往往删的是别人的数据。判据:先看报错原文是哪一句——如果是 Disk quota exceeded,df 很可能还空着。进一步确认用 quota、repquota、xfs_quota 这类工具(配额分数据块与 inode 两类上限,输出里会用 + 之类的标记区分);配额怎么配是另一个话题,这里只指路。
inotify 的 ENOSPC 与磁盘完全无关。 inotify_add_watch() 在 watch 配额用完时返回的也是 ENOSPC,上层库把它渲染成同一句 No space left on device。它的现象有辨识度:报错出现在保存文件、热重载、启动某个监视文件的进程时,而 df -h 与 df -i 全绿;判据是 cat /proc/sys/fs/inotify/max_user_watches 看到的每账号上限。限额怎么调是另一篇的主题(文件描述符与 inotify 上限),这篇只给这一句分流指路——认出它,你就不会去删磁盘上的文件。

七、只读取证命令组

动手之前先把现场留下来。下面这些命令都不改变系统状态:

df -h <报错路径>                        # 块:哪个文件系统、还剩多少
df -i <报错路径>                        # inode:是不是也到顶了
findmnt -T <报错路径>                   # 路径落在哪个挂载点、什么类型
lsof +L1                                # 已 unlink 但仍被打开的文件
tune2fs -l <设备> | grep -i reserved    # ext:保留块现状(现读,不要照抄)
du -x -d1 <目录> | sort -h              # 目录体积前几名
find <目录> -xdev -type f | wc -l       # 文件个数(对付海量小文件)

判据:每条输出对应第一节分流表里的一行;把结果连同时间一起记下来,清理之后要拿它对比。
工具缺失时的替代路径:没有 lsof 就直接读 /proc/<pid>/fd(grep deleted 或按链接找 (deleted));没有 e2fsprogs 就没有 tune2fs,此时 df、findmnt、/proc 依然能完成前六步分流——所以装包是可选动作,不是排障的前提。

八、安全清理的顺序:从最安全到最危险

清理的原则是"先取证,再按危险程度从小到大动",顺序如下:

  1. 取证:第七节的命令跑一遍,输出留档。
  2. 日志:systemd 系可以用 journalctl --vacuum-size=<按需定> 或 --vacuum-time=<按需定> 回收日志;应用自己的日志按它自带的轮转机制处理。边界很清楚:只动日志,不动业务数据。
  3. 包管理器缓存:只清下载下来的包文件与元数据缓存,不删已安装的软件。
  4. 应用自身的缓存目录:动手前先确认这个目录归谁、用途是什么、能不能重建——删错了往往会换来一次更难查的故障。
  5. 业务数据目录:最后才考虑,且必须先确认有备份或可重建。
# 第 2 步:回收系统日志(按需给上限,示例值)
journalctl --vacuum-size=<如 500M>
# 第 3 步:清包管理器缓存(只清缓存,不动已安装的包)
apt-get clean          # Debian / Ubuntu
dnf clean all          # RHEL 系

两条硬性禁忌:

  • 清理之前不要先重启。 重启会抹掉现场(进程、fd、/proc 里的证据),而且对"已删除但仍被持有"这类问题只是把症状盖住:句柄确实释放了,但你永远不知道是谁拿的、为什么会拿住,下次照样复发。
  • 不要 rm 你不认识的目录。 它可能是一个挂载点——删挂载点下面的内容,等于删另一块盘上的数据;也可能是一个正在运行的服务的工作目录。

九、改完怎么验证:每项一条判据

做完清理之后,按下面这些验证方式逐项确认,别只看 df 一眼就收工:

df -h <报错路径>      # 判据:Avail 上升,或至少不再是 0
df -i <报错路径>      # 判据:IFree 不再为 0
lsof +L1              # 判据:不再有可疑大项(没有 lsof 就查 /proc/<pid>/fd)
printf 'ok\n' > <报错路径>/.write-test && rm -f <报错路径>/.write-test   # 判据:能创建一个新文件并写进去

然后:

  • 服务层面:应用的日志里不再出现 No space left on device(注意是同一路径、同一操作不再报错,而不是"日志里暂时看不到");
  • 保留块:tune2fs -l <设备> | grep -i reserved 的数值没有被动过(除非你是有意调整);
  • tmpfs:如果报错发生在 tmpfs 上,确认占用它的进程或文件已经释放,并再次用 findmnt -T <路径> 确认它确实是 tmpfs——它的额度按内存算,磁盘余量帮不上忙。
    判据统一成一句:在报错的那个路径上,用出错的同一类操作再来一次,成功即为通过。

十、这篇不解决什么

  • 挂载怎么写不在这一篇里:fstab 怎么写、nofail 怎么加,属于《从连上到跑通》那篇;这篇只解决"这条路径落在哪个挂载点"的判定。
  • 容器侧的配置键不在这里重复:容器日志上限、共享内存大小这类键,docker 配置优化那篇已经讲过;这篇只在"容器口径与宿主不同"处一句带过。
  • 不展开 inotify 限额调优:那是"文件描述符与 inotify 上限"那篇的主题;这篇只负责让你认出它、不走错方向。

十一、踩坑清单

  • 只看 df -h 不看 df -i:inode 满了照样建不出文件,报错文字一模一样。
  • 把 ext 的保留块解释套到 XFS:XFS 没有百分比旋钮,它是固定块数的特权事务预留,外加可选特性的 btree 预留。
  • 删了文件以为空间会回来:还有进程拿着 fd,空间就不会回收。
  • 出问题先重启:现场没了,下次复发只能从零查;对"已删除仍被持有"也只是掩盖症状。
  • 拿配额问题当盘满处理:EDQUOT 与 ENOSPC 是两个 errno,删文件解决不了配额超限。
  • 忽略 /tmp、/dev/shm 是 tmpfs:容量按内存算,跟数据盘余量无关。
  • 以为容器里 df 的结论就是宿主的结论:两边设备与可用量常常不是一个口径。
  • 把 inotify 的 ENOSPC 当成盘满:磁盘完全没参与,删文件纯属白干。
  • 清理时删掉不认识的目录:它可能是挂载点,删掉的是另一块盘上的数据。
  • 只按 ls -l 的大小找大文件:预分配或带空洞的文件(fallocate 造出来的那类)与 du 的口径不一致,账对不上。
  • du 不加 -x 跨文件系统统计:把别的挂载点算进来,排名和结论都会错。
  • 直接删掉正在写的日志文件:文件被 unlink 但句柄还在,空间不回收;正确做法是轮转 + 让服务重新打开。
  • 保留块比例随手调小:它是极端情况下的兜底余量,调之前先把代价算清楚。
  • 清完只看一眼 df 就收工:没在报错路径上复现原操作,等于没验证。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。