
本文摘要一个常见的场景在基于Spring Boot 3.2的订单服务中集成了一个调用外部LLM API的Async Agent服务用于实时生成商品描述。一、问题与结论一个常见的场景在基于Spring Boot 3.2的订单服务中集成了一个调用外部LLM API的AsyncAgent服务用于实时生成商品描述。单体测试正常但上线后生产环境的端到端延迟骤增P99从200ms飙升至600ms以上且伴随服务内存持续增长数小时后触发OOM重启。结论问题根源在于1) Spring Boot 3.2中Async方法与底层虚拟线程/平台线程池的混用不当造成阻塞传染和线程池耗尽。2) AI模型流式响应的处理未正确管理Servlet响应缓冲区和客户端连接导致内存泄漏。二、排查与选择依据1. Spring Boot异步模型冲突与内存泄漏冲突的核心在于spring-boot-starter-web基于Servlet同步模型与Async任务对线程资源的争夺以及未处理流式响应的内存边界。排查工具jcmd pid Thread.print查看线程堆栈jcmd pid GC.heap_dump file分析堆内存。典型故障代码与修复未优化代码可能包含这样的流式处理端点GetMapping(value/stream,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterstream(){SseEmitteremitternewSseEmitter(60000L);// 模拟异步推送AI流式结果taskExecutor.execute(()-{try{for(Stringtoken:aiService.streamGenerate()){emitter.send(SseEmitter.event().data(token));}emitter.complete();}catch(Exceptione){emitter.completeWithError(e);}});returnemitter;}这段代码在客户端连接慢或异常断开时emitter.send可能阻塞导致执行此任务的线程被长期占用且内存中的事件无法释放。修复方向为线程池任务设置超时并正确配置SseEmitter的回调与缓冲区。// 修复示例增加生命周期回调与缓冲区设置GetMapping(value/stream,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterstream(){SseEmitteremitternewSseEmitter(60000L);emitter.onTimeout(()-emitter.complete());// 超时后完成释放资源emitter.onError(e-emitter.complete());// 错误后完成taskExecutor.execute(()-{try{for(Stringtoken:aiService.streamGenerate()){emitter.send(SseEmitter.event().data(token));}emitter.complete();}catch(Exceptione){emitter.completeWithError(e);}});returnemitter;}替代方案与取舍方案选择条件代价与边界本文方案混合同步受控异步主服务为同步Spring MVC仅将少量Agent调用异步化。技术栈迁移成本低。需精细管理线程池、超时和缓冲区复杂度高。不适用于全链路高并发流式场景。全面反应式WebFlux全新项目或能承受全栈重写需处理大量并发长连接流。学习曲线陡峭与大量现有阻塞式库如JDBC不兼容改造风险高。虚拟线程直阻塞Java 21代码以阻塞式I/O为主且能升级到Java 21。可大幅简化代码但要求所有依赖库是虚拟线程友好的。无法解决流式响应的背压问题。三、关键原理虚拟线程与Async的交互当spring.threads.virtual.enabledtrue时Spring Boot会尝试用虚拟线程执行Async任务。但如果该任务内部调用了不兼容虚拟线程的、基于平台线程池的阻塞库如旧版HTTP客户端虚拟线程会“固定”在平台上失去轻量优势并可能耗尽平台线程。内存泄漏根源Servlet容器的SseEmitter或AsyncContext在异步处理中如果开发者忘记设置响应缓冲区大小或未正确处理客户端断开事件emitter.onTimeout/emitter.onError底层关联的响应流和数据队列将无法被垃圾回收。四、可运行示例环境JDK 21, Spring Boot 3.2.5, Maven。示例目标复现因Async任务阻塞导致的请求排队。AgentService.javaimportorg.springframework.scheduling.annotation.Async;importorg.springframework.stereotype.Service;importjava.util.concurrent.CompletableFuture;importjava.util.concurrent.TimeUnit;ServicepublicclassAgentService{// 模拟一个调用外部AI服务或进行计算的“Agent任务”AsyncpublicCompletableFutureStringinvokeAgentTask(Stringinput){// 陷阱此处如果调用了同步阻塞的I/O库如旧的HTTP客户端// 即使方法是Async也会占用虚拟线程或平台线程池的线程。// 模拟一个可能阻塞的同步网络调用try{TimeUnit.SECONDS.sleep(2);// 假设这是同步HTTP调用}catch(InterruptedExceptione){Thread.currentThread().interrupt();}returnCompletableFuture.completedFuture(Processed: input);}}ApiController.javaimportorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;importjava.util.concurrent.CompletableFuture;RestControllerpublicclassApiController{privatefinalAgentServiceagentService;publicApiController(AgentServiceagentService){this.agentServiceagentService;}// 同步的REST端点调用异步服务GetMapping(/process)publicStringprocess(RequestParamStringquery)throwsException{// 等待异步任务完成CompletableFutureStringresultFutureagentService.invokeAgentTask(query);StringresultresultFuture.get();// 阻塞等待可能引发性能问题returnresult;}}application.properties# 启用虚拟线程 (需要 Java 21) spring.threads.virtual.enabledtrue # 自定义异步执行器可选用于对比 spring.task.execution.pool.core-size4 spring.task.execution.pool.max-size8运行与观察启动应用使用JMeter或wrk发送100个并发请求/process。预期输出所有请求几乎同时到达但响应时间应呈阶梯状后续请求等待时间累加。实际输出在未限制异步线程池大小时耗时远超2秒表明请求被串行化在有限的线程上。常见失败若未在application.properties中限制线程池spring.task.execution.pool.max-size虚拟线程的创建可能不受控掩盖了资源竞争问题。修复方法是为关键的异步执行器配置有界队列和明确的拒绝策略。五、失败场景与验证失败场景Async异常丢失导致线程池耗尽场景描述AI Agent调用链中的一个异步步骤因第三方服务故障而抛出异常但由于异常处理不当线程资源未被释放最终拖垮整个系统。失败代码片段AsyncpublicCompletableFutureStringriskyAgentCall(Stringinput){// 假设此方法调用了一个不稳定的外部APIif(Math.random()0.5){thrownewRuntimeException(External API failed);// 抛出未检查异常}returnCompletableFuture.completedFuture(OK);}调用方错误方式GetMapping(/risk)publicStringriskEndpoint()throwsException{CompletableFutureStringfutureriskyAgentCall(test);// 如果riskyAgentCall内部抛出异常 future.get() 会抛出 ExecutionExceptiontry{returnfuture.get(5,TimeUnit.SECONDS);// 设置超时}catch(Exceptione){// 此处捕获了异常但线程池中的任务状态可能已损坏returnError: e.getMessage();}}失败机制与排查症状应用运行一段时间后响应变慢最终大量请求超时。排查通过线程堆栈jstack或监控发现spring-task-execution线程池中的线程数量达到最大值且大部分线程处于WAITING或TIMED_WAITING状态。根本原因Async方法抛出未捕获的RuntimeException时Spring的默认异步异常处理器会将其记录为日志但该异步任务对应的CompletableFuture可能处于一个异常的完成状态。如果后续的Future.get()超时或未被妥善处理并且线程池没有适当的清理或驱逐机制资源线程、连接可能会泄漏。修复方向在Async方法内部做好完整的异常捕获和资源清理配置全局的AsyncUncaughtExceptionHandler优先考虑使用CompletableFuture链式调用.exceptionally(),.handle()来显式处理异常。验证结果内存泄漏修复流式处理并增加emitter.onTimeout(() - {})回调后通过jmap -dump对比堆中SseEmitter相关实例数量在压测后稳定不再持续增长。异常处理实现全局AsyncUncaughtExceptionHandler后线程池资源在异常发生后能被及时回收应用稳定性提升。参考资料Spring Boot 3.2 Release NotesJava 21 Virtual Threads Specification (JEP 444)Spring Framework 6.1 Async Execution and TaskExecutorJakarta Servlet 6.0 Asynchronous Processing