当传统的 Spring Boot 微服务遇到 LLM(大语言模型),我们面临的不再是简单的“接口调用”,而是一场关于线程模型、内存管理、超时策略和可观测性的系统性重构。 本文基于 Spring AI 框架,结合 Java 21 虚拟线程(Virtual Threads)与 GraalVM 原生镜像,深入剖析如何在保障现有微服务稳定性的前提下,构建一个高吞吐、低延迟的 AI 微服务。我们将直面并解决流式响应下的线程阻塞、长连接管理及向量数据库集成等硬核问题。
在集成 LLM 之前,我们的订单微服务 QPS 稳定在 2000,响应时间 P99 < 50ms。引入 AI 总结功能后,仅 50 并发请求,服务就出现了严重的“假死”现象。
根本原因在于资源模型的错配:
维度 | 传统 RESTful API | LLM 推理 API |
|---|---|---|
响应时间 | 毫秒级(< 100ms) | 秒级甚至分钟级(TTFT 数百 ms,TPOT 数十 ms/token) |
数据流向 | 短连接,一次性返回 JSON | 长连接,Server-Sent Events (SSE) 流式逐字返回 |
线程占用 | 请求处理完即释放 | 线程需挂起等待完整响应(可能持续 30s+) |
错误模式 | 偶发超时/5xx | 频发限流(Rate Limit)、网络抖动、部分 Token 丢失 |
在 Java 传统的 Thread-per-Request 模型(Tomcat 默认)下,一个 30 秒的 AI 请求会独占一个宝贵的容器线程。当并发数超过核心线程数,线程池排队,整个服务的健康检查(/actuator/health)都无法响应,触发 K8s 的 LivenessProbe 失败,Pod 被重启——这是典型的“雪崩”前兆。
面对上述痛点,很多架构师第一反应是引入 WebFlux(响应式编程)。但在 AI 场景下,WebFlux 的 Mono/Flux 虽然能解决线程阻塞问题,却带来了极高的门槛和调试复杂度(尤其是复杂的上下文传递)。
我们最终选择了 Spring AI + Java 21 虚拟线程(Virtual Threads) 的组合,将 Tomcat 的线程池替换为虚拟线程。
Spring AI 提供了统一的 ChatClient 接口,屏蔽了 OpenAI、Azure、Ollama 甚至本地模型的差异:
@RestController
@RequestMapping("/api/ai")
public class AIController {
private final ChatClient chatClient;
public AIController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个专业的电商助手,请用中文回复")
.build();
}
@GetMapping("/summarize")
public Flux<String> summarize(@RequestParam String orderId) {
// 返回 Flux<String> 实现 SSE 流式输出
return chatClient.prompt()
.user(u -> u.text("请总结订单 {id} 的购买记录和潜在风险").param("id", orderId))
.stream()
.content();
}
}Spring Boot 3.2+ 支持极简配置开启虚拟线程:
# application.yml
spring:
threads:
virtual:
enabled: true此时,Tomcat 接收到每个请求不再分配平台线程(Platform Thread),而是分配虚拟线程。虚拟线程的挂起成本极低(~几微秒),即使有 10000 个请求同时等待 LLM 响应,也不会耗尽内存或导致 CPU 上下文切换风暴(vmstat 中的 cs 指标保持平稳)。
性能实测对比:在模拟 500 并发 AI 请求(每个响应耗时 5s)下,传统 Tomcat(200 线程池)吞吐量跌至 40 req/s,CPU sys% 飙升 40%;启用虚拟线程后,吞吐量恢复至 480 req/s,CPU 开销几乎无增长。
AI 流式响应(SSE)要求网关(Nginx/Kong)和微服务必须禁用缓冲,否则 TTFT(首字延迟)会叠加网关缓冲时间。
location /api/ai/ {
proxy_pass http://ai-service;
proxy_buffering off; # 关键:禁用缓冲
proxy_cache off;
proxy_read_timeout 600s; # AI 响应慢,超时设大
chunked_transfer_encoding on;
}在 RestClient 或 WebClient 构建时,我们需要针对 AI 场景设置连接超时(ConnectTimeout)与读取超时(ReadTimeout)的分离策略:
@Configuration
public class AIClientConfig {
@Bean
public WebClient aiWebClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) // 3s 连不上就弃
.responseTimeout(Duration.ofSeconds(60)) // 整体响应 60s 上限
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(60, TimeUnit.SECONDS))
);
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024 * 10)) // 10MB
.build();
}
}当 LLM 生成速度过快,而客户端消费速度(如网络带宽)跟不上时,我们需要在应用层做削峰。利用 Project Reactor 的 limitRate:
Flux<String> aiStream = chatClient.prompt().user("...").stream().content();
// 控制每秒最多向下游推送 50 个 Token,防止客户端缓冲区溢出
return aiStream.limitRate(50);检索增强生成(RAG)已成为 AI 微服务的标配。我们选择 Redis Stack 作为向量数据库(因其支持 FT.SEARCH 且运维成本低),将商品文档的 Embedding 向量存入其中。
使用 Spring AI 的 EmbeddingClient 时,每次调用都会触发一次网络请求。如果每个请求都实时计算 Embedding,延迟将增加 200ms~500ms。
优化策略:引入本地缓存(Caffeine),对高频查询词(如“退货”、“保价”)进行缓存。
@Configuration
public class EmbeddingCacheConfig {
@Bean
public Cache<String, float[]> embeddingCache() {
return Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(Duration.ofHours(1))
.recordStats()
.build();
}
@Bean
public EmbeddingClient cachedEmbeddingClient(EmbeddingClient delegate, Cache<String, float[]> cache) {
return new CachedEmbeddingClient(delegate, cache);
}
}向量检索涉及 CPU 密集型计算(向量余弦相似度)。虽然在 Java 中通常由 Redis 执行,但结果反序列化仍会占用 CPU。我们通过 @Async + 虚拟线程 将 RAG 检索与 LLM 生成并行化(CompletableFuture 组合),将总响应时间从“检索+生成”串行的 2s 压缩到“Max(检索, 生成)”的 1.2s。
@Async
public CompletableFuture<List<Document>> searchSimilarDocs(String query) {
// 向量检索逻辑
}
// 在 Service 中组合
CompletableFuture<List<Document>> docsFuture = searchSimilarDocs(query);
CompletableFuture<String> answerFuture = chatClient.prompt()
.user(u -> u.text("基于上下文回答: {context}").param("context", docsFuture.join()))
.stream().content().collectList().map(list -> String.join("", list));AI 微服务通常需要加载大量的 Tensor 依赖库和模型分词器(Tokenizer),导致 Spring Boot 应用启动时间长达 30~60 秒,内存占用高达 1.5GB。在 K8s 滚动更新时,极易因为 initialDelaySeconds 不够而导致健康检查失败。
解决方案:使用 GraalVM Native Image 构建原生可执行文件,结合 Spring Boot 3 的 AOT(Ahead-Of-Time)处理。
Jackson 反序列化、OkHttp 的内部类大量使用反射。需要生成 reflect-config.json。我们利用 native-maven-plugin 的 agent 在测试阶段自动收集:
java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \
-jar target/ai-service.jar运行所有单元测试后,agent 会自动生成所需的配置文件。
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>--initialize-at-build-time=org.slf4j</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
<buildArg>-march=compatibility</buildArg> <!-- 兼容旧 CPU -->
</buildArgs>
<imageName>ai-service</imageName>
</configuration>
</plugin>效果:
由于 AI 调用涉及外部第三方 API(如 OpenAI),传统的 Trace 只能看到 HTTP 出口,无法感知“Prompt 长度”和“Token 消耗”。
利用 Micrometer Tracing,我们在 ChatClient 拦截器中注入业务标签:
@Component
public class TracingChatClientInterceptor implements ChatClientInterceptor {
@Override
public void before(ChatClientRequest request) {
Span currentSpan = Tracing.currentSpan();
if (currentSpan != null) {
currentSpan.setAttribute("ai.prompt.length", request.getUserText().length());
currentSpan.setAttribute("ai.model", "gpt-4");
}
}
@Override
public void after(ChatClientResponse response) {
Span currentSpan = Tracing.currentSpan();
if (currentSpan != null && response.getMetadata() != null) {
currentSpan.setAttribute("ai.usage.total_tokens",
response.getMetadata().getUsage().getTotalTokens());
}
}
}在 application.yml 中暴露自定义 Meter:
@Bean
public MeterBinder aiMetrics() {
return (registry) -> {
Counter.builder("ai.requests.total")
.tag("model", "gpt-4")
.register(registry);
Gauge.builder("ai.response.time", this::getAvgP99Latency)
.register(registry);
};
}配合 Grafana 面板,实时监控 Token 消耗速率 与 API 费用估算,这是 AI SRE 的必备看板。
LLM 提供商偶尔会出现大面积故障或限流。我们必须引入 Resilience4j 实现优雅降级。
@Bean
public Resilience4JProxyFactory resilience4JProxyFactory() {
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(30)) // 整体超时 30s
.build();
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofSeconds(2))
.retryOnResult(response -> response.contains("RateLimitError"))
.retryOnException(e -> e instanceof ConnectException)
.build();
return new Resilience4JProxyFactory(timeLimiterConfig, retryConfig);
}当 AI 调用失败且重试耗尽后,返回静态的“客服待命”消息或基于规则引擎(如 Drools)的硬编码回复,确保前端界面不报错。
@Fallback
public Flux<String> fallbackSummary(String orderId, Throwable ex) {
logger.warn("AI service degraded for order {}, error: {}", orderId, ex.getMessage());
return Flux.just("【温馨提示】当前智能总结繁忙,请稍后重试或联系人工客服。");
}Java 微服务集成 AI 并非简单的 SDK 引入,而是一次架构思维的升级。
Flux 流式返回,否则网关内存会因缓冲大 JSON 而溢出。prompt_tokens 和 completion_tokens,防止内部刷量导致巨额账单。AI 正在重塑后端开发的范式,作为 Java 开发者,我们不必羡慕 Python 的生态。借助 Spring AI 生态和现代 JVM 的强大特性,Java 微服务依然可以在 AI 时代占据一席之地,且凭借其天生的类型安全和卓越的并发模型,更适合承载企业级核心业务。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。