迁移至Azure后迭代写删文件频发IOException问题排查
根本原因
这是Windows文件系统删除语义和实时安全扫描共同导致的时序问题,和你存储文件的磁盘路径没有关系:
File.Delete()方法本身是非同步操作:调用返回成功仅代表系统已经收到删除请求,给文件打上了删除标记,并不会立刻移除文件系统的目录条目。只有当所有持有该文件句柄的进程全部释放句柄后,文件才会被真正删除,此时查询文件存在性才会返回false。- 你之前本地服务器多年稳定运行,只是因为本地环境几乎没有其他进程会主动打开你生成的临时文件,
File.Delete()返回后句柄会立刻释放,删除流程几乎瞬间完成,这个时序问题触发概率极低,完全被掩盖了。 - 迁移到Azure后,环境默认开启Microsoft Defender实时防护(也就是你在Process Explorer里看到的MsMpEng.exe进程):每当你写入关闭一个文件,Defender会立刻打开文件做恶意内容扫描,扫描过程会持有文件句柄,持有时长从几毫秒到几百毫秒不等。一旦你调用
File.Delete()时Defender还没释放句柄,文件就会进入待删除状态,此时你立刻调用File.Create()创建同名文件就会抛出IO占用异常,查询Exists属性也会返回true。 - 你尝试更换C/D/F盘、临时目录、网络共享路径都复现问题,是因为只要运行在Windows系统上、Defender实时扫描开启,这个句柄抢占的逻辑就存在,和底层存储介质无关。
更优解决方案
按改动成本从低到高排序,全部不需要重构原有核心业务逻辑,审批难度远低于直接改临时文件写入流程:
- 方案1(零代码改动,优先推荐)
给作业使用的专属临时文件目录配置Defender扫描排除规则。只要把存放FileA.txt、FileB.txt、FileC.txt的目录加入Defender排除路径,MsMpEng.exe就不会再打开扫描这些临时文件,File.Delete()返回后文件会立刻完成删除流程,从根源消除句柄占用问题。这个方案只需要调整Azure虚拟机的安全配置,不需要改动运行多年的稳定业务代码,审批成本最低。 - 方案2(极小代码改动,替代Sleep轮询)
替换你现在的固定间隔Sleep轮询逻辑,用文件独占打开的方式判断文件是否真正释放,比单纯轮询Exists属性更可靠,等待延迟也更低。参考实现:
只需要在每次创建临时文件前调用这个方法即可,比你现在100ms间隔轮询的等待效率高很多,也不会出现无限等待的问题。private static void WaitForFileReady(string filePath, int timeoutMs = 3000) { long startTick = Environment.TickCount64; while (File.Exists(filePath)) { try { // 尝试以完全独占模式打开文件,成功则说明无其他进程占用 using (File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) { File.Delete(filePath); return; } } catch (IOException) { if (Environment.TickCount64 - startTick > timeoutMs) throw new TimeoutException($"文件 {filePath} 长时间被占用,无法删除重建"); Thread.Sleep(10); } } } - 方案3(逻辑兼容最强,不依赖环境配置)
放弃复用同名临时文件的逻辑,每次处理人员时给三个临时文件加唯一后缀(比如Guid、自增序号),处理完合并到Output.txt后,不需要同步等待临时文件删除完成,直接进入下一个人员处理流程,旧临时文件可以交给后台异步批量清理。因为每次新建的临时文件名都是唯一的,就算旧文件还处于待删除状态,也完全不会影响新文件的创建,这个方案不依赖任何系统安全配置,在任何Windows环境下都能稳定运行,且不需要改动原有三个独立类的文件生成逻辑,只需要改文件名生成规则即可。
不建议采用重构跳过临时文件、直接写入Output.txt的方案:一方面原有三个独立类拆分生成文件的逻辑已经稳定运行多年,强行修改写入逻辑反而容易引入内容拼接、并发写入的新问题;另一方面重构业务代码的审批成本远高于上面三个方案。
内容的提问来源于stack exchange,提问作者robertviper08
相关产品推荐
相关产品推荐

