把 Schema 拆成多个子图之后,Router 就成了整个系统的"单点心脏":它接收查询、生成 query plan、协调子图往返、拼装响应。Router 一旦变慢或变傻,所有子图再快也没用。本文聚焦 Federation 的运维侧——如何读懂 query plan、如何识别实体解析的隐藏成本、如何做子图健康检查与降级、如何配置缓存与遥测、以及如何做性能调优和容量规划。关于联邦架构的设计原理,可先阅读 https://plumephp.com/graphql-federation/;Router 作为网关的部署形态,可参考 https://plumephp.com/api-gateway-kong-envoy/。
一、从 Gateway 到 Router:运维视角的演进
1.1 架构差异
Apollo Gateway(v1/v2)基于 Node.js,Router 则是 Rust 实现的高性能替代。运维视角的差异不只是"更快",更是"可观测"。
| 维度 | Gateway (Node.js) | Router (Rust) |
|---|---|---|
| 语言/运行时 | Node.js | Rust |
| 冷启动 | 秒级 | 毫秒级 |
| 内存占用 | 高(V8) | 低 |
| 查询计划缓存 | 内存 Map | 更细粒度 + 预热 |
| 遥测 | 插件式 | OpenTelemetry 原生 |
| 多核利用 | 需 cluster | 原生多线程 |
1.2 运维关注的四件事
- 正确性:query plan 是否按预期拆分、实体是否被正确解析。
- 延迟:子图往返次数、串行/并行结构、尾延迟。
- 可用性:单个子图故障时是否降级而非整体失败。
- 可观测:能否定位"慢在哪个子图的哪个字段"。
1.3 典型部署拓扑
# router.yaml —— 最小可用配置
supergraph:
listen: 0.0.0.0:4000
introspection: false # 生产关闭
query_planning:
cache:
in_memory:
limit: 512MB
health_check:
listen: 0.0.0.0:8088
enabled: true
一句话总结:Router 不只是"更快的 Gateway",它把查询计划、子图往返、缓存命中等内部行为变成了可观测指标——这才是运维的抓手。
二、Query Plan 解读
2.1 什么是 query plan
Router 把客户端查询编译成一棵执行树,节点描述"在哪执行、依赖谁、并行还是串行"。
query GetOrderWithUser {
order(id: "o-1") {
id
total
buyer {
id
fullName # 来自 users 子图
}
}
}
对应的 query plan:
QueryPlan {
Sequence {
Fetch(service: "orders") {
{ order(id: "o-1") { __typename id total buyer { __typename id } } }
},
Flatten(path: "order.buyer") {
Fetch(service: "users") {
{ ... on User { __typename id } } =>
{ ... on User { fullName } }
}
}
}
}
2.2 读 plan 的三个要点
| 关键字 | 含义 | 运维含义 |
|---|---|---|
Sequence | 串行执行 | 延迟累加,是优化重点 |
Parallel | 并行执行 | 延迟取最大值 |
Flatten(path:) | 实体展开 | 触发 _entities 往返 |
Fetch(service:) | 子图请求 | 一次网络往返 |
2.3 把 plan 变成可观察对象
# 通过 Router 的 dev 模式或 Apollo Studio 查看 plan
router --dev --config router.yaml
# 或使用 rover 在本地组合并查询
rover supergraph compose --config supergraph.yaml > supergraph.graphql
# router.yaml —— 开启 query plan 日志(谨慎,量大)
plugins:
experimental.expose_query_plan: true
telemetry:
instrumentation:
spans:
router:
attributes:
graphql.plan.node_count: true
一句话总结:query plan 是 Router 的"内心独白"。看不懂 plan,就无法解释"为什么这个查询慢了 300ms"。
三、实体解析与 _entities 往返
3.1 实体解析的本质
Federation 中跨子图取字段,靠的是 _entities 查询:Router 把上游返回的 { __typename, key } 列表发给下游子图,子图按 key 批量解析。
# Router 发给 users 子图的实际请求
query ($representations: [_Any!]!) {
_entities(representations: $representations) {
... on User {
fullName
}
}
}
{
"representations": [
{ "__typename": "User", "id": "u-1" },
{ "__typename": "User", "id": "u-2" }
]
}
3.2 往返成本模型
| 因素 | 影响 | 优化手段 |
|---|---|---|
| 串行层级 | 每层一次 RTT | 减少跨子图依赖深度 |
| 实体数量 | payload 大小 | 上游限制列表长度 |
| 子图 resolver | 是否批量 | 子图内必须用 DataLoader |
| 网络拓扑 | 同机房 vs 跨区 | 子图就近部署 |
3.3 子图侧的批量要求
// users 子图的 __resolveReference 必须批量,否则 N+1
const resolvers = {
User: {
// ✅ 用 DataLoader 批量
__resolveReference: (ref: { id: string }, { loaders }: Context) =>
loaders.userById.load(ref.id),
},
};
一句话总结:
_entities是 Federation 的隐形税——Router 帮你省去了 N+1 的编排,但子图内部仍必须批量,否则税会加倍。
四、子图健康检查与降级
4.1 健康检查的层次
| 层次 | 检查内容 | 频率 |
|---|---|---|
| 进程存活 | HTTP /health | 秒级 |
| Schema 一致 | supergraph 组合成功 | 发布时 |
| 查询可用 | 合成探针查询 | 分钟级 |
| 业务正确 | 关键字段断言 | 分钟级 |
4.2 Router 侧的健康配置
# router.yaml
health_check:
listen: 0.0.0.0:8088
enabled: true
path: /health
# 子图级别的超时与重试
traffic_shaping:
all:
timeout: 10s
global_rate_limit:
capacity: 20000
interval: 1s
subgraphs:
orders:
timeout: 5s
4.3 降级策略
# 用 @override / 熔断实现子图故障时的降级
override_subgraph_url:
# 灰度:把 users 子图切到降级实例
users: http://users-canary:4001/graphql
| 故障场景 | 降级手段 |
|---|---|
| 子图超时 | 返回部分数据 + errors |
| 子图宕机 | Router 返回 errors,其他字段正常 |
| 组合失败 | 拒绝发布,保留上一版 supergraph |
| 高负载 | 限流 + 查询复杂度拒绝 |
一句话总结:Federation 的可用性不等于"每个子图都活着",而是"子图死了,用户仍能拿到能拿的那部分数据"。
五、缓存策略:APQ / 实体 / 响应
5.1 三层缓存
| 缓存层 | 位置 | 命中对象 | 失效方式 |
|---|---|---|---|
| APQ | Router | 查询字符串 | 客户端哈希 |
| Query Plan | Router | 计划树 | Schema 变更 |
| 实体缓存 | Router | _entities 结果 | TTL / key |
| 响应缓存 | CDN / Router | 完整响应 | TTL / 标签 |
5.2 Query Plan 缓存
# router.yaml —— 计划缓存,最便宜的优化
supergraph:
query_planning:
cache:
in_memory:
limit: 512MB
# 预热高频查询
warmed_up_queries:
- query: "query WarmUp { __typename }"
5.3 实体缓存
# 实体缓存:跨请求复用实体解析结果
preview_entity_cache:
enabled: true
subgraph:
all:
enabled: true
ttl: 30s
subgraphs:
users:
ttl: 60s # 用户信息变化少,可长缓存
orders:
ttl: 5s # 订单变化频繁,短缓存
一句话总结:Router 的缓存按"计划 → 实体 → 响应"分层,越靠前的缓存收益越高、失效越简单。
六、遥测与分布式追踪
6.1 OpenTelemetry 原生接入
# router.yaml —— 导出到 OTLP
telemetry:
exporters:
tracing:
otlp:
enabled: true
endpoint: http://otel-collector:4317
instrumentation:
spans:
mode: spec_compliant
router:
attributes:
graphql.operation.name: true
graphql.operation.type: true
subgraph:
attributes:
subgraph.name: true
6.2 关键指标
| 指标 | 含义 | 用途 |
|---|---|---|
apollo_router_http_request_duration | 请求延迟分布 | SLO 监控 |
apollo_router_operations_total | 操作计数 | 流量分析 |
apollo_router_query_planning_duration | 计划耗时 | 缓存命中诊断 |
apollo_router_subgraph_request_duration | 子图延迟 | 定位慢子图 |
apollo_router_cache_hit_total | 缓存命中 | 缓存效果 |
6.3 追踪的 span 结构
Trace: 客户端请求
├── span: router.request (总延迟)
│ ├── span: query_planning
│ ├── span: subgraph.orders (Fetch)
│ └── span: subgraph.users (Flatten → _entities)
│ └── span: resolver.fullName
有了这层结构,“慢"就能被精确定位到"哪个子图的哪个字段”,而不是笼统的"GraphQL 慢"。可观测性的通用方法(指标、日志、追踪三支柱)在 Router 场景下同样适用。
一句话总结:Router 的遥测必须能回答"这次查询花了多少时间在计划、多少时间在子图、多少时间在解析",否则调优就是盲人摸象。
七、性能调优实战
7.1 调优优先级
| 优先级 | 手段 | 典型收益 |
|---|---|---|
| P0 | 开启 query plan 缓存 | 计划耗时降 90% |
| P0 | 子图 resolver 批量(DataLoader) | 消灭 N+1 |
| P1 | 减少串行层级 | 延迟线性下降 |
| P1 | 实体缓存 | 子图往返减少 |
| P2 | 连接复用(HTTP/2、keep-alive) | 降低 RTT |
| P3 | 限流与复杂度控制 | 保护后端 |
7.2 减少串行层级的 Schema 手段
# ❌ 深层串行:order → buyer → company → address
# ✅ 用 @key 让字段就近解析,减少跨图依赖
type Order @key(fields: "id") {
id: ID!
buyer: User! @provides(fields: "fullName") # 就近提供,避免额外往返
}
type User @key(fields: "id") {
id: ID!
fullName: String! @external
company: Company!
}
7.3 Router 资源调优
# router.yaml —— 并发与连接
limits:
http_max_request_bytes: 200000
parser_max_tokens: 15000
parser_max_recursion: 500
traffic_shaping:
all:
deduplicate_query: true # 相同子图查询去重
compression: gzip
# 压测:用 hey 或 k6 观察 Router 在不同并发下的尾延迟
hey -z 60s -c 50 -m POST \
-H "Content-Type: application/json" \
-d '{"query":"query { order(id:\"o-1\"){ id total } }"}' \
http://localhost:4000/
一句话总结:Router 调优的收益排序是"缓存 > 批量 > 并行 > 压缩"——先把免费的缓存打开,再谈架构级优化。
八、容量规划与发布
8.1 容量估算
| 输入 | 说明 |
|---|---|
| QPS 峰值 | 按历史峰值 × 1.5 预留 |
| 平均子图往返数 | 从 plan 统计得出 |
| 子图 RPS | QPS × 平均往返数 × 扇出 |
| Router 实例数 | 按 CPU 与内存压测曲线 |
8.2 发布流程
# 子图发布:先 check 再 publish
- name: Subgraph check
run: |
rover subgraph check my-graph@current \
--schema ./subgraph.graphql \
--name orders
- name: Subgraph publish
run: |
rover subgraph publish my-graph@current \
--schema ./subgraph.graphql \
--name orders \
--routing-url https://orders.internal/graphql
8.3 回滚与灰度
| 场景 | 手段 |
|---|---|
| 子图逻辑回滚 | 重新发布上一版 Schema + 部署旧镜像 |
| Router 回滚 | 回退到上一版 supergraph 与镜像 |
| 灰度 | 多 Router 实例分组 + 流量权重 |
| 紧急熔断 | 在 Router 层对该子图限流 |
# 灰度:新旧 Router 并存,按权重切流
# 通过上层 LB(如 Envoy)配置权重
# 90% → router-v1 (旧 supergraph)
# 10% → router-v2 (新 supergraph)
Federation 的运维是一项系统性工程,它的复杂度不来自单点技术,而来自"分布式 Schema + 分布式执行"的叠加。把 plan 看懂、把往返算清、把健康与遥测做全,Router 才会从"黑盒"变成"可控组件"。而子图内部的 resolver 性能,同样是决定端到端延迟的关键一环。
Router 是 Federation 的执行中枢,也是可观测性的最佳观测点。它既是性能瓶颈的所在地,也是性能优化的最前沿。掌握 query plan、实体往返、缓存分层与遥测四件事,就掌握了联邦架构运维的主动权。
一句话总结
联邦 Router 运维的核心是把"不可见的分布式执行"变成"可见的 query plan + 子图往返 + 遥测指标",然后用缓存和批量把成本压下去。
FAQ
Q1: Router 和 Gateway 应该选哪个?
A: 新项目直接用 Router。Gateway 适合仍在 Node.js 插件生态深度定制的存量系统。Router 的性能、内存与 OTel 支持都更优,迁移成本主要在自定义插件的重写。
Q2: 如何判断一个查询是不是"坏查询"?
A: 看 plan:串行层级深、Flatten 节点多、实体数量大,都是坏味道。再结合限流策略(如 parser_max_tokens、复杂度上限)在入口拦截。
Q3: 实体缓存会导致数据陈旧吗?
A: 会。实体缓存的 TTL 是"一致性 vs 性能"的取舍。对时效敏感的实体(如库存)用短 TTL 或禁用;对低频变化的实体(如用户昵称)可长缓存。
Q4: 子图超时了,Router 会返回什么?
A: Router 会返回 errors 数组,同时保留其他子图成功返回的字段(部分数据)。前提是查询设计允许部分成功——这要求客户端能处理"部分数据 + 错误"的响应形态。
Q5: 生产环境应该开启 introspection 吗?
A: 不应该。Router 配置中显式 introspection: false,改为通过 CI 生成的 supergraph 或 Registry 向客户端分发 Schema,避免攻击者探测内部结构。
相关阅读
- https://plumephp.com/graphql-observability-tracing/ —— 全链路追踪与指标采集
- 微服务专题 —— 服务编排与部署实践
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。