我的日报系统里有一个 JSON 文件,存的东西不多,但丢不起:订阅者名单、健康检查状态、LLM 调用统计,全在里面。
有一阵子我一直在担心一件事:如果进程在写这个文件的时候挂掉------断电、被 kill、磁盘满------会发生什么?
答案很不好看。用 open(path, "w") 直接覆盖写,写了一半挂掉,磁盘上留下的就是半截 JSON。文件存在,但 json.load 解不开。第二天定时任务一起来读它,整个流程起不来。数据丢一点可以忍,系统起不来不行。
所以后来我把所有落盘收敛成了一个 40 行的函数,全项目只允许这一个实现。今天把它拆开讲,里面有一个反直觉的取舍。
先看代码
python
import json, os, tempfile
def atomic_write_json(path, data, *, indent=2):
d = os.path.dirname(path) or "."
os.makedirs(d, exist_ok=True)
fd, tmp = tempfile.mkstemp(
prefix=os.path.basename(path) + ".",
suffix=".tmp", dir=d) # 注意:dir=d
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=indent)
os.replace(tmp, path)
finally:
if os.path.exists(tmp):
os.remove(tmp)
思路就一句话:不直接覆盖目标文件,先写一个临时文件,写成功了再一步替换过去。
os.replace 在同一个文件系统里是原子的------目标文件要么是完整的旧版本,要么是完整的新版本,不存在中间态。写一半挂掉?挂掉的只是那个 .tmp,正式文件一根毫毛都没动。
坑一:临时文件必须写在目标同一个目录
最常见的写法是 tempfile.mkstemp() 不传 dir,临时文件落在系统临时目录。Linux 上多数时候能跑,一换机器就炸。
原因是 os.replace 底层是 rename,rename 不允许跨文件系统 。你的数据在 D:\data\,系统临时目录在 C:\------跨盘,直接 OSError。
我在注释里专门写了一句:临时文件写在目标同目录(保证 os.replace 同盘,Windows 跨盘会失败)。这不是习惯问题,是硬约束。
顺带的好处:临时文件和正式文件同目录,权限、归属也是一致的,不会出现"临时目录里权限正常、搬过去就不对"的怪事。
坑二:一个故意的"不优化"------不做 fsync
看到这段代码的人,十有八九会想"顺手"补一句 f.flush(); os.fsync(f.fileno())。我的注释里专门堵了这条路:
故意不调用 os.fsync ------ 统计文件一轮会写 41 次,每次 fsync 会显著拖慢主流程。这是有意的取舍,不要"顺手优化"补上。
账是这么算的:fsync 强制把数据刷到物理盘,一次几毫秒到几十毫秒。这个统计文件一轮流程写 41 次,每次都 fsync,等于平白多出几百毫秒------而它保护的东西,最多丢一轮统计数字,下一次运行就补回来了。
fsync 买的是"断电也不丢",我的场景只要求"进程挂了不坏"。 os.replace 的原子性已经满足后者。为不需要的保证付 41 次的代价,不划算。
这个取舍我写进了 docstring。因为三个月后我自己都可能忘了为什么没有 fsync,然后"好心"补上。取舍不写下来,就等于没做过。
坑三:finally 里的清理要区分两种情况
python
finally:
if os.path.exists(tmp):
os.remove(tmp)
注意 os.replace 成功之后,tmp 这个路径已经不存在了(它变成了正式文件),所以 exists 是 False,remove 是空操作。真正会走到 remove 的只有一条路:写临时文件失败。
不清理的话,同目录会慢慢堆一堆 xxx.json.abc123.tmp 垃圾。不致命,但很难看,而且会让排障的人困惑。
最后
文本文件同理,换成 f.write(text) 就是,我项目里另一个 atomic_write_text 就是这么来的。
总结成三句话:
- 直接覆盖写 = 给进程崩溃留后门。写一半挂掉,留下的半截文件比没有文件更糟------它会让"文件存在"的检查通过,然后炸在解析上。
- 临时文件 + 同目录 +
os.replace,40 行,解决九成场景。 - 取舍要写进注释。尤其是"故意没做"的事情------没人会追问一个不存在的 fsync,但一定会有人好心补上它。