在单体应用向微服务演进的过程中,GraphQL 常常面临一个致命矛盾:客户端希望只访问一个统一的 Schema,而后端的数据却散落在数十个独立服务之中。GraphQL Federation(联邦架构)正是为了解决这一矛盾而生——它既保留了 GraphQL 强大的类型系统与声明式查询能力,又允许团队按照业务边界将 Schema 拆分到不同的子服务(Subgraph)中,最终通过网关(Router)将它们动态组合成一个全局视图(Supergraph)。本文将深入 Federation v2 的核心指令、子图设计模式、Apollo Router 生产部署以及 Schema 治理实践,帮助你构建可扩展的联邦 GraphQL 平台。
一、为什么需要 GraphQL Federation
1.1 单体 Schema 的膨胀困境
许多团队在 GraphQL 落地的初期采用单体架构:一个 graphql-server 包裹了数据库、缓存、REST 适配等所有数据源。随着业务增长,Schema 中的 type 定义迅速膨胀到数百甚至上千个,任何字段的修改都需要在同一仓库中协调。这导致以下问题:
- 部署耦合:Schema 的微小变更触发整个服务的重新部署,回滚风险高。
- 团队协作瓶颈:不同业务线的开发者争抢同一个 Schema 的 Code Review 权限,上线节奏被迫统一。
- 性能不可控:一个深层查询可能触发对多个下游系统的串行请求,单体服务难以针对不同数据源做精细优化。
1.2 微服务间的数据边界
将单体拆分为微服务后,每个服务拥有自己的数据库和领域模型。但 GraphQL 的客户端并不关心 users-service 与 orders-service 的边界——它只想通过一次请求拿到「用户的最近三笔订单及每笔订单的商品详情」。传统的 REST 微服务方案需要前端多次调用,或者由 BFF(Backend for Frontend)层手动聚合,导致开发效率与类型安全双双丢失。
1.3 从 Schema Stitching 到 Federation 的演进
在 Federation 出现之前,社区常用 Schema Stitching(由 graphql-tools 提供)来合并多个远程 Schema。其基本思路是将各服务的 Schema 通过网关进行 figurative 拼接,编写自定义的 mergeSchemas 配置,并手动声明 type 之间的解析关系。
Schema Stitching 的局限在于:
- 无原生跨服务引用:两个服务中的同名 type 被视为不同对象,必须手动定义
delegateToSchema实现跳转。 - 缺乏类型级合约:网关无法自动验证子图之间的引用是否合法,运行时才发现字段缺失。
- 扩展性差:当服务数量超过十个后,手动维护 stitching 配置的复杂度呈指数级增长。
Apollo Federation 彻底改变了这一局面。它引入了一套标准化的指令与实体(Entity)机制,让子图之间能够通过 @key 声明可复用的身份标识,网关则自动完成查询规划(Query Planning)与跨服务分发。Federation v2(基于 Apollo Federation 2.0+ 规范)进一步细化了所有权与共享语义,使得子图可以安全地共同定义同一个 type,而由 composition 阶段自动检测冲突。
二、Federation 核心概念
2.1 Supergraph vs Subgraph
- Subgraph(子图):一个独立部署的 GraphQL 服务,拥有自己的 Schema 和解析器(Resolver)。每个子图通常对应一个微服务或领域边界。例如
users-subgraph、orders-subgraph、products-subgraph。 - Supergraph(超图):由所有子图的 Schema 经过 Composition(组合) 算法合并后得到的统一 Schema。客户端只感知 Supergraph,不知道数据实际来自哪个子图。
- Router(路由器):实现 Supergraph 网关的运行时。它接收客户端查询,生成 Query Plan,将子查询并行或串行地路由到对应 Subgraph,再按结构组装返回结果。Apollo Router 是目前最主流的开源实现,基于 Rust 构建,性能远高于早期 Node.js 版本的
@apollo/gateway。
2.2 Entity(实体)与 @key
Entity 是 Federation 中唯一能够在多个子图间共享的类型。一个 type 只要标记了 @key(fields: "id"),就成为实体,其指定的字段构成该实体的复合主键。其他子图可以通过 @key 引用该实体,并在此基础上扩展字段。
# users-subgraph
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.6",
import: ["@key", "@shareable", "@external"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String! @shareable
}
type Query {
me: User
user(id: ID!): User
}
@key 的核心作用:
- 身份标识:告诉网关「
User的全局身份由id唯一确定」。 - 回查能力:当其他子图返回一个
User但只有id时,网关可以凭借@key回到users-subgraph拉取剩余字段(这称为_entities查询)。 - 复合键支持:
@key(fields: "organizationId userId")支持多字段联合主键。
2.3 所有权与字段解析语义
Federation v2 引入了一组精确描述字段所有权的指令,这是与 v1 最本质的区别之一:
| 指令 | 语义说明 |
|---|---|
@shareable | 声明该字段可由多个子图共同定义并解析。若未标记,则字段默认只能属于一个子图。 |
@external | 声明该字段来自其他子图,当前子图的 resolver 不直接提供其值,仅用于 @requires 或 __typename 匹配。 |
@requires(fields: "foo") | 当前字段的解析依赖于本对象上其他子图提供的字段。例如运费计算需要 shippingAddress。 |
@provides(fields: "name") | 当前字段解析器在返回值中已经包含了指定字段,网关无需再发子请求。 |
@override(from: "products") | 将字段的解析职责从指定子图迁移到当前子图,灰度迁移的利器。 |
@inaccessible | 字段存在于子图中,但不上报到 Supergraph,可用于内部调试或过渡。 |
三、Federation v2 vs v1 关键差异
3.1 @shareable 替代 extend type
在 Federation v1 中,如果一个子图想扩展另一个子图的实体,必须使用 extend type:
# v1 风格
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order!]!
}
这带来了两个问题:
- 语义模糊:
extend type到底是指「扩展实体」还是「普通类型扩展」?在大型 Schema 中难以快速判断。 - 所有权冲突:如果两个子图都
extend type User添加了同名字段,composition 不会报错,但在运行时可能产生非预期覆盖。
Federation v2 废弃了 extend type 的强制要求。现在每个子图都独立定义完整的类型,并通过 @shareable 显式声明哪些字段是跨子图共享的:
# v2 风格:orders-subgraph
type User @key(fields: "id") {
id: ID!
orders: [Order!]!
}
如果 users-subgraph 和 orders-subgraph 都定义了 type Query,那么 Query 上的同名字段必须标记为 @shareable,否则 composition 会失败。这种显式优于隐式的设计让冲突在编译期即可发现。
3.2 @interfaceObject 与接口联邦
Federation v2.3 起支持将 Interface 声明为联邦实体:
interface Node @key(fields: "id") {
id: ID!
}
type User implements Node @key(fields: "id") {
id: ID!
name: String!
}
type Product implements Node @key(fields: "sku") {
sku: String!
title: String!
}
在 v1 中,Interface 不能被标记为 @key,因此所有跨服务引用必须下沉到具体 type。v2 的接口联邦允许团队先定义抽象契约(如分页列表中的 Node),再由各子图独立实现,这对中台化的 Schema 设计极为重要。
3.3 更严格的 Composition 校验
v2 的 composition 算法(由 Rust 编写的 @apollo/composition)比 v1 严格得多:
- 检测
@shareable缺失导致的所有权冲突。 - 校验
@requires引用的字段是否真实存在于某子图。 - 禁止
@key引用的字段在对应子图中被标记为@external。
这些检查大大降低了运行时出错的概率。
四、子图设计实战
假设我们要构建一个电商平台的联邦 GraphQL 层,核心领域包括用户(User)、订单(Order)和商品(Product)。下面给出三个子图的完整 Schema 设计,并展示跨服务引用、字段提供与依赖解析。
4.1 用户服务子图(users-subgraph)
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.6",
import: ["@key", "@shareable", "@external"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String! @shareable
phone: String
createdAt: String!
}
type Query {
me: User
user(id: ID!): User
users(ids: [ID!]!): [User]!
}
该子图是 User 实体的权威来源(source of truth),负责用户基础资料的增删改查。email 被标记为 @shareable,因为订单服务可能也需要在退货通知场景中展示用户邮箱,但它并不拥有 email 的写入权。
4.2 订单服务子图(orders-subgraph)
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.6",
import: ["@key", "@shareable", "@external", "@provides", "@requires"])
type Order @key(fields: "id") {
id: ID!
buyer: User! @provides(fields: "id")
items: [OrderItem!]!
total: Float! @shareable
status: OrderStatus!
shippingAddress: String! @external
estimatedDelivery: String! @requires(fields: "shippingAddress")
createdAt: String!
}
type OrderItem {
id: ID!
productSku: String!
quantity: Int!
unitPrice: Float!
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}
type User @key(fields: "id") {
id: ID!
orders: [Order!]!
}
extend type Query {
order(id: ID!): Order
ordersByUser(userId: ID!): [Order!]!
}
设计要点:
Order是订单子图的核心实体,buyer返回User但只有id被提供(@provides(fields: "id"))。这意味着如果客户端只查询orders { buyer { id } },网关无需再去用户服务拉取,节省一次子请求。shippingAddress被标记为@external,它在订单子图中没有自己的 resolver,而是依赖于其他子图(例如 logistics-subgraph)提供。estimatedDelivery使用@requires(fields: "shippingAddress")声明:我的 resolver 需要地址才能算出预计送达时间。User在订单子图中通过@key(fields: "id")被重新声明。这实际上是对User实体的扩展——订单子图没有定义name、email,但它为User新增了orders字段。
4.3 产品服务子图(products-subgraph)
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.6",
import: ["@key", "@shareable", "@override"])
type Product @key(fields: "sku") {
sku: String!
name: String! @shareable
description: String
price: Float! @shareable
category: String!
inventory: Int!
}
type Order @key(fields: "id") {
id: ID!
items: [OrderItem!]!
}
type OrderItem @key(fields: "id") {
id: ID!
product: Product!
quantity: Int!
}
extend type Query {
product(sku: String!): Product
products(category: String): [Product!]!
}
设计要点:
Product以sku为主键,这是电商系统中常见的业务主键。跨服务引用产品时使用sku而非自增 ID。Order再次出现在产品子图中,只为Order增加items字段和其中product的嵌套关联。Order的三个字段(id、items、OrderItem的product)均由产品子图负责解析。- 假设未来业务要求将
price的维护权从products-subgraph迁移到pricing-subgraph,只需在 pricing 子图中定义price: Float! @override(from: "products")即可。Router 会自动将price的请求路由到新子图,旧子图可以在过渡期内保留定义而不产生冲突。
4.4 客户端查询体验
在上述子图组合为 Supergraph 后,客户端可以发起如下自然查询:
query GetUserWithOrders {
me {
id
name
email
orders {
id
total
status
items {
quantity
product {
name
price
}
}
estimatedDelivery
}
}
}
Router 会将这条查询拆分为多条子请求:
- 向
users-subgraph查询me { id name email }。 - 获得
id后,向orders-subgraph查询该用户的订单列表及items { quantity productSku }。 - 获得
productSku后,向products-subgraph批量查询商品详情。 - 如果
estimatedDelivery需要shippingAddress,Router 会先向提供地址的子图获取地址,再调用订单子图的estimatedDeliveryresolver。
整个过程对客户端完全透明,且得益于 Query Plan 与 DataLoader 风格的批量查询,延迟被控制在合理范围。
五、Apollo Router 深度部署
Apollo Router 是 Federation 的参考网关实现,使用 Rust 编写,单线程性能可达 Node.js Gateway 的 5-10 倍。以下给出生产环境常用的配置模式。
5.1 基础配置文件(router.yaml)
supergraph:
listen: 0.0.0.0:4000
# 开启内省的沙箱与查询计划可视化(仅 dev / staging)
introspection: true
# 最大查询深度限制
preview_defer_support: true
health_check:
listen: 0.0.0.0:8088
telemetry:
exporters:
metrics:
prometheus:
enabled: true
listen: 0.0.0.0:9090
tracing:
jaeger:
enabled: true
agent:
endpoint: jaeger-agent:6831
apollo:
# 上报至 Apollo Studio
field_level_instrumentation: 0.1
# 查询计划缓存
query_planner:
cache:
in_memory:
limit: 512
# 操作级限制(开源版内置)
limits:
preview_operation_limits:
max_depth: 10
max_height: 100
max_aliases: 30
max_root_fields: 5
5.2 Header Propagation(请求头透传)
在微服务架构中,认证令牌、链路追踪(trace ID)和租户标识(tenant ID)通常需要在子图之间透传:
headers:
all:
request:
# 1. 透传所有客户端传入的请求头
- propagate:
matching: ^x-.*
# 2. 注入链路追踪 ID(若客户端未提供则生成)
- insert:
name: x-request-id
value: "{{uuid}}"
# 3. 将认证头显式转发给子图
- propagate:
named: authorization
default: ""
# 4. 仅向 orders-subgraph 透传特定头
subgraphs:
orders:
request:
- propagate:
named: x-tenant-id
透传规则支持正则匹配(matching)、显式命名(named)以及默认值(default)。注意:对外暴露的 Header 应通过白名单控制,避免将敏感内部头泄露给客户端。
5.3 JWT 认证集成
Router 支持通过配置直接校验 JWT,无需在 Gateway 层编写自定义代码:
authentication:
router:
jwt:
jwks:
- url: https://auth.example.com/.well-known/jwks.json
poll_interval: 15m
header_name: authorization
header_value_prefix: Bearer
# 可选:将解析后的 claims 注入请求上下文的 key
# 配合 Rhai 脚本或 coprocessor 做细粒度鉴权
# 若 JWT 无效则直接返回 401
若业务需要基于 JWT claims(如 roles、user_id)决定字段可见性,可结合 Rhai 脚本实现:
fn process_request(request) {
let claims = request.context["apollo_authentication::JWT::claims"];
if claims == () {
// 未登录
return;
}
let roles = claims["roles"];
request.context["user_roles"] = roles;
}
然后在 Supergraph 级别或子图级别根据 user_roles 执行 @authenticated 或 @requiresScopes directive(需 Apollo Router 1.28+ 与 GraphOS 企业版或自定义 coprocessor)。
5.4 速率限制
Router 开源版未内置分布式速率限制,但可以通过 preview_operation_limits 与 rhai / coprocessor 组合实现:
limits:
preview_operation_limits:
max_depth: 10 # 防止深层嵌套 DoS
max_height: 100 # 防止返回过大响应
max_aliases: 30 # 防止别名放大攻击
max_root_fields: 5 # 限制 Query 根字段数量
对于基于 IP 或用户的 token bucket 限制,建议在 Router 前架设 Envoy/Kong,或者通过 Coprocessor 将请求摘要发送到 Redis 做计数。
5.5 查询规划(Query Planning)可视化
Federation 的核心魔法在于 Query Planner。当 Router 启动时,它会根据 Supergraph SDL 预计算一个查询计划图;客户端查询到达后,Planner 将查询树拆分为对子图的子请求序列。
开发者可以通过 Apollo Sandbox(http://localhost:4000 开启 introspection 后)查看任意查询的 Query Plan:
Fetch(service: "users") {
query { me { id name email } }
}
-> Flatten(path: "me") {
Fetch(service: "orders") {
query ($ representations: [_Any!]!) {
_entities(representations: $representations) {
... on User { orders { id total status } }
}
}
}
}
-> Flatten(path: "me.orders.@.items") {
Fetch(service: "products") {
...
}
}
通过分析 Query Plan,你可以发现:
- 是否存在瀑布式子请求(一个 Fetch 的结果作为下一个 Fetch 的输入)。瀑布过多意味着并行度不足,可能需要通过
@provides或数据冗余进行优化。 - 某个子图是否成为热点瓶颈,是否应考虑将其高频字段标记为
@shareable并在消费子图缓存。
六、Schema 治理
联邦架构的优势在于分布式开发,但其风险在于失去对全局 Schema 的统一管控。Schema 治理确保各子图的演进不会破坏整体契约。
6.1 Apollo Studio Schema Registry
Apollo Studio(现称 GraphOS)提供了托管的 Schema Registry。每个子图在 CI 中将 Schema 推送到 Registry:
rover subgraph publish my-graph@prod \
--name users \
--schema ./users.graphql \
--routing-url http://users-service:4001
Registry 充当单一事实来源,Router 在启动时(或通过 uplink 定期)拉取最新的 Supergraph SDL,无需本地文件拼接。
6.2 Schema Checks(Breaking Change 检测)
在合并 PR 之前,使用 Rover CLI 执行 Schema Check:
rover subgraph check my-graph@prod \
--name orders \
--schema ./orders.graphql
GraphOS 会将新 Schema 与最近的操作日志(operation signature)进行比对,检测以下问题:
- Breaking Change:删除客户端正在使用的字段、修改变量类型、将非空变为可空。
- Composition Error:
@key引用了不存在的字段、两个子图对同一字段的所有权冲突(缺少@shareable)。 - Performance Hint:某个变更导致 Query Plan 中产生额外的子图请求。
Schema Check 失败时,CI 应该阻断合并,防止破坏性变更进入生产环境。
6.3 Composition 验证与 lint
GraphOS 使用 Rust 编写的 apollo-federation composition 引擎对子图进行组合校验。除运行时检查外,建议在 CI 中本地执行 lint:
rover subgraph lint \
--schema ./products.graphql \
--name products \
--ignore-existing-lint-violations
此外,团队应约定编码规范:
- 所有实体类型统一使用
@key。 - 跨子图共享字段必须显式标记
@shareable。 - 废弃字段使用
@deprecated(reason: "Use newField"),保留至少两个 Sprint 后再删除。 - 禁止使用
_entities和_service在客户端查询中直接暴露(这是 Federation 内部协议)。
七、替代方案概述
尽管 Apollo Federation 生态最为成熟,但以下替代方案在某些场景下值得考虑。
7.1 Schema Stitching(Type Merging)
graphql-tools v8+ 引入了 Type Merging,使得 Schema Stitching 不再依赖手写 resolver delegation,而是自动匹配同名 type:
import { stitchSchemas } from '@graphql-tools/stitch';
const schema = stitchSchemas({
subschemas: [
{ schema: userSchema, executor: userExecutor },
{ schema: productSchema, executor: productExecutor, merge: { Product: { selectionSet: '{ sku }', fieldName: 'product', args: (sku) => ({ sku }) } } }
]
});
Type Merging 适合不想引入 Apollo 全家桶、或者已有大量非 Apollo 服务的团队。缺点是缺乏 Router 级别的查询规划缓存与内建治理工具。
7.2 GraphQL Mesh
GraphQL Mesh 由 The Guild 维护,它的理念是将任意数据源(REST、gRPC、数据库、SOAP)自动转化为 GraphQL Schema,再通过联邦方式组合:
# .meshrc.yaml
sources:
- name: UsersREST
handler:
openapi:
source: ./users-api.json
baseUrl: http://users-service
- name: ProductsREST
handler:
openapi:
source: ./products-api.json
baseUrl: http://products-service
Mesh 的优势在于无需重写下游服务即可获得 GraphQL 接口,非常适合存量系统改造。但其自动生成的 Schema 往往粒度较粗,定制能力不如手写 Federation Subgraph。
7.3 WunderGraph
WunderGraph 采用了一种不同的架构:不暴露通用 GraphQL 端点,而是将每个查询预编译为持久化操作(Persisted Operation)。它在构建时完成所有数据源(REST、GraphQL、数据库)的合并与类型生成,客户端只能调用预注册的操作。
这种设计天然防御了 GraphQL 的深度查询攻击,并提供了极佳的性能和类型安全。代价是失去了 GraphQL 即席查询(ad-hoc query)的灵活性,更适合以 BFF 形式面向前端团队交付。
八、性能优化
联邦架构的性能瓶颈通常集中在网关层。以下是生产环境验证过的优化手段。
8.1 Query Plan 缓存
Apollo Router 默认将 Query Plan 缓存在内存中(见前文 router.yaml 配置)。对于高频查询,应确保所有客户端使用持久化查询(Persisted Queries),这不仅能命中缓存,还能防止未注册查询的执行。启用方式:
preview_persisted_queries:
enabled: true
# 只允许已注册的 queries,拒绝任意 ad-hoc query
log_unknown: true
8.2 并行子请求与批量实体查询
Router 在规划 Multi-Fetch 时,凡是相互独立的子请求都会并行发出。开发者应尽量减少实体链的深度。例如,不要设计 User -> Order -> OrderItem -> Product -> Category -> Brand 这种长达五层的实体链,因为每层都可能引入一次 _entities 批量查询。
在子图实现中,务必为 _entities 查询启用 DataLoader,将短时间内到达的多个 (typename, key) 组合并为一个数据库 SELECT ... WHERE id IN (...) 查询。
8.3 深度查询限制与成本分析
除了 Router 内置的 max_depth 和 max_height,还可在网关层引入查询成本分析(Query Cost Analysis)。基本原理是为每个字段分配权重,查询总成本超过阈值时拒绝执行:
# router.yaml(需企业版或 coprocessor 插件实现)
cost_estimation:
max_cost: 10000
default_field_cost: 1
multiplier_for_lists: 10
开源版可通过 Rhai 脚本实现简易版本:遍历查询 AST,统计列表字段数量,超过阈值时返回错误。
8.4 边缘缓存与 CDN
对于公共读接口(如商品详情、类目列表),可以在 Router 与客户端之间增设 CDN(Cloudflare、Fastly)。方法是将 GraphQL 查询 POST 转为 GET,并利用 Cache-Control 头缓存响应。Router 支持自动为特定字段级 directive 生成缓存标签,实现精准失效。
九、FAQ
Q1: Federation 是否要求所有子图都使用 Apollo Server?
不是。任何符合 Federation Subgraph Spec 的 GraphQL 服务都可以接入 Router。社区提供了 Go (gqlgen)、Java (dgs-framework, graphql-java)、Rust (async-graphql)、Python (strawberry) 等多种实现。
Q2: 实体查询的 N+1 问题如何解决?
Router 通过 _entities 查询将多个引用合并为一次请求;子图服务应当实现 DataLoader,将 representations: [_Any!]! 参数中的多个 key 批量查询。
Q3: @shareable 和 @external 有什么区别?
@shareable 表示「本字段由本服务解析,但其他服务也可以解析相同字段」;@external 表示「本字段由其他服务解析,本服务只是借用其值」。前者用于共同所有权,后者用于依赖读取。
Q4: Federation 支持 Subscription 吗?
Apollo Router 从 1.0 起支持 Subscriptions over WebSocket,使用的协议为 graphql-ws(推荐)与 subscriptions-transport-ws。每个 subgraph 可以独立提供 subscription,Router 负责协议转换与多路复用。但跨 subgraph 的 subscription entity resolution 目前仍有一定限制,建议将 subscription 集中在单一 subgraph 中管理。
Q5: 如何灰度迁移字段到新子图?
使用 @override(from: "old-subgraph")。新子图声明该字段并标记 @override,Router 会自动将请求路由到新子图;旧子图无需立即删除字段定义,待验证稳定后再清理。迁移完成后去掉 @override 即可。
十、一句话总结
GraphQL Federation 通过在子图中显式声明实体、共享语义与解析依赖,让分布式微服务在类型层面「联邦」为一个统一 Supergraph;配合 Apollo Router 的高性能查询规划与 Schema 治理体系,它已成为构建大规模、可演进的 GraphQL 平台的事实标准。
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。