2KB写入因No space left on device失败,df -h却显示仍有GB空闲

2KB写入因No space left on device失败,df -h却显示仍有GB空闲

2KB的写入因No space left on device失败,而df -h显示仍有数GB空闲,我花了48小时反复删除日志才发现inodes早已耗尽。这份现场笔记记录了误导我的命令和现在的检查清单。

文件系统在分配资源时同时管理块和inode。块用于存放实际数据,inode则存放文件的元数据,包括权限、时间戳和数据块指针。当一个文件系统创建时,系统会预先分配固定数量的inode。这个数量通常在格式化时确定,后期难以动态扩容。

df -h只报告块的使用情况。它显示可用空间时,许多人直接判断磁盘不紧张。但如果inode已经用完,即使还有大量空闲块,任何需要创建新文件或目录的操作都会立即失败,报No space left on device。系统无法为新文件分配inode,就无法完成写入。

这个误导在日志密集型场景特别常见。开发者看到df -h还有空间,就本能地去清理大日志文件,却忽略了真正卡住的是inode数量。48小时的反复删除正是因为一直盯着错误指标。

df -h空闲不等于可以创建新文件

df -h命令输出中显示的可用空间指的是数据块剩余量。它不反映inode的使用状态。当一个分区格式化时,系统会根据分区大小按比例分配inode。例如一个1TB分区可能预分配数百万个inode,但具体数量取决于mkfs时的参数。

一旦inode耗尽,哪怕只剩90%的块空间,新文件的创建也会失败。2KB的JSON写入需要一个新的inode来存放元数据,系统找不到空闲inode就直接拒绝操作。这就是为什么df -h显示还有数GB空闲却无法写入。

很多开发者第一次遇到这个问题时都会困惑。他们运行df -h,看到可用空间充足,就开始排查磁盘配额、权限或进程锁。但这些都不是根源。根源是inode池已经枯竭。

这个现象在容器化和微服务环境中更突出。每个Pod或容器可能产生大量临时文件,快速消耗inode却不占用太多实际空间。df -h看起来健康,实际系统已经无法创建新文件。

小文件sidecar直接耗尽inode而非磁盘空间

我当时正在迭代一个Python worker。它在每次运行后都会在目录下生成一个JSON sidecar文件。这些文件很小,通常只有几百字节到2KB,却每个都对应一个独立的inode。

worker持续运行,不断dump新的JSON sidecar。每个sidecar都是一个独立文件,需要占用一个inode。几万次迭代后,inode数量就迅速耗尽,而实际占用的磁盘空间仍然很小。

小文件是inode杀手。相比大日志文件,小文件数量多,每个都需要单独的inode和目录项。Python worker这种“每次运行写一个sidecar”的模式特别容易触发这个问题。它不是把数据追加到已有文件,而是不断创建新文件。

生产环境中类似场景很多。监控agent生成的大量metrics文件、临时缓存的session文件、CI/CD流水线产生的报告,都以小文件形式存在。它们不吃磁盘空间,却能快速吃光inode。

删除文件后inode未释放的真实原因

很多人以为删掉日志文件就能释放inode。但实际情况更复杂。文件删除时,inode只有在硬链接数为0且没有进程打开该文件时才会被真正释放。

日志清理的常见坑在于只删内容不删文件。一些脚本用truncate -s 0 logfile或echo > logfile清空内容,但文件本身还在,inode没有释放。进程如果还打开着这个文件,即使你删除了文件名,inode也依然被占用,直到进程退出或关闭文件句柄。

我花了48小时反复删除日志,正是因为很多日志文件被正在运行的进程持有。rm命令删除了目录项,但inode计数没有下降。lsof命令能看到这些被删除但仍被进程引用的文件,它们以 (deleted) 状态存在,却继续占用inode。

这个机制保护了正在写入的日志不被意外破坏,但也制造了排查陷阱。清理脚本如果不重启相关进程或使用lsof排查,就无法真正释放inode。

必须同时用df -i和ls检查inode使用率

正确的诊断必须同时检查块和inode。df -i命令直接显示每个文件系统的inode使用率。当IUse%接近100%时,即使Use%很低,也会触发No space left on device。

ls命令配合-i参数可以查看具体文件的inode号,帮助定位哪些目录下文件数量最多。find /path -xdev -type f | wc -l能统计某个分区下的文件总数,快速找到inode消耗大户。

我最终发现问题时,正是同时运行了df -i和df -h。对比两个输出才意识到inode已经100%而块空间还有余量。之前只看df -h的习惯直接把我带偏了48小时。

生产系统中应该把这两个命令做成日常检查的一部分。脚本可以这样写:

1
2
3
4
5
df -h

df -i

find /var/log -xdev -type f | wc -l

这样能第一时间发现是块还是inode出了问题。

日志清理脚本应加入inode阈值判断

生产环境的日志清理脚本不能只看磁盘使用率。必须同时判断inode使用率,并在达到阈值时触发更激进的清理策略。

一个合理的脚本应该先用df -i检查,如果inode使用率超过85%,就不仅删除旧日志,还要查找并清理小文件密集目录。可以使用find命令按文件数量或按最后修改时间清理。

清理时要考虑进程持有问题。脚本可以先发送信号让应用滚动重启,释放被持有的已删除文件句柄,或者使用lsof +L1找出被删除但仍占inode的文件并处理。

经验表明,只删大文件不处理小sidecar的脚本效果很差。Python worker这类应用产生的海量小JSON文件才是主凶。清理策略需要区分文件大小,对小于10KB的文件采用更短的保留周期。

为inode使用率单独建立监控告警

避免重复踩坑的最好办法是为inode使用率建立独立监控。Prometheus可以采集node_filesystem_inode_free和node_filesystem_inode_files指标,Grafana面板上单独画一条inode使用率曲线。

告警规则建议设置两个阈值:85%警告,95%紧急。告警信息里要明确指出是inode问题,而不是笼统地说磁盘满。这样运维人员就不会再花几十小时盯着df -h排查。

除了系统级监控,应用层也应该暴露自己产生的文件数量指标。Python worker可以统计sidecar生成速率,当速率异常升高时提前告警。

监控系统还应该追踪每个挂载点的文件数增长趋势。突然的斜率变化往往预示着某个新部署的服务在疯狂创建小文件。

事后checklist能把排查时间从48小时压到分钟级

现在我每次遇到No space left on device,第一步不再是删日志,而是按固定checklist走。

清单第一条:同时运行df -h和df -i,对比Use%和IUse%。

第二条:如果IUse%高,用find找出文件数最多的目录。

第三条:用lsof +L1列出被删除但仍被占用的文件,确认是否有进程持有。

第四条:检查最近变更的应用是否在产生大量小文件,特别是JSON、临时缓存或日志sidecar。

第五条:清理前先评估是否需要重启相关进程释放inode。

这个checklist把我后续的类似问题排查时间从48小时压到几分钟。它把经验转化成了可重复的步骤,对中文开发者尤其是运维同学特别实用。很多团队还在只看df -h,踩同样的坑。

把这份清单加入团队的故障处理SOP,能显著降低因inode耗尽导致的故障时长。

参考来源