Java 程序员第 44 阶段01:大模型微服务拆分,独立服务解耦便于扩容维护,总体设计思路
[从单体到微服务:大模型时代的必然选择](#1-从单体到微服务大模型时代的必然选择)- [大模型微服务拆分的核心驱动因素](#2-大模型微服务拆分的核心驱动因素)
- [总体架构设计:分层与服务边界](#3-总体架构设计分层与服务边界)
- [服务拆分原则与度量指标](#4-服务拆分原则与度量指标)
- [技术选型:Spring Cloud Alibaba 大模型微服务栈](#5-技术选型spring-cloud-alibaba-大模型微服务栈)
- [核心服务清单与职责划分](#6-核心服务清单与职责划分)
- [跨服务通信与数据一致性策略](#7-跨服务通信与数据一致性策略)
- [演进路线:从绞杀者模式到完全解耦](#8-演进路线从绞杀者模式到完全解耦)
- [容量规划与弹性扩容设计](#9-容量规划与弹性扩容设计)
- [本章小结与最佳实践清单](#10-本章小结与最佳实践清单)
1.1 单体架构在大模型场景下的困境
在传统 Java Web 应用中,单体架构(Monolithic Architecture)因其开发简单、部署便捷,长期占据中小型项目的主流地位。一个典型的 Java 单体应用会把用户管理、订单处理、支付回调、报表统计等功能模块全部塞进同一个 WAR 或 JAR 包,共享同一个数据库连接池和同一个 JVM 进程。在没有引入大模型之前,这种架构通常能稳定支撑日活十万级的业务流量。
然而,当业务开始集成大语言模型(LLM)后,单体架构的矛盾会迅速暴露。大模型调用与传统业务调用在资源消耗特征上存在本质差异:一次普通的订单查询耗时通常在 50 毫秒以内、占用几 KB 内存;而一次大模型推理请求动辄耗时 3 到 30 秒、占用数百 MB 显存或内存,且并发能力受限于推理框架的批处理窗口。把这两种调用放在同一个 JVM 进程里,会出现三类典型问题。
第一类是**线程池争抢**。大模型调用通常走 HTTP 长连接,默认的 Tomcat 最大工作线程数是 200,当有 50 个请求同时等待大模型响应时,线程池就被占去四分之一,剩下的传统业务请求可用的并发槽位大幅减少,P99 延迟急剧上升。第二类是**内存压力**。大模型的流式响应需要在内存中累积 token 缓冲区,如果同时处理多个长上下文请求,堆内存会被迅速吃掉,触发频繁 Full GC,进而拖垮整个进程。第三类是**扩缩容错配**。传统业务流量有明显的高峰低谷,适合按 CPU 利用率自动扩缩容;而大模型推理服务更适合按 GPU 利用率或排队深度扩缩容,两者放在一起会导致扩容策略顾此失彼。
1.2 微服务拆分带来的核心收益
把大模型相关能力从单体中剥离,形成独立的微服务,能从根本上解决上述矛盾。拆分后的收益可以归纳为五个维度。
**独立扩缩容**是最直接的收益。AI 推理服务可以部署在带 GPU 的节点上,按请求队列深度独立扩容;Prompt 管理服务部署在普通 CPU 节点上,按 QPS 扩容;两者互不干扰,资源利用率都能做到最优化。在一次实际压测中,我们把推理服务从 4 实例扩到 16 实例,而业务网关保持 8 实例不变,整体成本比"整体扩容"方案节省了 37%。
**故障隔离**是第二个收益。大模型服务依赖外部推理框架或云厂商 API,网络抖动、限流、超时是家常便饭。如果推理逻辑和订单逻辑在同一个进程里,推理服务的超时可能通过线程阻塞传导到订单模块,造成"大模型挂了导致下单失败"的荒谬场景。拆分成独立服务后,通过熔断器和舱壁隔离,推理故障被限制在自身服务边界内,不影响核心交易链路。
**技术栈异构**成为可能。大模型推理层可能用 Python + vLLM 部署效果更好,而业务编排层用 Java 更稳妥。微服务架构允许每个服务选择最合适的技术栈,通过标准 HTTP/gRPC 接口通信,这在单体架构里是无法实现的。
**独立发布节奏**让团队迭代更快。Prompt 模板的调整、向量库索引的重建,都不需要跟着业务系统的发布窗口走,可以独立灰度、独立回滚。
**成本可追溯**是容易被忽视的收益。大模型调用是按 token 计费的,把推理服务独立出来后,可以精确统计每个业务线、每个接口的 token 消耗,为成本优化和内部结算提供数据基础。
1.3 拆分的代价:不能只看收益
当然,微服务拆分不是银弹,它也带来了显著的复杂度成本。服务间通信引入了网络延迟和失败模式;分布式事务比本地事务难处理得多;可观测性需要从"看一个日志文件"升级到"看全链路追踪";运维要从部署一个包变成编排十几个服务。因此拆分必须是有节奏的、有原则的,而不是为了拆而拆。本系列文章的核心目标,就是给出一套可落地的大模型微服务拆分方法论,让收益最大化、代价可控化。
2.1 资源异构驱动:CPU 与 GPU 的物理隔离
大模型微服务拆分的第一个驱动因素是资源异构。在一个完整的 AI 应用里,不同模块对硬件资源的需求差异极大,如下表所示。
|
模块 |
主要资源 |
典型耗时 |
并发特征 |
扩容依据 |
|
------ |
--------- |
--------- |
--------- |
--------- |
|
业务网关/编排 |
CPU |
5-50ms |
高并发 |
QPS |
|
AI 推理服务 |
GPU/大内存 |
1-30s |
低并发长耗时 |
队列深度 |
|
Prompt 管理 |
CPU/DB |
5-20ms |
中高并发 |
QPS |
|
Embedding 向量服务 |
GPU/CPU |
20-200ms |
中并发 |
QPS+GPU利用率 |
|
向量检索服务 |
内存 |
5-30ms |
高并发 |
QPS+内存 |
|
知识库管理 |
CPU/DB |
50-500ms |
低并发 |
业务量 |
如果把这些模块放在同一个进程里,最直接的后果是部署时无法选择合适的机型。你不可能给一台既跑业务网关又跑推理服务的机器同时配 GPU 和高主频 CPU——那样成本会爆炸。物理隔离后,推理服务跑在 GPU 机型上,网关跑在普通 CPU 机型上,各取所需。
2.2 故障域驱动:外部依赖的失败隔离
大模型应用通常依赖多个外部组件:云厂商 LLM API、开源推理框架、向量数据库、OCR 服务等。这些外部依赖的稳定性远低于内部数据库,失败概率可能高出一个数量级。把对这些外部依赖的调用封装在独立服务中,可以通过熔断、降级、重试等手段把故障限制在一个服务边界内,避免扩散。
举个真实案例:某团队把 LLM 调用直接写在订单服务里,某天 LLM 厂商 API 出现间歇性 503,由于没有设置合理超时,订单服务的线程池被逐渐耗尽,最终整个下单链路瘫痪 40 分钟。拆分成独立的推理服务后,类似故障只会触发推理服务的熔断降级,订单服务收到快速失败响应并走降级文案,核心交易不受影响。
2.3 演进驱动:业务模型的不确定性
大模型应用的业务模型还在快速演进中。今天的 Prompt 模板可能下周就要调整,当前的向量检索策略可能下个月就要换成混合检索。如果把所有逻辑耦合在单体里,每一次调整都要重新部署整个应用,风险高、周期长。微服务架构允许这些高频变化的模块独立演进,符合"高频变化的部分应该独立部署"这一架构原则。
2.4 团队协作驱动:康威定律的体现
康威定律指出,系统架构会映射团队的组织结构。大模型应用涉及算法工程师、Java 后端、前端、运维等多种角色。算法工程师更熟悉 Python 和模型推理细节,Java 后端更熟悉业务编排和稳定性保障。把推理逻辑和业务编排拆成不同服务,可以让不同团队各司其职、并行开发,减少跨角色协作的摩擦。
3.1 四层架构总览
大模型微服务的总体架构可以分为四层,从上到下依次是:接入层、编排层、能力层、数据层。下图展示了这一分层结构。
┌─────────────────────────────────────────────────┐
│ 接入层:API网关、鉴权、限流、协议转换 │
├─────────────────────────────────────────────────┤
│ 编排层:业务流程编排、会话管理、降级路由 │
├─────────────────────────────────────────────────┤
│ 能力层:AI推理服务、Prompt管理、Embedding服务、 │
│ 向量检索服务、知识库服务 │
├─────────────────────────────────────────────────┤
│ 数据层:向量数据库、关系数据库、对象存储、缓存 │
└─────────────────────────────────────────────────┘
每一层的职责边界清晰:接入层只负责协议和流量控制,不包含业务逻辑;编排层负责把多个能力服务组合成完整的业务流程;能力层提供原子化的 AI 能力;数据层提供持久化支撑。这种分层的好处是,任何一层的实现变化都不会扩散到其他层,例如把向量数据库从 Milvus 换成 Qdrant,只需要在能力层的向量检索服务内部调整,上层无感知。
3.2 接入层设计要点
接入层通常由 Spring Cloud Gateway 承担,主要职责包括统一鉴权、限流熔断、请求路由、协议转换。在大模型场景下,接入层还需要特别处理流式响应(SSE/WebSocket)的透传。下面是一段网关的流式响应配置示例。
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// AI 推理路由:支持 SSE 流式
.route("ai-inference-route", r -> r
.path("/api/chat/**")
.filters(f -> f
.filter(jwtAuthFilter())
.filter(rateLimitFilter(100)) // 每秒100请求
.requestSize(16L * 1024 * 1024) // 限制16MB
.circuitBreaker(c -> c
.setName("inference-cb")
.setFallbackUri("forward:/fallback/chat")))
.uri("lb://ai-inference-service"))
// Prompt 管理路由
.route("prompt-route", r -> r
.path("/api/prompt/**")
.filters(f -> f.filter(jwtAuthFilter()))
.uri("lb://prompt-service"))
.build();
}
private GatewayFilter rateLimitFilter(int limit) {
return (exchange, chain) -> {
String key = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
// 基于 Redis 的令牌桶限流
return redisRateLimiter(key, limit)
.flatMap(allowed -> {
if (allowed) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
});
};
}
}
需要注意的是,流式响应在网关层不能做缓冲,必须配置为透传模式,否则会破坏 token 逐字输出的用户体验。
3.3 编排层设计要点
编排层是整个系统的"大脑",负责把多个能力服务组合成业务流程。典型的编排场景包括:多轮对话管理(先查历史、再检索知识、最后调用推理)、RAG 流程编排(检索-重排-生成)、工具调用编排(推理-调用外部API-再推理)。编排层需要维护会话状态、处理中间结果、做降级路由,是业务逻辑最集中的地方。
3.4 能力层服务清单
能力层是本次拆分的重点,包含以下独立服务:
- **AI 推理服务**:封装对大模型的调用,支持多模型路由、流式输出、重试降级。
- **Prompt 管理服务**:统一管理 Prompt 模板的版本、变量渲染、AB 测试。
- **Embedding 向量服务**:把文本转向量,支持多模型切换。
- **向量检索服务**:封装向量数据库的增删改查,提供相似度检索能力。
- **知识库管理服务**:管理文档的解析、切分、入库全流程。
后续文章会逐一展开每个服务的设计细节。
3.5 数据层选型
数据层需要支撑多种数据形态:向量数据用 Milvus 或 Qdrant,关系数据用 MySQL,缓存用 Redis,对象存储用 MinIO。数据层本身不构成微服务,而是被能力层的服务调用。这里的关键原则是:每个能力服务只访问自己专属的存储,不跨服务共享数据库,避免存储层耦合。
4.1 五大拆分原则
大模型微服务拆分应遵循以下五条原则,避免常见的拆分陷阱。
**原则一:单一职责且高内聚。** 一个服务只做一件事,且把完成这件事所需的所有逻辑内聚在服务内部。AI 推理服务只负责"把 Prompt 送进模型、把输出返回出来",不负责会话上下文拼接、不负责知识检索。会话拼接属于编排层职责,知识检索属于向量检索服务职责。
**原则二:按业务能力而非技术层切分。** 不要按"Controller 层一个服务、Service 层一个服务、DAO 层一个服务"这样横向切,那是"分布式单体"。正确的切法是按业务能力纵向切,每个服务包含自己完整的 Controller-Service-DAO 栈。
**原则三:数据独占,不共享数据库。** 服务之间不直接访问对方的数据库表,所有数据交换通过 API 进行。这条原则是微服务松耦合的底线,一旦共享数据库,拆分就名存实亡。
**原则四:接口先行,契约稳定。** 服务拆分前先定义接口契约(OpenAPI/Protobuf),契约确定后再各自实现。接口契约的变更需要走版本化,避免破坏性变更导致调用方连锁故障。
**原则五:可独立部署、可独立扩缩容。** 每个服务必须有独立的部署流水线和扩缩容策略,不能依赖其他服务的部署节奏。这是拆分能否带来实际收益的检验标准。
4.2 服务粒度的度量指标
拆分粒度太粗等于没拆,太细则运维成本爆炸。可以用以下指标来校验拆分粒度是否合理。
**服务内聚度(Cohesion):** 衡量一个服务内各模块的相关程度。可以用"功能内聚度"来定性判断——服务内的所有方法是否都在为同一个业务目标服务。如果发现一个服务里有两个完全不相关的功能模块,说明它该继续拆。
**服务耦合度(Coupling):** 衡量服务间依赖的强度。可以用"同步调用链路深度"来量化:A 调 B、B 调 C,深度为 3。理想情况下服务间同步调用深度不超过 2,超过 3 就要考虑用异步事件解耦。
**变更频率一致性:** 一个服务内的所有模块应该有相近的变更频率。如果某个模块每周改一次,其他模块半年才改一次,说明高频模块应该独立出去。
**数据边界清晰度:** 一个服务的数据应该能被一个明确的领域概念覆盖。如果说不清"这个服务的数据属于哪个领域",说明边界还没理清。
下面是一段用于统计服务间调用关系的埋点代码,可用于量化耦合度。
@Aspect
@Component
public class ServiceCallTraceAspect {
private static final String TRACE_HEADER = "X-Trace-Id";
private static final int MAX_DEPTH = 10;
@Autowired
private MeterRegistry meterRegistry;
@Around("@annotation(feign.Client) || @within(feign.Client)")
public Object traceFeignCall(ProceedingJoinPoint pjp) throws Throwable {
String caller = resolveCallerService();
String callee = resolveCalleeService(pjp);
String method = pjp.getSignature().getName();
Timer.Sample sample = Timer.start(meterRegistry);
try {
Object result = pjp.proceed();
recordCall(caller, callee, method, "success", sample);
return result;
} catch (Throwable e) {
recordCall(caller, callee, method, "error", sample);
throw e;
}
}
private void recordCall(String caller, String callee, String method,
String outcome, Timer.Sample sample) {
sample.stop(Timer.builder("service.call")
.tag("caller", caller)
.tag("callee", callee)
.tag("method", method)
.tag("outcome", outcome)
.register(meterRegistry));
}
}
通过 Prometheus 采集这些指标,可以生成服务间调用关系图,直观看到哪些服务耦合过紧、哪些调用链路过深。
4.3 拆分时机的判断
不是所有系统一上来就该拆。当出现以下信号时,才考虑启动拆分:
- 单体应用的发布频率被大模型相关变更拖累,核心业务发布受限。
- 大模型相关请求的故障传导到核心业务,造成非预期不可用。
- 大模型模块的扩容需求和业务模块不一致,被迫整体过度扩容。
- 团队规模增长,多人在同一代码库冲突频繁。
如果以上信号都没有出现,保持单体是更务实的选择。
5.1 整体技术栈
本系列采用 Java 生态最成熟的 Spring Cloud Alibaba 作为微服务基座,结合大模型场景做必要扩展。核心组件选型如下。
|
关注点 |
选型 |
选型理由 |
|
-------- |
------ |
--------- |
|
服务注册发现 |
Nacos |
配置中心一体化,运维简单 |
|
服务通信 |
OpenFeign + Resilience4j |
声明式调用+成熟熔断 |
|
网关 |
Spring Cloud Gateway |
原生支持 SSE 流式 |
|
配置中心 |
Nacos Config |
支持热更新 |
|
链路追踪 |
SkyWalking |
对 Java 侵入小 |
|
指标监控 |
Prometheus + Grafana |
生态成熟 |
|
消息队列 |
RocketMQ |
事务消息支持好 |
|
向量数据库 |
Milvus |
开源、Java SDK 完善 |
|
缓存 |
Redis |
通用 |
5.2 统一的依赖管理
为了确保各服务版本一致,建议用一个统一的 BOM 管理依赖版本。下面是父 pom 的关键片段。
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.llm</groupId>
<artifactId>llm-microservices-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.0</spring-boot.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
<resilience4j.version>2.2.0</resilience4j.version>
<milvus-sdk.version>2.3.4</milvus-sdk.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
5.3 公共 starter 抽取
各服务共享的通用能力(如统一异常处理、链路追踪上下文、大模型调用客户端)应抽取为一个公共 starter,避免代码重复。公共 starter 的命名规范是 `llm-common-starter`,各服务通过引入该 starter 获得统一的基础能力。下面是公共 starter 中统一响应封装的示例。
public class ApiResult<T> {
private int code;
private String message;
private T data;
private String traceId;
public static <T> ApiResult<T> success(T data) {
ApiResult<T> r = new ApiResult<>();
r.code = 0;
r.message = "ok";
r.data = data;
r.traceId = MDC.get("traceId");
return r;
}
public static <T> ApiResult<T> error(int code, String message) {
ApiResult<T> r = new ApiResult<>();
r.code = code;
r.message = message;
r.traceId = MDC.get("traceId");
return r;
}
}
6.1 服务全景
完成拆分后,系统将包含以下服务,各自职责明确。
|
服务名 |
职责 |
主要技术 |
独立扩容依据 |
|
-------- |
------ |
--------- |
------------- |
|
business-gateway |
接入、鉴权、限流 |
Gateway |
QPS |
|
orchestration-service |
业务流程编排 |
Spring Boot |
QPS |
|
ai-inference-service |
大模型推理调用 |
Spring Boot + vLLM |
队列深度 |
|
prompt-service |
Prompt 模板管理 |
Spring Boot + MySQL |
QPS |
|
embedding-service |
文本向量化 |
Spring Boot + ONNX |
QPS+GPU |
|
vector-search-service |
向量检索 |
Spring Boot + Milvus |
QPS |
|
knowledge-service |
知识库管理 |
Spring Boot + MySQL |
业务量 |
6.2 服务间调用关系
服务间调用关系遵循"只允许上层调用下层、不允许同层互调"的规则。具体的调用链路为:网关 -> 编排服务 -> 能力层服务(推理/Prompt/Embedding/检索/知识库)。能力层服务之间不直接调用,如果确实需要组合(如检索后重排),由编排服务负责串联,避免能力层形成网状依赖。
6.3 典型链路:多轮对话
以一次多轮对话请求为例,完整的调用链路如下:
- 客户端请求到达网关,网关完成鉴权和限流。
- 网关路由到编排服务。
- 编排服务调用 Prompt 服务,渲染当前对话的 Prompt 模板。
- 编排服务调用向量检索服务,获取相关知识片段。
- 编排服务把知识片段拼入 Prompt,调用 AI 推理服务。
- 推理服务流式返回结果,编排服务透传给客户端。
这条链路涉及 5 个服务,链路深度为 3(网关->编排->能力层),符合前面提到的"深度不超过 2"的建议需要靠异步化优化——在后续章节会讨论如何用预处理和缓存把关键路径深度降下来。
7.1 同步通信与异步通信的选择
大模型微服务间的通信方式需要按场景区分。同步通信(HTTP/gRPC)适合需要实时结果的场景,如推理调用、Prompt 渲染;异步通信(消息队列)适合解耦和削峰的场景,如知识库文档入库、向量索引重建。
选择同步还是异步的核心判断是:**调用方是否需要立即拿到结果**。推理调用必须同步,因为用户在等回答;知识库文档入库可以异步,因为用户上传文档后不需要立即看到向量化结果。
7.2 同步通信的容错模式
同步调用必须配套容错模式,否则一个下游服务的故障会通过线程阻塞传导到整个链路。Resilience4j 提供了三种核心容错模式:熔断器(CircuitBreaker)、隔离舱(Bulkhead)、限流(RateLimiter)。下面是推理服务调用的容错配置示例。
@FeignClient(name = "ai-inference-service", fallback = InferenceFallback.class)
public interface InferenceClient {
@PostMapping(value = "/v1/chat/completions",
consumes = MediaType.APPLICATION_JSON_VALUE)
@CircuitBreaker(name = "inference", fallbackMethod = "fallbackChat")
@Bulkhead(name = "inference", type = Bulkhead.Type.SEMAPHORE)
@Retry(name = "inference")
InferenceResponse chat(@RequestBody InferenceRequest request);
default InferenceResponse fallbackChat(InferenceRequest req, Throwable t) {
// 降级:返回缓存的历史回答或标准兜底文案
return InferenceResponse.builder()
.content("当前服务繁忙,请稍后重试")
.fallback(true)
.build();
}
}
对应的 application.yml 配置:
resilience4j:
circuitbreaker:
instances:
inference:
sliding-window-size: 20
minimum-number-of-calls: 10
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
permitted-number-of-calls-in-half-open-state: 5
bulkhead:
instances:
inference:
max-concurrent-calls: 30
max-wait-duration: 0
retry:
instances:
inference:
max-attempts: 3
wait-duration: 500ms
这里有几个关键参数需要根据大模型特性调整:熔断器的窗口大小不能太小,因为推理调用本身耗时长,窗口太小会导致统计样本不足;隔离舱的并发数要和推理服务的实际承载能力匹配,而不是和调用方线程池匹配。
7.3 数据一致性:最终一致 + 事件驱动
大模型微服务涉及的数据一致性场景主要有两类:知识库文档状态同步(文档入库后要通知检索服务更新索引)、Prompt 版本发布同步(新版本发布后要通知编排服务刷新缓存)。这类场景适合用"本地消息表 + 消息队列"的最终一致性方案。
核心思路是:业务操作和消息记录在同一个本地事务里写入,然后由一个后台任务轮询消息表、投递到 RocketMQ,消费方消费成功后回写状态。这样即使消息队列暂时不可用,消息也不会丢失,保证了最终一致性。下面是本地消息表的核心实现。
@Service
public class LocalMessagePublisher {
@Autowired
private MessageTableMapper messageMapper;
@Autowired
private RocketMQTemplate mqTemplate;
@Transactional(rollbackFor = Exception.class)
public void publishWithBusiness(String topic, Object payload,
Runnable businessLogic) {
// 1. 执行业务逻辑(同事务)
businessLogic.run();
// 2. 写入本地消息表(同事务)
MessageEntity msg = new MessageEntity();
msg.setTopic(topic);
msg.setPayload(JsonUtils.toJson(payload));
msg.setStatus("PENDING");
msg.setRetryCount(0);
msg.setCreateTime(LocalDateTime.now());
messageMapper.insert(msg);
}
// 定时任务每秒扫描待发送消息
@Scheduled(fixedDelay = 1000)
public void scanAndSend() {
List<MessageEntity> pending = messageMapper.selectPending(100);
for (MessageEntity msg : pending) {
try {
mqTemplate.convertAndSend(msg.getTopic(), msg.getPayload());
messageMapper.markSent(msg.getId());
} catch (Exception e) {
messageMapper.incrementRetry(msg.getId());
if (msg.getRetryCount() >= 5) {
messageMapper.markFailed(msg.getId());
}
}
}
}
}
8.1 不要一步到位
微服务拆分最大的陷阱是"一步到位"式的大爆炸重写。正确的方式是采用绞杀者模式(Strangler Fig Pattern),逐步把能力从单体中剥离,每剥离一块就独立部署、独立验证,确保业务连续性。大模型微服务的演进通常分四个阶段。
8.2 阶段一:旁路验证
在不动现有单体的前提下,先旁路部署一个独立的 AI 推理服务,用少量流量灰度验证。这个阶段的目标是验证独立部署的推理服务在性能、稳定性上是否达标,以及网络通信的延迟是否可接受。验证通过后再进入下一阶段。
8.3 阶段二:能力剥离
把 Prompt 管理、Embedding 服务、向量检索等"纯能力"服务逐个从单体剥离。这些服务业务逻辑相对独立、对外依赖少,剥离风险最低。每剥离一个服务,单体里对应的代码就删除,改为调用新服务,这就是"绞杀"的过程。
8.4 阶段三:编排独立
当所有能力服务都剥离后,再把编排层从单体中独立出来。编排层涉及的业务逻辑最多、依赖最复杂,放在最后剥离风险最低。这一阶段要重点处理会话状态的迁移,把原来存在单体内存里的会话上下文迁到 Redis,实现无状态化。
8.5 阶段四:单体瘦身
最后,原来的单体只剩纯业务逻辑(用户、订单、支付),它本身可以继续保持单体,也可以按需进一步拆分。到这一步,大模型相关能力已经完全解耦,可以独立扩容维护了。
下面用一段代码展示阶段二中"绞杀"时的灰度路由:通过配置控制流量比例,逐步把推理调用从单体内部切换到独立服务。
@Service
public class InferenceRouter {
@Value("${inference.external.enabled:false}")
private boolean externalEnabled;
@Value("${inference.external.ratio:0}")
private double externalRatio;
@Autowired
private LegacyInferenceService legacyService; // 单体内旧实现
@Autowired
private InferenceClient externalClient; // 独立服务客户端
public InferenceResponse infer(InferenceRequest req) {
if (!externalEnabled) {
return legacyService.infer(req);
}
// 按比例灰度
if (ThreadLocalRandom.current().nextDouble() < externalRatio) {
try {
return externalClient.chat(req);
} catch (Exception e) {
log.warn("外部推理服务失败,回退旧实现", e);
return legacyService.infer(req);
}
}
return legacyService.infer(req);
}
}
通过动态调整 `externalRatio` 从 0 逐步到 1,实现平滑切换,出问题随时回退。
9.1 大模型服务的容量特征
大模型微服务的容量规划和传统服务有显著差异。传统服务通常用"QPS × 平均耗时 ÷ 单实例并发数"估算实例数,但大模型推理服务的耗时分布是长尾的(短回答 1 秒、长回答 30 秒),用平均值估算会严重偏差。更合理的做法是用排队论模型:把推理服务看作 M/M/c 排队系统,根据到达率和服务率计算等待时长和所需实例数。
9.2 基于 GPU 利用率的扩容
AI 推理服务的扩容不能只看 CPU 和内存,必须引入 GPU 利用率指标。下面是一段基于自定义指标的扩容规则配置(Kubernetes HPA + 自定义指标)。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ai-inference-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ai-inference-service
minReplicas: 2
maxReplicas: 20
metrics:
- type: Pods
pods:
metric:
name: inference_queue_depth
target:
type: AverageValue
averageValue: "4" # 每实例队列深度超过4则扩容
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 100
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 25
periodSeconds: 60
关键设计点:扩容窗口短(30 秒),因为推理请求积压会快速恶化用户体验;缩容窗口长(300 秒),避免流量波动导致频繁缩扩。队列深度指标由推理服务通过 Prometheus 暴露,比 CPU 利用率更能反映真实负载。
9.3 预热与冷启动优化
大模型推理服务的新实例冷启动较慢(模型加载可能需要 30-60 秒),直接接入流量会导致首批请求超时。解决方案是在 readiness probe 中检查模型是否加载完成,并在实例启动后做一次预热推理,确保接入流量时已就绪。
10.1 总体设计要点回顾
本章确立了整个系列的方法论基础:大模型微服务拆分的核心驱动是资源异构、故障隔离和演进节奏;总体架构分接入层、编排层、能力层、数据层四层;拆分遵循单一职责、按业务能力切分、数据独占、接口先行、可独立部署五原则;演进采用绞杀者模式分四阶段推进。
10.2 最佳实践清单
- **先接口后实现**:拆分前先定义服务间接口契约,用 OpenAPI 描述,各方按契约开发。
- **流式响应透传**:网关和编排层对 SSE 流式响应必须透传,不能缓冲。
- **容错三件套**:每个同步调用都配熔断、隔离舱、重试,参数按大模型特性调优。
- **灰度绞杀**:用比例路由逐步切换流量,保留旧实现作为兜底,随时可回退。
- **队列深度扩容**:推理服务按队列深度而非 CPU 扩容,更贴合长耗时请求特征。
- **预热就绪**:新实例必须预热完成后才接流量,避免冷启动超时。
- **本地消息表**:跨服务数据一致性用本地消息表 + MQ 的最终一致方案,不用强一致事务。
- **指标埋点**:服务间调用关系必须埋点,用数据驱动拆分粒度优化。
- **不共享数据库**:这是底线,一旦共享数据库,拆分就失去意义。
- **异步化降低链路深度**:关键路径的同步调用深度超过 2 就要考虑异步化或预处理。
10.3 后续章节预告
接下来的四篇文章将分别深入拆分的具体环节:第 02 篇讲如何识别拆分边界、确定服务粒度;第 03 篇讲 AI 推理服务的独立化设计;第 04 篇讲 Prompt 管理服务的实现;第 05 篇讲 Embedding 向量服务的设计与部署。每一篇都会给出完整的代码示例和落地经验,帮助读者把方法论转化为可运行的系统。
> 本文是"Java 程序员第 44 阶段"系列的第 01 篇,聚焦大模型微服务拆分的总体设计思路。系列共 05 篇,建议按顺序阅读。
更多推荐


所有评论(0)