服务器入坑指北:磁盘还有空间却写不进去
服务半夜开始报错,日志里躺着同一句
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 依然能完成前六步分流——所以装包是可选动作,不是排障的前提。
八、安全清理的顺序:从最安全到最危险
清理的原则是"先取证,再按危险程度从小到大动",顺序如下:
- 取证:第七节的命令跑一遍,输出留档。
- 日志:systemd 系可以用
journalctl --vacuum-size=<按需定>或--vacuum-time=<按需定>回收日志;应用自己的日志按它自带的轮转机制处理。边界很清楚:只动日志,不动业务数据。 - 包管理器缓存:只清下载下来的包文件与元数据缓存,不删已安装的软件。
- 应用自身的缓存目录:动手前先确认这个目录归谁、用途是什么、能不能重建——删错了往往会换来一次更难查的故障。
- 业务数据目录:最后才考虑,且必须先确认有备份或可重建。
# 第 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就收工:没在报错路径上复现原操作,等于没验证。
- 点赞
- 收藏
- 关注作者
评论(0)