Python 临时缓冲落盘:还没达到 max_size,为什么 fileno 已经创建了文件

前天 4阅读

一个小文件处理器使用 SpooledTemporaryFile,希望内容较小时留在内存,超过阈值再使用临时文件。数据明明只有三个字节,接入某个库后却发生了落盘。原因可能是对方请求了 fileno:底层需要操作系统文件描述符时,内存缓冲必须先转成真实临时文件,大小阈值不是唯一触发条件。

这种行为常见于需要底层句柄的接口边界。排查时应记录谁调用了文件能力,而不只统计写入量。下面分别触发超过大小、请求描述符和显式 rollover 三条路径,并验证转换后内容仍然可读;没有依赖上传服务或真实业务文件。

可直接运行的对照实验

保存为 demo.py,用 Python 3.12 运行。Probe 只重写公开的 rollover 方法,记录调用后仍委托标准库执行。程序只检查是否发生过该调用,不读取私有内部属性,也不假设内部会调用多少次。例子使用二进制模式,阈值单位因此容易核对。

Python 临时缓冲落盘:还没达到 max_size,为什么 fileno 已经创建了文件

AI概念插图:轻量缓冲中的内容在请求底层文件能力后转入存储抽屉。图片不是运行截图,也不表示实测性能。

import os
from tempfile import SpooledTemporaryFile

class Probe(SpooledTemporaryFile):
    def __init__(self, **kwargs):
        self.rollover_seen = False
        super().__init__(**kwargs)

    def rollover(self):
        self.rollover_seen = True
        return super().rollover()

with Probe(max_size=4, mode="w+b") as stream:
    stream.write(b"abcd")
    assert not stream.rollover_seen
    stream.write(b"e")
    assert stream.rollover_seen
    stream.seek(0)
    assert stream.read() == b"abcde"
    print("size threshold: 4 stays, 5 rolls")

with Probe(max_size=1000, mode="w+b") as stream:
    stream.write(b"abc")
    assert not stream.rollover_seen
    descriptor = stream.fileno()
    assert stream.rollover_seen
    assert os.fstat(descriptor).st_size == 3
    stream.seek(0)
    assert stream.read() == b"abc"
    print("fileno: rolled at 3 bytes")

with Probe(max_size=1000, mode="w+b") as stream:
    stream.write(b"xy")
    stream.rollover()
    assert stream.rollover_seen
    stream.seek(0)
    assert stream.read() == b"xy"
    print("explicit rollover: content preserved")

超过阈值,和等于阈值不是一回事

第一份缓冲设置 max_size=4,写入四个字节后还没有触发 rollover;再写入第五个才出现转换记录。这里限制的是缓冲达到的文件大小,而不是某次 write 参数的长度。把内容分成很多小块写入,并不能让不断增长的结果永远留在内存。

第二份设置很大的阈值,却在三个字节时调用 fileno。返回值通过 os.fstat 检查确实对应三个字节的文件,同时探针记录转换。真实文件描述符数字由进程环境决定,所以示例不输出它,也不把固定编号写进断言。转换后先 seek 到零,再读出原始内容。

调用 fileno 本身会改变状态

如果只是想观察缓冲有没有落盘,顺手调用 fileno 来查看并不合适:这个观察动作就会触发落盘,改变你准备测量的状态。需要调查下游组件时,可以在测试替身或包装层记录方法调用,再将结果与数据量对照,避免由调试代码制造出原本不存在的现象。

第三份缓冲直接调用 rollover,证明应用也能主动选择转为文件。这适合后续步骤明确要求底层文件的流程,但不代表一定能得到可重新打开的可见路径。接口承诺的是文件对象与描述符能力,具体临时文件命名和目录项行为仍取决于平台。

把容量和交接约定写清楚

max_size 只决定自动切换策略,不限制最终文件总大小,也不是整个任务的内存硬上限。若输入可能很大,仍要在接收层限制总字节数,并考虑临时目录空间。代码选择正数阈值,避免让默认值的特殊行为掩盖本次要验证的边界。

缓冲交给下游时,还应约定读写位置。写完后直接 read 往往位于末尾,得到空字节并不表示落盘丢失。示例在三条路径都显式回到开头,再核对完整内容;调用方若接着追加或随机读写,应自行管理位置与共享句柄的使用顺序。

实验结束由 with 关闭自身创建的资源。这里的“落盘”只表示采用文件存储,不证明数据已持久保存到物理介质,也不构成可恢复备份。对于必须保留的产物,应另行安排保存位置、成功标记和持久性验证,不能把临时缓冲当作最终交付。

资料核对日期:2026年10月2日(北京时间)。代码在 CPython 3.12.14 中独立运行。

参考资料

Python 3.12 官方文档:SpooledTemporaryFile

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。