GraphQL Federation:构建统一的微服务API网关

深入讲解GraphQL Federation架构模式,详解Apollo Federation、Schema stitching的实现原理,提供微服务间Schema合并、跨服务查询解析、类型扩展等实战案例。

引言

在微服务架构中,每个服务都有自己的API和数据模型。GraphQL Federation允许我们将多个独立的GraphQL Schema组合成一个统一的API图(Supergraph),让客户端像访问单一服务一样查询所有微服务的数据。

Federation vs Schema Stitching

特性FederationSchema 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)模式。

维度FederationBFF
团队规模>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核心价值:

  1. 服务自治:每个团队独立管理自己的Schema
  2. 统一API:客户端看到单一图,无需关心后端拆分
  3. 类型安全:编译时检查跨服务查询
  4. 灵活演进:通过指令实现平滑的Schema变更

适用场景:

  • 多团队协作的大型微服务系统(>3 个后端团队)
  • 需要统一 API 网关的前端团队
  • 数据关系跨服务、领域模型复杂的系统

不适用场景:

  • 简单 CRUD 应用(REST + Swagger 足够)
  • 团队规模小(< 3 个服务),引入 Federation 增加不必要的复杂度
  • 对查询性能有极致要求(Federation Gateway 拆解开销不可忽略,考虑 REST + 边缘缓存)

生产环境部署检查清单

将 Federation 架构部署到生产环境前,请确认以下事项:

  1. Schema Registry 已配置:每次部署前自动运行 rover subgraph check
  2. ** breaking change 告警**:Schema 删除字段时自动通知相关客户端团队
  3. Gateway 高可用:Apollo Router 至少部署 2 个实例,前置负载均衡器
  4. 查询深度限制:配置 max_query_depth: 15,防止递归攻击
  5. Persisted Queries:生产环境仅接受预注册查询,防止恶意复杂查询
  6. 超时配置:子图超时 10 秒,Gateway 总超时 30 秒,超时时返回 partial data
  7. 监控指标:子图响应时间 P99、查询计划复杂度、缓存命中率、错误率按子图分解
  8. 降级策略:子图不可用时,Gateway 返回可用字段 + 错误信息,而非完全失败

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「backend」更多文章

  1. 零信任安全架构:从边界防御到身份中心的安全范式
  2. 混沌工程实践:构建高可用系统的故障注入与弹性测试
  3. 流式数据处理:Kafka Streams与Flink实战指南