Java 微服务架构设计与 Spring Cloud 实:从一个真实任务开始做
“从一个真实任务开始做”首先要落到可观察、可回滚的工程动作上。本文从配置、调用链和运行指标三个层面梳理判断方法,重点说明应先收集什么证据、怎样做小范围验证,以及何时应停止扩张改动。
1. 拆掉大单体:上下文检索请求在微服务链路上的履约过程
在没拆分前,一个完整的 RAG 请求往往要经历“接收用户输入 -> 提取关键词 -> 调 Vector DB 查向量 -> 查 MySQL 补全业务数据 -> 拼接 Prompt -> 调 LLM -> 返回结果”的全过程。在 Java 的 Servlet 阻塞模型里,这个链路极其脆弱。
我们要做的第一件事,就是把这个臃肿的过程拆解为三个职责明确的子服务:
- RAG Gateway / Orchestrator(编排服务):负责接收请求、校验权限、组装 Prompt,并通过 WebClient/SSE 与前端维持长连接。
- Vector Retrieval Service(向量检索微服务):专门对接 Milvus 或 PGVector,只做纯粹的高维向量相似度计算,返回 Doc ID 列表。
- Domain Data Provider(业务数据微服务):根据 Doc ID 批量回表查询明细,应用业务敏感词与权限过滤。
这种拆分的好处是隔离了故障。即使向量数据库因为高并发检索打满 CPU,也只是影响 Vector Search 微服务 的响应速度,不会直接拉垮整个系统的用户登录与订单服务。
2. Gateway 与 Orchestrator 的分工:哪些校验必须在最外层卡死
在 Spring Cloud 架构中,Spring Cloud Gateway 扮演着守护神的角色。但在引入 AI 上下文编排后,很多人容易把 Gateway 和 Orchestrator 的功能混为一谈。
Gateway 应该只关注网络层与通用安全的卡口:
- Token 桶限流:针对单个 IP 或用户 ID 实施严格的 QPS 限制,防止恶意脚本刷接口打爆大模型 Billing。
- Payload 长度校验:在网关层就直接拒绝超大 Body 请求(比如单个 Request Body 超过 5MB),根本不给它进入后端分配内存的机会。
- JWT 鉴权透传:解析 Header 中的 Token,提取
tenant_id和user_id并挂载在 Header 中向下游传递。
而 Orchestrator(编排服务)才负责真正的业务逻辑校验:
@RestController
@RequestMapping("/api/v1/rag")
public class RagOrchestratorController {
private final VectorRetrievalClient vectorClient;
private final DomainDataClient dataClient;
private final LlmStreamClient llmClient;
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestBody ChatRequest request, @RequestHeader("X-User-Id") String userId) {
// 1. 业务级 Prompt 安全校验
if (PromptSafetyFilter.isUnsafe(request.getPrompt())) {
return Flux.just("请求内容触发安全策略,无法回答。");
}
// 2. 并行发起向量检索与历史上下文获取
Mono<List<String>> docsMono = vectorClient.searchAsync(request.getPrompt(), 5);
Mono<List<ChatMessage>> historyMono = dataClient.getRecentHistoryAsync(userId, 10);
// 3. 响应式组装并流式输出
return Mono.zip(docsMono, historyMono)
.flatMapMany(tuple -> {
String fullPrompt = PromptBuilder.build(request.getPrompt(), tuple.getT1(), tuple.getT2());
return llmClient.streamComplete(fullPrompt);
});
}
}
注意这里必须改用 Spring WebFlux 的响应式编程模型(Flux / Mono)。传统的 Spring MVC 阻塞线程模型在面对这种长达几秒乃至十几秒的 LLM SSE 请求时,几百个线程很快就会被耗尽。
3. 向量检索与缓存层拆分:Spring Boot Starter 调用的内存陷阱
很多 Java 开发者喜欢直接在工程里引入 SDK,比如官方的 Milvus Java SDK 或者 LangChain4j 的 Starter。如果不加约束地直接在 Spring Bean 里调用,极其容易引发 JVM 堆外内存泄漏。
因为向量 SDK 底层往往依赖了 Netty 或者 JNI C++ 库做高维向量压缩,这些内存是不受 JVM -Xmx 参数约束的。如果高并发下频繁创建客户端连接,JVM 还没报错,Pod 已经因为触发 OOMKilled 被 K8s 物理抹杀了。
解决这个问题的最小可行方案是建立二级缓存与独立连接池:
- 热点 Token / Query 缓存:大部分用户提问有很高的重复度(比如“怎么退货”、“发票怎么开”)。在请求打到向量数据库之前,先过一层 Redis。Redis 里存 Query 的 MD5 对应的 Doc ID 结果。
- 连接池隔离:专门为向量检索服务配置独立的 Netty 线程池,严禁与 Spring MVC 共享通用线程池。
spring:
cloud:
feign:
client:
config:
vector-service:
connectTimeout: 1000
readTimeout: 3000
loggerLevel: basic
配合 Resilience4j 的舱壁隔离(Bulkhead),限定调用向量服务的最大并发线程数。哪怕 Milvus 卡住,响应也只会快速失败降级返回基础回答,而不是把整套 Java 进程拖垮。
4. 最小可运行验证:压测数据与关键指标追踪
架构拆分完成后,我们需要用压测来验证这个最小可运行方案的稳定性。
不要一上来就用 JMeter 压 1000 并发,AI 微服务场景下的压测要分阶段做:
- 阶段一:向量检索单点摸底。只压
Vector Search 微服务,观察在 50~100 并发下,p99 延迟是否能稳定在 200ms 以内,观察 CPU 核心利用率与堆外内存占用。 - 阶段二:全链路 Mock 大模型压测。把实际的 LLM 替换为能够稳定在 100ms 返回 Chunk 的 Mock 服务,重点测试 Spring Cloud Gateway 到 Orchestrator 的 SSE 连接保持能力。
在 Grafana 面板上,你需要重点盯防以下三个指标口径:
- Orchestrator 活跃 SSE 连接数:观察连接数是否随着客户端断开而及时释放,有没有发生
Netty Leak。 - Vector DB 命中率与 Timeout 比例:设置 1.5 秒硬超时,一旦超时率超过 2%,说明向量索引需要重建或分片扩容。
- 线程池等待队列深度:针对数据补全服务的 Feign 线程池,监控 wait count。如果队列积压严重,说明业务 API 回表查询成为了瓶颈。
从一个最简单的任务切入,把链路拆透、把响应式流式管道搭稳,Java 微服务才能在 AI 时代继续担当支撑核心业务的基石。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/dicky_zhang3/article/details/163996735



