当GraphQL服务从小团队的个人项目成长为企业级应用时,一个单体Schema很快成为瓶颈。几十个开发者同时在同一个Schema上修改,CI合并冲突不断,发布周期被迫对齐——这跟微服务拆分的初衷矛盾。
解决方案有两个方向:Schema Stitching(模式拼接)和GraphQL Federation(联邦)。前者由工具库实现,后者由Apollo推动并形成了一套完整的规范和工具链。
Schema Stitching的基本思路是:每个团队维护自己的GraphQL服务(独立的Schema和端点),然后用一个网关服务把多个Schema"缝合"成一个统一的Schema。
import { stitchSchemas } from '@graphql-tools/stitch'; const gatewaySchema = stitchSchemas({ subschemas: [ { schema: userServiceSchema, url: 'http://users-service:4001/graphql' }, { schema: postsServiceSchema, url: 'http://posts-service:4002/graphql' }, { schema: commentsServiceSchema, url: 'http://comments-service:4003/graphql' }, ], // 合并类型定义 mergeTypes: true, });
Stitching需要手动处理类型合并——比如 User 类型在用户服务和帖子服务中都定义了(用户服务定义 name、email,帖子服务定义 posts),需要指定如何合并。
Stitching的优势是灵活——你可以连接任何GraphQL服务,不要求对方是特定框架。劣势是合并规则需要手动配置,服务多了维护成本高。
Apollo Federation(V2)是目前最成熟的分布式GraphQL方案。它的核心概念是:每个微服务定义自己的子图(Subgraph),通过一个网关(Gateway)组合成超级图(Supergraph),对外暴露统一的Schema。
每个子图只定义自己拥有的类型和字段,以及它需要从其他子图引用的实体(Entity)。实体是跨服务共享的数据单元——比如 User 在用户子图定义了基础字段,帖子子图和评论子图通过 @key 引用它。
# 用户子图 (users-service) type User @key(fields: "id") { id: ID! name: String! email: String! avatarUrl: String } extend type Query { user(id: ID!): User }
# 帖子子图 (posts-service) type Post @key(fields: "id") { id: ID! title: String! author: User! @link(from: "authorId", to: "id") authorId: ID! } extend type User @key(fields: "id") { posts(limit: Int = 10): [Post!]! @requires(fields: "id") }
# 评论子图 (comments-service) type Comment @key(fields: "id") { id: ID! body: String! post: Post! @link(from: "postId", to: "id") postId: ID! author: User! @link(from: "authorId", to: "id") authorId: ID! } extend type User @key(fields: "id") { comments: [Comment!]! }

当帖子子图需要返回 author 字段时,它自己没有用户数据——它只有 authorId。联邦通过实体解析解决这个问题。
网关在执行查询时,会把 author 字段的路由到用户子图。用户子图收到一个"按ID查找用户"的请求,返回用户数据,网关把它组装到帖子结果中。这个过程对客户端完全透明。
// 用户子图的解析器 const resolvers = { Query: { user: (_, { id }) => db.users.findById(id), }, User: { // 实体解析器:网关通过这个函数按key获取实体 __resolveReference: (reference) => { return db.users.findById(reference.id); } } };
| 维度 | Schema Stitching | Apollo Federation |
|---|---|---|
| 配置方式 | 手工指定合并规则 | 声明式 @key 和 @link |
| 服务间通信 | HTTP调用 | 网关调度 |
| 实体解析 | 手工配置 | 自动 |
| 工具链 | @graphql-tools/stitch | Apollo Rover, Studio |
| 社区支持 | 通用 | Apollo生态 |
| 学习曲线 | 较低 | 中等 |
Federation是当前大规模GraphQL架构的主流选择。如果你的组织已经有多个微服务,每个服务有自己的数据域,Federation可以在不改变现有服务架构的情况下,为前端提供一个统一的GraphQL入口。它的代价是引入了网关层和实体解析的复杂度,但这在大多数企业级场景中是值得的。
除了 @key 和 @link,联邦 V2 还提供几个常用指令,理解它们才能写出边界清晰的子图:
@shareable。声明一个字段可以被多个子图提供。比如 name 在用户子图和评论子图都声明为 @shareable,网关会从中选择一个来源,避免"字段归属冲突"错误。
@external。声明某个字段的值由其他子图提供,本子图只引用它。通常配合 @requires 使用:本子图计算一个字段时需要依赖外部字段。
@requires。声明解析本子图字段时,需要先从其他子图取哪些外部字段。比如订单子图要返回 shippingCountry,它依赖用户子图的 country 字段——用 @requires(fields: "country") 告诉网关先取 country。
# 订单子图:需要用户的国家信息来计算运费 extend type User @key(fields: "id") { id: ID! @external country: String @external } type Order @key(fields: "id") { id: ID! total: Float! shipping: ShippingInfo! @requires(fields: "userId") } type ShippingInfo { country: String fee: Float }
联邦不是所有多服务场景的最优解,两个常见的反例:
服务间调用密集但数据域高度重叠。如果两个服务的实体几乎相同,联邦只会制造重复的 @external 声明和实体解析开销,不如直接合并服务或共享数据库视图。
查询模式基本固定。如果前端长期只查固定的几个页面,每个页面跨服务的数据模式稳定,用 BFF 层手写聚合函数反而更直观——联邦的自动组合能力派不上用场,却引入网关、子图注册、版本治理一整套路复杂度。
判断标准很简单:当你的数据图开始"长得像图"——实体之间存在多种交叉引用、不同团队各自扩展同一实体时,联邦的价值才真正体现。在此之前,单体 Schema 或 Schema Stitching 更划算。
另外,联邦的调试成本也不可忽视:跨子图的查询要借助网关的 trace 才能看清每段耗时,团队需要额外的可观测性投入。如果组织还没有建立基本的监控体系,建议先补齐再上联邦,否则出了问题连"慢在哪一段"都定位不了。