Skip to content

服务拆分与租户隔离:边界怎么划才不会返工 ​

1. 本节产出 ​

一份服务拆分方案(含边界判定依据、服务契约、ADR),以及租户隔离的三个层次实现(数据/配额/模型)。

2. 前置依赖 ​

3. 为什么 AI 时代的服务边界和传统微服务不同 ​

传统微服务按「业务领域」拆(订单、库存、用户)。AI 服务不同,因为:

差异影响
共享昂贵的外部依赖(模型、向量库)拆分后重复调用 = 重复花钱
计算密集但无状态(生成)vs 数据密集(检索)两者的伸缩模式完全不同
任务长耗时(Agent 分钟级)与 HTTP 请求超时模型不匹配
效果依赖共享配置(prompt、模型版本)配置分散会导致效果不一致

所以不能简单按业务领域拆。

4. 核心原理 ​

4.1 推荐的拆分维度 ​

按「技术特性 + 变更频率」拆,而不是按业务领域:

1. model-gateway      模型网关(横切,变更极少)
2. rag-service        检索服务(数据密集,随知识库变更)
3. agent-service      编排服务(计算密集,随业务流程变更)
4. ingest-service     灌库服务(批处理,独立伸缩)
5. admin-service      租户/配额/计费(业务配置)
6. eval-service       评测服务(离线,独立部署)

判定依据:

服务为什么独立
model-gateway横切关注点,所有服务共用,变更要极其谨慎
rag-service数据密集,需要靠近向量库,伸缩模式不同
agent-service长耗时任务,需要独立的线程池与超时策略
ingest-service批处理,资源消耗大,不能影响在线
eval-service离线任务,可以停机维护

4.2 一个反例:按业务领域拆的后果 ​

错误拆法:
  order-ai-service    (订单相关的 AI 能力)
  marketing-ai-service(营销相关的 AI 能力)

后果:
  ├─ 两个服务各自接模型 → Key 分散、无法统一限流
  ├─ 两个服务各自建向量库 → 同样的文档灌两遍、Embedding 花两次钱
  ├─ prompt 版本各管各的 → 效果不一致,无法统一优化
  └─ 新增一个业务领域 → 又复制一套

结论:按业务拆会导致 AI 基础设施被复制 N 份。 正确做法是按技术特性拆,业务差异通过配置和 prompt 表达。

4.3 服务契约 ​

yaml
# rag-service 的契约(OpenAPI 片段)
POST /rag/query
  request:
    tenantId: string (required, from JWT)
    query: string
    topK: int (default 5)
    filters: object (optional)
  response:
    chunks: [{ text, score, sourceId, titlePath }]
    rejected: boolean
    tokensUsed: int

# agent-service 的契约
POST /agent/run
  request:
    tenantId, userId, goal, caseType, limits
  response:
    runId, status, answer, trace, tokensUsed
POST /agent/{runId}/approve

契约里必须带 tenantId 和 tokensUsed:前者保证隔离,后者保证成本可归因。

4.4 租户隔离的三个层次 ​

层次内容实现
数据隔离向量库、文档的隔离元数据过滤(02-12)+ 存储前缀
配额隔离Token/金额/速率网关配额(04-03)
模型隔离不同租户可用不同模型路由策略按租户(04-02)

数据隔离是硬要求,另外两个是商业策略。

4.5 数据隔离的三种部署模式 ​

模式做法成本隔离强度
共享库 + 元数据过滤一张表,tenantId 过滤最低中(依赖代码正确性)
共享实例 + 分表/分 schema每租户一个 schema中高
独立实例每租户一套最高最高

推荐默认「共享库 + 元数据过滤」,对有合规要求的租户提供独立 schema 或独立实例(作为付费选项)。

共享模式的前提:必须有自动化断言测试证明隔离生效(02-12 的 TenantIsolationIT)。这是选型能否用共享模式的唯一依据。

5. 代码走查 ​

5.1 服务间调用(带租户透传) ​

java
// agent-service 调用 rag-service
@Service
public class RagClient {

    private final WebClient http;

    public List<Chunk> query(String query, int topK) {
        return http.post()
                .uri(ragServiceUrl + "/rag/query")
                // 关键:租户上下文必须透传
                .header("X-Tenant-Id", TenantContext.require())
                .header("X-Trace-Id", TraceContext.currentId())
                .bodyValue(new RagQuery(query, topK))
                .retrieve()
                .body(RagResponse.class)
                .chunks();
    }
}

租户透传靠 HTTP Header + 服务端 Filter 重建上下文,不能靠参数传递(容易漏)。

5.2 契约测试(防止服务间悄悄改坏) ​

java
// src/test/java/com/example/platform/RagServiceContractTest.java
@Test
void rag服务契约() {
    // 用 Pact 或简单的集成测试固定契约
    var resp = ragClient.query("年假怎么算", 5);

    assertThat(resp.chunks()).isNotNull();
    assertThat(resp.tokensUsed()).isGreaterThan(0);
    // 契约里承诺的字段必须存在,否则 CI 失败
}

5.3 隔离断言(跨服务) ​

java
@Test
void 跨服务的租户隔离() {
    // agent-service 调用 rag-service 时,租户必须被正确传递
    TenantContext.set("acme");
    var r = agentService.run(goal);

    assertThat(r.trace().allChunks())
            .allSatisfy(c -> assertThat(c.tenantId()).isEqualTo("acme"));
}

这个测试很关键:它验证租户上下文在跨服务调用中没有丢失——这是分布式系统里最容易出错的地方。

5.4 ADR 示例 ​

markdown
# ADR-003:服务拆分方式

## 状态:已接受

## 背景
需要支持多个业务团队接入 AI 能力。

## 决策
按技术特性拆分(gateway / rag / agent / ingest / admin / eval),
不按业务领域拆分。业务差异通过 prompt 配置与工具集表达。

## 备选
按业务领域拆分(order-ai / marketing-ai):会导致 AI 基础设施
被复制 N 份,成本与维护成本均不可接受。

## 后果
- 正:基础设施复用、成本可归因、升级一次全生效
- 负:单个服务会成为多个业务的共享依赖,需要更强的可用性保障

6. 跑起来 ​

bash
git checkout ch04-04-service-split
docker compose up -d      # 多服务 + PG + Redis
mvn -q test -Dtest=ServiceSplitIntegrationTest
检查项通过标准
服务可独立部署每个服务能单独重启
租户透传跨服务调用后租户上下文正确
隔离断言跨服务隔离测试通过
契约测试契约变更时 CI 失败
ADR 齐全拆分决策有记录

7. 生产避坑 ​

  1. 不要按业务领域拆 AI 服务。这会导致模型接入、向量库、prompt 版本被复制 N 份,成本和一致性都失控。按技术特性拆,业务差异用配置表达。
  2. 租户上下文跨服务必须靠 Header 透传 + Filter 重建,不能靠方法参数。靠参数传递时,任何一处忘记传就是越权漏洞,而这种漏洞在单服务测试中完全测不出来。
  3. 共享库模式的隔离必须有自动化断言。共享存储 + 元数据过滤的隔离强度依赖代码正确性,任何一处漏过滤就是数据泄露。没有断言测试就不要用共享模式——这是选型的前提条件,不是可选项。

8. 延伸与锚点 ​