磁盘明明还有空间,测试报告为什么就是写不进去?

0 阅读7分钟

回归跑完了,最后一步生成报告却失败了。

日志提示 No space left on device。你打开容量监控:磁盘还有空间。重新执行,仍然写不进去。于是开始怀疑报告插件,甚至怀疑刚接入的 AI 分析模块是不是生成了异常内容。

先停一下: “还能存多少字节”和“还能创建多少文件”,不是同一个问题。

测试平台尤其容易踩到这条边界。每条用例产生 JSON、截图、日志、附件索引,单个文件不大,数量却可能持续增长。

本文用教学场景讲排查方法,不声称某家公司发生过这次事故。所有诊断应在获准的测试环境执行;不需要把共享磁盘真的写满,也不需要先清空目录。

第一件事不是删文件,而是确认你看的是同一块盘

报告最终写到哪里?容器里的临时目录、挂载的数据卷、宿主机工作区,可能属于不同文件系统。看着宿主机根分区有余量,不能证明报告目录也有。

下面命令以已存在的报告目录 /srv/test-reports 为例,路径要替换为你的实际目录:

df -hT /srv/test-reports
df -i /srv/test-reports

第一条看该路径所在文件系统的数据块使用情况和类型;第二条看 inode 信息。它们都是只读诊断。

在具有 inode 数量限制的文件系统上,新建普通文件需要可用 inode。大量小文件可能先耗尽这类资源,而不是先耗尽字节容量。目录也会消耗资源,所以估算时不要只数截图。

但不能反过来说“所有文件系统都预先分配固定数量的 inode”。不同文件系统、网络卷、容器存储驱动的统计和分配方式不同;如果某项统计不可用,要标为未知,继续查底层卷和配额,而不是把零值机械解释成已经耗尽。

这里有一个实用排查原则:同一路径、同一执行身份、同一时刻,比较字节、inode 与配额。 脱离其中任何一个条件,都可能看到一张无关的“绿色图”。

同样是写失败,错误码不能混着治

ENOSPC 指向可分配空间问题;在具体文件创建场景中,可能需要继续区分数据块、inode 等限制。EDQUOT 指向配额。即使全盘还很空,某个用户或项目也可能已经达到自己的限额。

还有另一类常被混在一起的失败:EMFILE 表示进程打开的文件描述符达到限制,ENFILE 表示系统层面的打开文件数量限制。它们不等于“磁盘存满了”。只删旧报告,不修复未关闭的文件或连接,通常治不到根上。

此外还有权限不足、只读挂载等情况。应保留实际错误码、出错操作、目标路径和运行身份,别把所有异常统一改写成“报告生成失败,请重试”。

四万条用例不算多,留存文件却可能达到百万级

假设一个教学平台每轮运行 40,000 条用例,每条平均留下 6 个独立文件,保留最近 12 轮。

只算这些用例产物,就是:

40,000 × 6 × 12 = 2,880,000 个文件

这不是实际 inode 消耗的精确测量,也没有计入目录、汇总产物、临时文件和并行执行时的新一轮数据。它的作用是提醒团队:只给截图计算 MB、只给报告设置保留天数,还不足以形成容量预算。

预算至少同时约束四项:每轮字节数、每轮新增文件数、峰值并行轮次、保留策略。失败任务和中断任务也要有回收规则,否则“只清理已完成报告”会留下越来越多孤儿目录。

把小文件归档,可以减少长期保留的独立文件数量,但归档过程中仍可能需要原文件和新压缩包同时存在。必须估算峰值空间、确认归档可读,再按受控清单回收源文件;不能把“打包”当成没有成本的即时救命按钮。

可以先做容量预检,但别把它当成预留成功

下面是一个只读预检的最小实现,适合挂在测试平台的产物生成前。它不会创建海量文件,也不会清理现有目录。

import os

def capacity_check(path, planned_bytes, planned_files):
    if any(type(x) is not int or x < 0
           for x in (planned_bytes, planned_files)):
        raise ValueError("预算必须是非负整数")
    stat = os.statvfs(path)
    available_bytes = stat.f_bavail * stat.f_frsize
    # 有些文件系统不提供有意义的 inode 总量。
    available_inodes = stat.f_favail if stat.f_files > 0 else None
    return {
        "available_bytes": available_bytes,
        "available_inodes": available_inodes,
        "bytes_enough": available_bytes >= planned_bytes,
        "inodes_enough": (
            None if available_inodes is None
            else available_inodes >= planned_files
        ),
    }

f_bavail 与 f_favail 使用面向非特权用户的可用量字段,比直接把所有空闲量都算到任务头上更谨慎;但这些字段仍不能代替所有用户配额、项目配额或容器限制的检查。

传入的预算应该已经包含安全余量和目录等额外需求。inodes_enough 为 None 表示未知,不应该在调度页面显示成“检查通过”。如果路径尚不存在,应检查它将要创建于哪个已存在的父目录和文件系统,而不是随便改查当前工作目录。

更重要的是,预检与实际写入之间有时间差。两个任务可以同时看到足够容量,然后一起把余量吃完。生产系统还需要集中调度预算或等价的并发控制,并正确处理实际写入失败;预检不是文件系统资源预留。

容量用例,最好同时测“拒绝”和“恢复”

无需对共享机器施加破坏性负载。先在代码层替换统计结果,验证:字节充足但 inode 不足、inode 充足但字节不足、两者恰好达到预算、inode 数据未知、预算非法。

这些单元测试验证的是判断逻辑,不证明真实文件系统行为。集成验证应使用有明确配额、隔离范围和回收方案的专用环境,并控制测试数据上限。

真正容易被漏掉的是失败之后。报告必须区分“测试执行结果”和“产物生成状态”:用例执行完成,不等于报告已经完整可读;产物失败,也不该被当成某条业务断言失败。

可以先写临时产物,完成内容校验后再发布可用状态。即便采用临时文件加重命名,也仍要处理写入、刷新及发布环节的失败;一次成功调用不自动等于持久化承诺已经兑现。

恢复验收要回答三件事:失败任务能否从明确的阶段恢复?未完成产物会不会被下载成正式报告?回收动作会不会影响其他正在运行的任务?

下次再看到“还有空间”,多问一句

是哪个路径还有空间?哪个身份可以使用?剩下的是字节、inode,还是配额?报错发生在打开文件、写入内容,还是发布报告?

把这些问题分清,排查就会从“再试一次、删一点东西”,变成有证据的定位。

测试平台的容量,不只是能跑多少条用例,还包括能安全生成、保存、恢复和回收多少份证据。 当产物本身失去可靠性,一份全绿的执行结果,也未必还能被复核。

关于我们

本文部分内容参考了霍格沃兹测试开发学社整理的相关技术资料,主要涉及软件测试、自动化测试、测试开发及 AI 测试等内容,侧重测试实践、工具应用与工程经验整理。