Node.js fs模块循环同步文件操作报EPERM错误的稳定方案咨询
Node.js 同步fs模块高频写删文件偶发EPERM问题
问题描述
在Node.js开发中,fs模块同步文件操作偶发不稳定,看似操作未完全执行完成就返回并抛出异常。
问题复现代码如下:
"use strict"; Error.stackTraceLimit = Infinity; const fs = require("fs"); const filename = "foo.txt"; for (let i = 0; ; i++) { try { fs.writeFileSync(filename, "bar"); fs.unlinkSync(filename); } catch (e) { throw Object.assign(new Error(`Error at iteration ${i}!`), { cause: e }); } }
上述代码无限循环执行writeFileSync写入foo.txt、unlinkSync删除foo.txt的同步操作,测试环境下运行到第12980次迭代时抛出如下错误:
Error: Error at iteration 12980! at Object.<anonymous> (C:\test.js:15:4) at Module._compile (internal/modules/cjs/loader.js:999:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10) at Module.load (internal/modules/cjs/loader.js:863:32) at Function.Module._load (internal/modules/cjs/loader.js:708:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12) at internal/main/run_main_module.js:17:47 { cause: Error: EPERM: operation not permitted, open 'foo.txt' at Object.openSync (fs.js:462:3) at Object.writeFileSync (fs.js:1384:35) at Object.<anonymous> (C:\test.js:10:6) at Module._compile (internal/modules/cjs/loader.js:999:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10) at Module.load (internal/modules/cjs/loader.js:863:32) at Function.Module._load (internal/modules/cjs/loader.js:708:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12) at internal/main/run_main_module.js:17:47 { errno: -4048, syscall: 'open', code: 'EPERM', path: 'foo.txt' } }
核心疑问:
- 其他开发者是否也复现过相同的现象?
- 采用何种方案可以保障这类文件操作的稳定执行?
解答
复现情况说明
这是Windows平台下Node.js开发的典型已知问题,所有在Windows环境做过高频循环文件写删操作的开发者基本都复现过。
这个问题不是fs同步API未等操作完成就返回,根因是Windows系统的文件系统机制差异:Windows的文件句柄释放是异步执行的,当删除、关闭文件的API返回成功时,内核层面的文件锁清理、句柄销毁可能还在队列中未完成,此时立刻对同一路径发起打开、写入请求,就会抛出错误码为-4048的EPERM文件占用报错。Linux、macOS等类Unix系统不存在这个问题,这类系统的unlink操作返回后,对应路径可立即被重新使用,不会出现占用冲突。
稳定执行方案
不需要替换为异步fs API,同步场景下使用以下两种方案即可保障操作稳定:
- 加错误重试逻辑
遇到EPERM错误时不要直接抛出,等待1050ms后重试当前操作即可,一般重试13次就能成功,系统的句柄释放最多几十毫秒即可完成。
同步场景下的重试逻辑参考:function retryFsOp(op, retries = 5, delay = 20) { for (let i = 0; i < retries; i++) { try { return op(); } catch (e) { if (e.code !== 'EPERM' || i === retries - 1) throw e; // 同步等待,仅适用于同步fs操作场景 Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay); } } } // 调用示例 for (let i = 0; ; i++) { retryFsOp(() => fs.writeFileSync(filename, "bar")); retryFsOp(() => fs.unlinkSync(filename)); } - 避免高频复用同一路径做写删操作
如果是临时文件场景,每次写入时生成随机唯一文件名,不要反复对同一个文件名做写删循环,从根源上避开文件句柄未释放的冲突窗口。
注意:不要在unlink操作后加固定时长sleep规避问题,不同设备的系统负载差异很大,固定等待时长要么无谓拖慢执行效率,要么在高负载场景下依然偶发报错,重试是目前成本最低、可靠性最高的解决方案。
内容的提问来源于stack exchange,提问作者pronodingo
相关产品推荐
相关产品推荐

