引言
在微服务架构中,每个服务都有自己的API和数据模型。GraphQL Federation允许我们将多个独立的GraphQL Schema组合成一个统一的API图(Supergraph),让客户端像访问单一服务一样查询所有微服务的数据。
Federation vs Schema Stitching
| 特性 | Federation | Schema Stitching |
|---|---|---|
| 所有者 | Apollo(标准化) | 社区(多种实现) |
| Schema控制 | 各服务自治 | 集中式合并 |
| 扩展性 | 高 | 中 |
| 类型扩展 | ✅ 支持 | 需手动配置 |
| 工具链 | Apollo Studio | 自定义 |
Apollo Federation架构
┌─────────────────────────────────────────────────────┐
│ Gateway (Router) │
│ 统一的GraphQL入口点 │
└──────────────┬──────────────────────────────────────┘
│
┌──────────┼──────────┬──────────────┐
↓ ↓ ↓ ↓
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ User │ │ Order │ │Product │ │Review │
│Service │ │Service │ │Service │ │Service │
└────────┘ └────────┘ └────────┘ └────────┘
子图(Subgraph)定义
用户服务
# users-service/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
username: String!
email: String!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
users(limit: Int = 10, offset: Int = 0): [User!]!
}
// users-service/index.js
const { ApolloServer } = require('@apollo/server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { gql } = require('graphql-tag');
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
type User @key(fields: "id") {
id: ID!
username: String!
email: String!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
}
`;
const resolvers = {
User: {
// Federation解析器:通过ID获取完整User
__resolveReference: async (reference) => {
return await userService.getUserById(reference.id);
},
},
Query: {
user: async (_, { id }) => {
return await userService.getUserById(id);
},
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});
订单服务(扩展User类型)
# orders-service/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@external", "@requires"])
# 扩展User类型(定义在其他服务)
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order!]!
totalSpent: Float!
}
type Order @key(fields: "id") {
id: ID!
user: User!
items: [OrderItem!]!
totalAmount: Float!
status: OrderStatus!
createdAt: DateTime!
}
type OrderItem {
product: Product!
quantity: Int!
unitPrice: Float!
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}
type Query {
order(id: ID!): Order
ordersByUser(userId: ID!): [Order!]!
}
// orders-service/resolvers.js
const resolvers = {
Order: {
__resolveReference: async (reference) => {
return await orderService.getOrderById(reference.id);
},
user: (order) => {
// 返回User的引用(仅包含key字段)
return { __typename: 'User', id: order.userId };
},
items: async (order) => {
return await orderService.getOrderItems(order.id);
},
},
User: {
// 扩展User的orders字段
orders: async (user) => {
return await orderService.getOrdersByUserId(user.id);
},
totalSpent: async (user) => {
return await orderService.getTotalSpentByUserId(user.id);
},
},
};
产品服务
# products-service/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
description: String
category: Category!
reviews: [Review!]!
averageRating: Float
}
type Category @key(fields: "id") {
id: ID!
name: String!
products: [Product!]!
}
extend type OrderItem @key(fields: "id") {
id: ID! @external
product: Product!
}
type Query {
product(id: ID!): Product
products(categoryId: ID, limit: Int = 20): [Product!]!
}
Gateway配置
Apollo Router
# router.yaml
supergraph:
listen: 0.0.0.0:4000
introspection: true
subgraphs:
users:
routing_url: http://users-service:4001/graphql
schema:
subgraph_url: http://users-service:4001/graphql
orders:
routing_url: http://orders-service:4002/graphql
schema:
subgraph_url: http://orders-service:4002/graphql
products:
routing_url: http://products-service:4003/graphql
schema:
subgraph_url: http://products-service:4003/graphql
reviews:
routing_url: http://reviews-service:4004/graphql
schema:
subgraph_url: http://reviews-service:4004/graphql
telemetry:
metrics:
prometheus:
enabled: true
listen: 0.0.0.0:9090
# 启动Apollo Router
router --supergraph supergraph.graphql --config router.yaml
# 或使用Rover CLI生成supergraph
rover supergraph compose --config supergraph.yaml > supergraph.graphql
跨服务查询
查询示例
# 客户端查询(Gateway自动拆解分发)
query GetUserWithOrders {
user(id: "123") {
id
username
email
orders {
id
totalAmount
status
items {
product {
name
price
reviews {
rating
comment
}
}
quantity
}
}
totalSpent
}
}
查询执行计划
Query Plan:
Sequence {
Fetch(service: "users") {
user(id: "123") {
id username email
}
}
Parallel {
Flatten(path: "user") {
Fetch(service: "orders") {
... on User { id }
orders { id totalAmount status }
totalSpent
}
}
Flatten(path: "user.orders.@.items") {
Fetch(service: "products") {
... on OrderItem { id }
product { name price }
}
}
Flatten(path: "user.orders.@.items.@.product") {
Fetch(service: "reviews") {
... on Product { id }
reviews { rating comment }
}
}
}
}
类型扩展指令
# @key: 定义实体的唯一标识
type User @key(fields: "id") { ... }
# 复合key
type ProductVariant @key(fields: "productId size") { ... }
# 多个key(支持多种查找方式)
type User @key(fields: "id") @key(fields: "email") { ... }
# @external: 标记外部定义的字段
extend type User @key(fields: "id") {
id: ID! @external
email: String! @external # 从其他服务获取
}
# @requires: 声明依赖的外部字段
extend type Product @key(fields: "id") {
id: ID! @external
weight: Float @external
shippingCost: Float @requires(fields: "weight")
}
# @provides: 声明本服务能提供的额外字段
extend type Order @key(fields: "id") {
id: ID!
product: Product! @provides(fields: "name price")
}
性能优化
查询缓存
// Gateway层查询缓存
const { InMemoryLRUCache } = require('@apollo/utils.keyvaluecache');
const cache = new InMemoryLRUCache({
maxSize: 100 * 1024 * 1024, // 100MB
});
// 基于查询签名缓存响应
async function cachedQuery(query, variables) {
const signature = hashQuery(query, variables);
const cached = await cache.get(signature);
if (cached) {
return JSON.parse(cached);
}
const result = await executeQuery(query, variables);
// 根据TTL策略缓存
await cache.set(signature, JSON.stringify(result), {
ttl: result.cacheControl.maxAge || 60,
});
return result;
}
数据加载器(DataLoader)
// 子图服务中使用DataLoader避免N+1查询
const DataLoader = require('dataloader');
const userLoader = new DataLoader(async (userIds) => {
const users = await userService.getUsersByIds(userIds);
return userIds.map(id => users.find(u => u.id === id));
});
const resolvers = {
Order: {
user: (order) => userLoader.load(order.userId),
},
};
实体解析与跨服务引用:@key 与 Reference Resolver
Federation 的核心创新是让分散在各个服务中的类型能够相互引用。@key 指令标记了一个类型的唯一标识字段,Gateway 通过这个标识在不同子图之间"跳转"获取数据。
@key 指令详解
# 基础 key:单字段标识
type User @key(fields: "id") {
id: ID!
name: String!
}
# 复合 key:多字段联合标识
type ProductVariant @key(fields: "productId size color") {
productId: ID!
size: String!
color: String!
sku: String!
}
# 多个 key:支持多种查询路径
type User @key(fields: "id") @key(fields: "email") {
id: ID!
email: String!
name: String!
}
Reference Resolver 实现
当 Gateway 需要在子图 A 中解析子图 B 定义的实体时,它会传递一个"引用对象"(只包含 @key 字段),子图通过 __resolveReference 将其扩充为完整实体:
// users-service/resolvers.ts
const resolvers = {
User: {
// Gateway 传入 { __typename: 'User', id: '123' }
// 子图负责根据 id 查询完整用户信息
__resolveReference: async (reference, context) => {
return await context.dataSources.userAPI.getUserById(reference.id);
},
},
};
// orders-service/resolvers.ts
// 此服务扩展了 User 类型,添加 orders 字段
const resolvers = {
User: {
// 当 Gateway 需要 orders 时,reference 已包含用户 id
orders: async (user, args, context) => {
return await context.dataSources.orderAPI.getOrdersByUserId(user.id);
},
totalSpent: async (user, args, context) => {
return await context.dataSources.orderAPI.getTotalSpent(user.id);
},
},
};
循环引用处理:当 User → Order → Product → Review → User 形成循环时,Gateway 的查询计划器必须检测并防止无限递归。Apollo Federation 通过最大查询深度限制和字段收集标记来解决:
# router.yaml
supergraph:
# 限制单个查询的最大深度(防止递归炸弹)
query_planning:
max_query_depth: 15
Schema 治理与版本兼容性
微服务架构下,多个团队同时修改 Schema 极易引入破坏性变更。Schema Registry 是解决这一问题的中心枢纽。
Apollo Studio Schema Checks
# 在 CI 中自动检查 Schema 变更是否破坏现有查询
rover subgraph check my-graph@current \
--schema ./products-schema.graphql \
--name products
# 输出示例:
# ✓ PASS Schema check passed
# ⚠ HINT Field 'Product.oldPrice' is deprecated, consider removing in 90 days
# ✗ FAIL Removing field 'Product.sku' breaks 3 client operations
破坏性变更检测规则:
| 变更类型 | 严重级别 | 原因 |
|---|---|---|
| 删除字段/类型 | 🔴 Breaking | 客户端查询直接失败 |
| 字段类型收紧(String! → ID!) | 🔴 Breaking | 可能导致运行时类型错误 |
| 添加 required 参数 | 🔴 Breaking | 现有查询缺少该参数 |
| 删除 enum 值 | 🟡 Dangerous | 可能影响客户端 switch 逻辑 |
| 添加字段 | 🟢 Safe | 向后兼容 |
| 标记 @deprecated | 🟢 Safe | 给予迁移窗口期 |
@deprecated 与 @inaccessible 迁移策略
type Product @key(fields: "id") {
id: ID!
name: String!
# 第一步:添加新字段
displayName: String!
# 第二步:旧字段标记废弃,客户端开始迁移
fullName: String @deprecated(reason: "Use displayName instead. Will be removed 2025-03-01.")
# 第三步:隐藏字段(仍在服务端保留,但对客户端不可见)
internalCode: String @inaccessible
}
Schema 演进黄金法则:永远优先"添加"而非"修改";删除字段前必须经过
@deprecated→@inaccessible→ 物理删除的三阶段,给客户端至少 3 个月的迁移窗口。
Gateway 性能优化与 N+1 问题
Federation 的便利不是没有代价的。一个看似简单的查询可能在 Gateway 层拆解为数十个子请求,引发严重的性能问题。
N+1 查询问题
# 客户端查询:获取用户及其订单的商品信息
query {
user(id: "123") {
orders {
items {
product { name price }
}
}
}
}
# Gateway 执行的实际子请求:
# 1. Fetch user from users-service
# 2. Fetch orders from orders-service (for user 123)
# 3. Fetch product-1 from products-service
# 4. Fetch product-2 from products-service
# 5. Fetch product-3 from products-service
# ... 如果有 100 个不同商品,就是 100 次请求!
DataLoader 批量加载
// products-service/dataloaders.js
const DataLoader = require('dataloader');
// 批量加载:将多个单个查询合并为一个 IN 查询
const productLoader = new DataLoader(async (productIds) => {
// 单次查询:SELECT * FROM products WHERE id IN (...)
const products = await productService.getProductsByIds([...new Set(productIds)]);
// 按原始请求顺序返回结果
const productMap = new Map(products.map(p => [p.id, p]));
return productIds.map(id => productMap.get(id) || new Error(`Product ${id} not found`));
});
const resolvers = {
OrderItem: {
product: (item) => productLoader.load(item.productId), // DataLoader 自动批处理
},
};
性能优化策略矩阵:
| 优化手段 | 效果 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| DataLoader | 减少 N+1 至 O(1) | 低 | 所有列表查询 |
| 查询缓存 | 热点查询加速 10-100x | 中 | 读多写少 |
| 并行 Fetch | 减少串行等待 | 低(Gateway 内置) | 无依赖字段 |
| @provides | 避免跨服务跳转 | 低 | 本服务已有所需字段 |
| ** persisted queries ** | 只接受预注册查询 | 中 | 防止恶意复杂查询 |
@provides 优化跨服务跳转
# orders-service 已经缓存了商品的 name 和 price
# 无需再向 products-service 查询
extend type OrderItem @key(fields: "id") {
id: ID!
product: Product! @provides(fields: "name price")
}
# products-service 只需提供 name 和 price
type Product @key(fields: "id") {
id: ID!
name: String! @shareable
price: Float! @shareable
}
联邦认证与授权
Federation 的 Gateway 是统一入口,天然适合作为认证和授权的中心节点。JWT 验证在 Gateway 层完成,解析后的用户上下文传递给各个子图。
# router.yaml 认证配置
authentication:
jwt:
- issuer: "https://auth.example.com"
jwks_url: "https://auth.example.com/.well-known/jwks.json"
header_name: "Authorization"
header_value_prefix: "Bearer "
# 将 JWT claims 注入到子图请求头中
headers:
subgraph:
all:
- propagate:
matching: "x-user-id"
- propagate:
matching: "x-user-role"
# 子图中通过 @requires 实现字段级权限
extend type Order @key(fields: "id") {
id: ID!
# 只有管理员或订单所有者能看到支付详情
paymentDetails: PaymentDetails
@requires(fields: "id")
@authorized(
policy: "OWNER_OR_ADMIN",
arguments: { ownerField: "userId" }
)
}
安全原则:Gateway 负责验证 JWT 的真实性和过期时间,但具体的业务权限(“能否查看这条订单”)应由子图自行判断。Gateway 只传递身份上下文,不做业务授权决策。
Federation vs BFF:何时选择联邦
Federation 不是银枪。团队规模、服务数量和前端复杂度决定了应该选择 Federation 还是传统的 BFF(Backend for Frontend)模式。
| 维度 | Federation | BFF |
|---|---|---|
| 团队规模 | >3 个独立后端团队 | 1-2 个全栈团队 |
| 服务数量 | >5 个微服务 | <5 个服务 |
| 前端多样性 | Web/移动端/iOS/安卓共享 API | 每端有定制需求 |
| Schema ownership | 各团队自治 | 集中管理 |
| 数据关系 | 跨服务实体关系复杂 | 服务内聚,关系简单 |
| 学习成本 | 高(Federation 规范、Apollo 工具链) | 低 |
| 性能天花板 | Gateway 拆解开销 | 直接调用,无中间层 |
决策建议:
- 初创公司(< 20 人后端):REST + BFF,快速迭代
- 中型公司(多业务线,50+ 后端):Federation,团队自治
- 大型公司(> 10 个微服务团队):Federation + Domain Graph(按业务域组织 Subgraph)
Schema版本管理与演进
# 使用@deprecated指令平滑演进
type User @key(fields: "id") {
id: ID!
username: String!
email: String!
# 新字段
displayName: String!
# 旧字段标记为废弃
fullName: String @deprecated(reason: "Use displayName instead")
}
# 使用@inaccessible隐藏内部字段
type Product @key(fields: "id") {
id: ID!
name: String!
internalCode: String @inaccessible
}
总结
GraphQL Federation核心价值:
- 服务自治:每个团队独立管理自己的Schema
- 统一API:客户端看到单一图,无需关心后端拆分
- 类型安全:编译时检查跨服务查询
- 灵活演进:通过指令实现平滑的Schema变更
适用场景:
- 多团队协作的大型微服务系统(>3 个后端团队)
- 需要统一 API 网关的前端团队
- 数据关系跨服务、领域模型复杂的系统
不适用场景:
- 简单 CRUD 应用(REST + Swagger 足够)
- 团队规模小(< 3 个服务),引入 Federation 增加不必要的复杂度
- 对查询性能有极致要求(Federation Gateway 拆解开销不可忽略,考虑 REST + 边缘缓存)
生产环境部署检查清单
将 Federation 架构部署到生产环境前,请确认以下事项:
- Schema Registry 已配置:每次部署前自动运行
rover subgraph check - ** breaking change 告警**:Schema 删除字段时自动通知相关客户端团队
- Gateway 高可用:Apollo Router 至少部署 2 个实例,前置负载均衡器
- 查询深度限制:配置
max_query_depth: 15,防止递归攻击 - Persisted Queries:生产环境仅接受预注册查询,防止恶意复杂查询
- 超时配置:子图超时 10 秒,Gateway 总超时 30 秒,超时时返回 partial data
- 监控指标:子图响应时间 P99、查询计划复杂度、缓存命中率、错误率按子图分解
- 降级策略:子图不可用时,Gateway 返回可用字段 + 错误信息,而非完全失败
延伸阅读
- Apollo Federation Documentation
- GraphQL Gateway Patterns
- Schema Composition
- Hasura Remote Joins
- https://plumephp.com/graphql-subscriptions-realtime-updates/ — GraphQL 订阅实现实时推送
- API 测试策略与契约测试 — 接口自动化测试体系
- https://plumephp.com/websocket-realtime-communication-architecture/ — WebSocket 实时通信架构
- https://plumephp.com/zookeeper-coordination/ — 分布式协调服务
- https://plumephp.com/event-driven-architecture-domain-events/ — 微服务事件驱动架构
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。