Spring WebMVC多客户端文件写入时出现I/O设备错误排查
多客户端下Spring WebMVC文件写入I/O设备错误问题分析
问题背景
使用Spring WebMVC开发的Web服务,单客户端周期性请求时运行正常,但新增两个客户端后,文件写入频繁出现500错误,日志提示I/O设备错误。同一文件名的后续请求无异常,且各客户端不会发送相同文件名的请求。
代码实现
控制器端点
@PostMapping( path = "append", consumes = MediaType.APPLICATION_JSON_VALUE ) public String append(@RequestBody AppendRequest request){ service.appendFile(request); return "OK"; }
服务层实现
@Value private String destinationPath; public void appendFile(AppendRequest appendRequest) { log.info("Incoming payload, filename: {}, total records: {}", appendRequest.getFilename(), appendRequest.getPayload().size()); Path directory = Paths.get(destinationPath, appendRequest.getSubdirectory()); if (!directory.toFile().exists()){ boolean ret = directory.toFile().mkdirs(); if(!ret) log.warn("Cannot create destination directory: {}", directory); } try { BufferedWriter writer = new BufferedWriter(new FileWriter(Paths.get(directory.toString(), appendRequest.getFilename()).toFile(), true)); writer.write(String.join(System.lineSeparator(), appendRequest.getPayload()) + System.lineSeparator()); writer.close(); } catch (IOException ioe) { log.error("Exception while writing to destination file: " + ioe.getMessage(), ioe); throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Store file failed"); } }
实体类定义
@Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppendRequest { private String subdirectory; private String fileName; private List<String> payload; }
可能原因分析
- 目录创建的竞态条件:多线程并发请求时,
directory.toFile().exists()检查与mkdirs()执行之间存在时间间隙。当多个线程同时检测到目录不存在并尝试创建时,其中一个线程可能创建失败,但此时目录实际已被其他线程创建完成,不过系统层面可能存在元数据同步延迟,导致该线程后续写入文件时触发I/O错误。 - 磁盘I/O资源过载:多客户端并发写入导致磁盘I/O负载骤增,超出了磁盘的处理能力,系统暂时无法处理写入请求,返回I/O设备错误。后续请求时磁盘负载降低,即可正常写入。
- Windows文件系统元数据延迟:日志显示路径为Windows系统,NTFS文件系统在频繁创建目录和写入文件时,可能存在短暂的元数据更新锁或同步延迟,多线程并发触发时更容易出现这类临时性错误。
- 流资源未安全管理:当前代码中
writer.close()未放在finally块中,若写入过程中出现异常,可能导致流资源泄漏,长期积累后加剧I/O资源竞争。
解决方案建议
- 原子化创建目录:使用
Files.createDirectories()替代File.mkdirs(),该方法是原子操作,能避免竞态条件,且会抛出异常明确告知目录创建失败原因:try { Files.createDirectories(directory); } catch (IOException e) { log.error("Failed to create directory: {}", directory, e); throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Create directory failed"); } - 自动管理流资源:使用try-with-resources语法自动关闭流,确保资源及时释放,避免泄漏:
try (BufferedWriter writer = new BufferedWriter(new FileWriter(Paths.get(directory.toString(), appendRequest.getFilename()).toFile(), true))) { writer.write(String.join(System.lineSeparator(), appendRequest.getPayload()) + System.lineSeparator()); } catch (IOException ioe) { log.error("Exception while writing to destination file: {}", ioe.getMessage(), ioe); throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Store file failed"); } - 添加I/O重试逻辑:针对临时性I/O错误,添加有限次数的重试机制,等待系统元数据同步或磁盘负载降低后再尝试操作:
int maxRetries = 3; int retryCount = 0; boolean success = false; while (retryCount < maxRetries && !success) { try { Files.createDirectories(directory); try (BufferedWriter writer = new BufferedWriter(new FileWriter(Paths.get(directory.toString(), appendRequest.getFilename()).toFile(), true))) { writer.write(String.join(System.lineSeparator(), appendRequest.getPayload()) + System.lineSeparator()); } success = true; } catch (IOException e) { retryCount++; log.warn("Write failed, retrying ({} of {})", retryCount, maxRetries, e); try { Thread.sleep(100); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Retry interrupted"); } } } if (!success) { throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Store file failed after retries"); } - 监控磁盘状态:检查磁盘剩余空间、健康状态及I/O负载,若磁盘性能不足,考虑升级存储设备或优化写入策略(如批量写入、异步写入)。
内容的提问来源于stack exchange,提问作者of32 inc
相关产品推荐
相关产品推荐

