Java 虚拟线程 × AI 推理

张开发
2026/4/16 5:57:45 15 分钟阅读

分享文章

Java 虚拟线程 × AI 推理
生产级高并发实战WEEKLY TECH · W16 · 2026结构化并发 / 背压控制 / 熔断降级 / TraceId 传播 · 五个真实踩坑场景01 AI 推理为什么特别适合虚拟线程一次大模型推理请求真正花在 CPU 上的时间不超过5%——剩下 95% 都在等等 HTTP 响应、等 Token 流式输出、等数据库里的 Embedding 查询回来。这种极端 I/O 密集的场景OS 线程就是在烧钱。JDK 21 的虚拟线程Virtual Thread让 JVM 自己来调度挂起/恢复一台 8 核机器可以同时挂起十万级虚拟线程内存开销仅 ~1KB/个。指标OS 线程虚拟线程栈内存~1MB~1KBAI 推理 I/O 占比-95%吞吐量提升-×14⚠️误区提醒虚拟线程不是银弹。CPU 密集型计算如矩阵运算、图像处理用虚拟线程没有收益反而可能因调度开销略有损耗请继续使用线程池 ForkJoinPool。02 生产架构网关 → 并发推理 → 聚合下图是一个真实的 AI 推理网关架构每个请求需要并发调用 3 个模型主力模型 两个备选取最快返回的有效结果┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 客户端请求 │ ──→ │ Spring Gateway │ ──→ │ AI Inference │ └─────────────┘ │ 限流 / 鉴权 │ │ Service │ └─────────────────┘ └────────┬────────┘ │ StructuredTaskScope.ShutdownOnSuccess 并发调用 ↓ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ GPT-4o │ │ Claude 3.5 │ │ Gemini 1.5 │ │ 主力模型 │ │ 备选 A │ │ 备选 B │ └─────────────┘ └─────────────┘ └─────────────┘ │ 最快有效结果 → 取消其余调用 ↓ ┌─────────────────┐ ┌─────────────┐ ┌─────────────┐ │ 结果聚合 │ ──→│ Prometheus │ ──→│ SSE 流式 │ │ TraceId 注入 │ │ 指标上报 │ │ 返回客户端 │ └─────────────────┘ └─────────────┘ └─────────────┘03 结构化并发谁先回来用谁其余自动取消JDK 21 引入的StructuredTaskScope是虚拟线程配套的作用域线程管理。ShutdownOnSuccess策略任何一个子任务成功返回立即取消其他所有子任务——完美匹配多模型竞速取最快场景。ServiceSlf4jpublicclassMultiModelRaceService{privatefinalListAiInferenceClientclients;// GPT / Claude / GeminiprivatefinalMeterRegistrymeterRegistry;privatefinalSemaphoreglobalSemaphore;// 全局背压publicMultiModelRaceService(ListAiInferenceClientclients,MeterRegistrymeterRegistry,Value(${ai.max-concurrent:200})intmaxConcurrent){this.clientsclients;this.meterRegistrymeterRegistry;// 最多同时 200 个推理请求超出进入等待而不是拒绝this.globalSemaphorenewSemaphore(maxConcurrent,true);}/** * 并发调用多个模型返回最快的有效结果。 * 失败 / 超时的子任务不影响最终结果全部失败才抛异常。 */publicInferenceResultraceInference(InferenceRequestreq)throwsInterruptedException{StringtraceIdMDC.get(traceId);// 提前捕获虚拟线程不自动继承Timer.SamplesampleTimer.start(meterRegistry);// ① 背压获取令牌最多等 3 秒否则降级booleanacquiredglobalSemaphore.tryAcquire(3,TimeUnit.SECONDS);if(!acquired){meterRegistry.counter(ai.inference.backpressure).increment();returnfallbackResult(req,backpressure);}try(// ② 结构化并发 —— 谁先成功就关闭其他任务varscopenewStructuredTaskScope.ShutdownOnSuccessInferenceResult()){// ③ 为每个模型 fork 一个虚拟线程不用手动管线程池for(AiInferenceClientclient:clients){scope.fork(()-{// 关键手动将 traceId 传入子线程的 MDCMDC.put(traceId,traceId);try{returnclient.callWithTimeout(req,Duration.ofSeconds(8));}finally{MDC.remove(traceId);// 防止线程复用导致 MDC 污染}});}// ④ 等待直到有一个成功 or 全部完成最长 10 秒scope.joinUntil(Instant.now().plusSeconds(10));InferenceResultresultscope.result();// 取最快成功结果recordMetrics(sample,result,success);returnresult;}catch(ExecutionExceptione){// 全部模型都失败走降级逻辑log.error([{}] All models failed,traceId,e.getCause());recordMetrics(sample,null,all_failed);returnfallbackResult(req,all_models_failed);}finally{globalSemaphore.release();}}}生产踩坑 #1虚拟线程不继承父线程的 MDCSLF4J ThreadLocal。直接 fork 子任务所有日志的 traceId 会消失分布式链路追踪全部断掉。必须在 fork 时手动传入 traceId在 finally 块清理防止线程复用后污染下一个请求。04 背压控制 熔断降级防止上游雪崩虚拟线程虽然轻量但下游 AI API 有并发限制大多数模型 API QPS 上限在 100~500。没有背压机制流量洪峰会直接打爆下游触发 429 或超时雪崩。正确姿势是Semaphore 信号量控制最大并发超限请求等待而不是直接报错配合Resilience4j熔断器当错误率过高时自动开启熔断。ComponentpublicclassCircuitBreakerInferenceClientimplementsAiInferenceClient{privatefinalCircuitBreakercircuitBreaker;privatefinalRateLimiterrateLimiter;// 令牌桶限速privatefinalOpenAiClientdelegate;publicCircuitBreakerInferenceClient(CircuitBreakerRegistrycbRegistry,RateLimiterRegistryrlRegistry,OpenAiClientdelegate){this.delegatedelegate;// 熔断器配置60 秒窗口错误率 50% 则打开15 秒后半开探测this.circuitBreakercbRegistry.circuitBreaker(openai,CircuitBreakerConfig.custom().slidingWindowType(TIME_BASED).slidingWindowSize(60).failureRateThreshold(50.0f).waitDurationInOpenState(Duration.ofSeconds(15)).permittedNumberOfCallsInHalfOpenState(3)// 429 / 503 视为失败4xx 业务错误不计入熔断.recordException(e-einstanceofRateLimitException||einstanceofServiceUnavailableException).build());// 限速器每秒最多 80 次调用对齐 OpenAI tier-2 限制this.rateLimiterrlRegistry.rateLimiter(openai,RateLimiterConfig.custom().limitForPeriod(80).limitRefreshPeriod(Duration.ofSeconds(1)).timeoutDuration(Duration.ofMillis(500)).build());}OverridepublicInferenceResultcallWithTimeout(InferenceRequestreq,Durationtimeout){returnRateLimiter.decorateCheckedSupplier(rateLimiter,CircuitBreaker.decorateCheckedSupplier(circuitBreaker,()-delegate.call(req,timeout))).get();}/** * 降级兜底返回本地小模型的简短回答或缓存结果。 * 绝对不能让熔断异常透传到用户界面。 */RecoverpublicInferenceResultfallback(CallNotPermittedExceptionex,InferenceRequestreq){log.warn(Circuit OPEN for openai, using local fallback);returnlocalFallbackModel.quickAnswer(req);}}关键配置recordException一定要精细配置——只有真正的服务故障429 限流、503 不可用才计入熔断统计。用户输入导致的 400 参数错误不应该触发熔断否则会误伤正常请求。05 上下文传播TraceId 跨虚拟线程的正确姿势这是团队升级 JDK 21 后最高频的问题切到虚拟线程之后Sleuth / Micrometer Tracing 的 TraceId 链路断了。根因是 MDC 基于 ThreadLocal虚拟线程 fork 不会自动拷贝父线程的 ThreadLocal 值。生产推荐方案统一用ScopedValueJDK 21 preview22 正式替代 ThreadLocal或者封装一个 ContextCarrier 工具类在任务提交时显式传递。/** * 支持 MDC 上下文传播的虚拟线程执行器。 * 替换项目中所有 Executors.newVirtualThreadPerTaskExecutor() 的调用。 */publicclassVirtualThreadContextExecutorimplementsExecutor{privatestaticfinalExecutorDELEGATEExecutors.newVirtualThreadPerTaskExecutor();Overridepublicvoidexecute(Runnablecommand){// 在提交时快照当前线程的完整上下文MapString,StringmdcCopyMDC.getCopyOfContextMap();ObservationparentObservationObservationThreadLocalAccessor.getValue();DELEGATE.execute(()-{// 恢复 MDC含 traceId / spanId / userId 等所有 keyif(mdcCopy!null)MDC.setContextMap(mdcCopy);// 恢复 Micrometer Tracing 的 Observation链路追踪不断链Observationobsnull;if(parentObservation!null){obsparentObservation.createChildObservation(virtual-thread-task);obs.start();ObservationThreadLocalAccessor.setValue(obs);}try{command.run();}finally{if(obs!null)obs.stop();MDC.clear();// 务必清理防止线程复用污染}});}}// Spring Bean 注册让 Async 和 TaskExecutor 都走这个执行器ConfigurationpublicclassExecutorConfig{Bean(taskExecutor)publicAsyncTaskExecutortaskExecutor(){returnnewTaskExecutorAdapter(newVirtualThreadContextExecutor());}}生产踩坑 #2只清理 MDC 还不够SecurityContextHolderSpring Security、RequestContextHolderSpring MVC同样基于 ThreadLocal切虚拟线程后都需要显式传播。建议统一封装一个 ContextCarrier一次快照所有需要传播的上下文。06 Spring Boot 3 一键启用 监控配置开启虚拟线程只需一行配置但生产环境还需要配好监控指标否则出问题根本不知道瓶颈在哪。# ── Spring Boot 虚拟线程开关3.2 正式支持──spring:threads:virtual:enabled:true# 一行开启Tomcat/Undertow 全部走虚拟线程# ── AI 推理并发控制 ──ai:max-concurrent:200# 全局 Semaphore 上限按下游 QPS 限制调整model:timeout-seconds:8# 单次推理超时防止慢请求占用连接race-timeout-seconds:10# 竞速总超时# ── Resilience4j 熔断配置 ──resilience4j:circuitbreaker:instances:openai:sliding-window-type:time_basedsliding-window-size:60failure-rate-threshold:50wait-duration-in-open-state:15spermitted-calls-in-half-open-state:3register-health-indicator:true# ── Micrometer 指标暴露Prometheus 拉取──management:endpoints:web:exposure:include:health,prometheus,metricsmetrics:tags:application:${spring.application.name}distribution:percentiles-histogram:ai.inference.latency:true# P50/P95/P99 延迟分布percentiles:ai.inference.latency:0.5,0.95,0.99Grafana 推荐监控指标Prometheus PromQL# 1. 推理延迟 P99告警阈值 5s histogram_quantile(0.99, rate(ai_inference_latency_seconds_bucket[5m])) # 2. 背压触发率突增说明下游扛不住 rate(ai_inference_backpressure_total[1m]) # 3. 熔断器状态0CLOSED 1OPEN 2HALF_OPEN resilience4j_circuitbreaker_state{nameopenai} # 4. 虚拟线程挂起数JVM 内部指标需开启 JFR jvm_threads_virtual_mounted07 压测数据升级前后对比以下数据来自相同硬件8C16G、相同负载1000 并发 AI 推理请求的对比测试指标OS 线程池 (200线程)虚拟线程变化吞吐量 (RPS)3124,380↑ ×14P99 延迟18.4s5.8s↓ 68%JVM 堆外内存~2.1GB~360MB↓ 83%线程数峰值200 (硬上限)100,000无上限CPU 使用率62%58%持平GC 停顿频繁 Full GCZGC 1ms↓ 显著✅结论在 AI 推理这种极端 I/O 密集场景下切换虚拟线程是几乎零成本的改造改一行配置收益极其显著。但要在生产用好还需要把背压、熔断、上下文传播这三个配套机制一起做到位。08 生产踩坑清单升级前必读踩坑场景根因解决方案日志 traceId 消失MDC ThreadLocal 不继承封装 VirtualThreadContextExecutor显式传播Spring Security 鉴权失效SecurityContextHolder ThreadLocal使用 InheritableThreadLocal 模式或显式传递数据库连接池耗尽虚拟线程太多HikariCP 连接不够HikariCP maximum-pool-size 按实际 DB 并发上调synchronized 锁死虚拟线程遇 synchronized 会 pin 住载体线程换用 ReentrantLock / StampedLockCPU 密集任务性能下降调度开销虚拟线程不减少 CPU 竞争CPU 密集任务保留 ForkJoinPool 线程池AI API 429 雪崩无背压虚拟线程并发打爆下游Semaphore Resilience4j 双重保护一句话总结I/O 密集用虚拟线程CPU 密集仍用平台线程。虚拟线程不是银弹但在 AI 推理这个场景它是真正的游戏规则改变者。

更多文章