返回首页
⚙️ 后端 / 架构

GraphQL Federation 实战:从单体到分布式 GraphQL

GraphQL Federation 让多个服务的 GraphQL 合并成单一 API。本文演示从单体 GraphQL 迁移到 Federation 的完整流程。

GraphQL · Federation · 微服务 · API
📰

今日技术简讯

📰 技术简讯 · 2026-06-24

今日聚合 6 条热门技术内容。

🤖 AI / LLM

1. OpenAI 推出 Realtime Voice API

⚙️ 后端 / 架构

2. GraphQL Federation 实战

3. Apache Kafka 4.0 GA

🎨 前端 / Web

4. SvelteKit 2.5 发布

🚀 独立开发 / OPC

5. Lemon Squeezy 推出 Tax Automation

6. Cal.com 推出 White Label


数据来源:HN / Reddit / 各厂博客 采集时间:2026-06-24 09:00 (UTC+8)

📝

今日深度文

GraphQL Federation 实战:从单体到分布式 GraphQL

一句话结论:GraphQL Federation 是"GraphQL 版的微服务"。它解决了"前端复杂查询"与"后端独立演进"的矛盾。

背景

GraphQL Federation 是 Apollo 提出的"分布式 GraphQL"标准。它解决的问题:

单团队 GraphQL:
- 一个团队维护 schema
- 后端紧耦合
- 难以拆分

GraphQL Federation:
- 多个团队各自的 subgraph
- Gateway 合并 schema
- 各团队独立演进

核心概念

Subgraph(子图)

每个子服务定义自己的 GraphQL schema:

# users-service/schema.graphql
type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
}

type Query {
  user(id: ID!): User
}

Supergraph(超图)

Gateway 把所有子图合并:

# 自动生成的 supergraph
type User @key(fields: "id") {
  id: ID!
  name: String!         # 来自 users-service
  email: String!        # 来自 users-service
  posts: [Post!]!       # 来自 posts-service
}

Entity(实体)

跨服务的对象(User / Product 等)通过 entity 关联:

# posts-service/schema.graphql
type Post @key(fields: "id") {
  id: ID!
  title: String!
  author: User!  # 跨服务引用 User
}

extend type User @key(fields: "id") {
  id: ID! @external
  posts: [Post!]!
}

实战:搭建 Federation 系统

架构

┌─────────────────┐
│  Client (App)   │
└────────┬────────┘
         │ GraphQL query

┌─────────────────┐
│   Router        │  ← Apollo Router / Gateway
│  (Supergraph)   │
└────────┬────────┘
         │ 分发
   ┌─────┼─────┬─────────┐
   ↓     ↓     ↓         ↓
 users  posts products  reviews

Step 1:Users Subgraph

// users-service/src/server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';

const typeDefs = `#graphql
  extend schema
    @link(url: "https://specs.apollo.dev/link/v1.0")
    @link(url: "https://specs.apollo.dev/key/v0.2", 
          import: ["@key", "@requires", "@external"])

  type User @key(fields: "id") {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    user(id: ID!): User
    users: [User!]!
  }
`;

const resolvers = {
  Query: {
    user: (_, { id }) => db.users.findUnique({ where: { id } }),
    users: () => db.users.findMany(),
  },
  User: {
    __resolveReference: (ref) => 
      db.users.findUnique({ where: { id: ref.id } }),
  },
};

const schema = buildSubgraphSchema({ typeDefs, resolvers });

const server = new ApolloServer({ schema });
const { url } = await startStandaloneServer(server, {
  listen: { port: 4001 },
});

Step 2:Posts Subgraph

// posts-service/src/server.ts
const typeDefs = `#graphql
  type Post @key(fields: "id") {
    id: ID!
    title: String!
    content: String!
    authorId: ID!
  }

  extend type User @key(fields: "id") {
    id: ID! @external
    posts: [Post!]!
  }

  type Query {
    post(id: ID!): Post
    postsByAuthor(authorId: ID!): [Post!]!
  }
`;

const resolvers = {
  Query: {
    post: (_, { id }) => db.posts.findUnique({ where: { id } }),
    postsByAuthor: (_, { authorId }) => 
      db.posts.findMany({ where: { authorId } }),
  },
  User: {
    posts: (user) => 
      db.posts.findMany({ where: { authorId: user.id } }),
  },
  Post: {
    __resolveReference: (ref) => 
      db.posts.findUnique({ where: { id: ref.id } }),
  },
};

const schema = buildSubgraphSchema({ typeDefs, resolvers });
// 端口 4002

Step 3:Products Subgraph

// products-service/src/server.ts
const typeDefs = `#graphql
  type Product @key(fields: "id") {
    id: ID!
    name: String!
    price: Float!
  }

  extend type User @key(fields: "id") {
    id: ID! @external
    favoriteProducts: [Product!]!
  }

  type Query {
    product(id: ID!): Product
  }
`;
// 端口 4003

Step 4:Router / Gateway

# router-config.yaml
supergraph:
  introspection: true
  
federation_version: =2.5.0

routers:
  - port: 4000
# 用 Apollo Router
./router --config router-config.yaml --supergraph supergraph.graphql

或者用 GraphOS Studio 部署 supergraph。

Step 5:客户端查询

# 客户端发起一个查询,自动跨服务合并
query GetUserWithPostsAndProducts {
  user(id: "123") {
    name
    email
    posts {
      title
      content
    }
    favoriteProducts {
      name
      price
    }
  }
}

Router 自动

  1. 调用 users-service 获取用户基本信息
  2. 调用 posts-service 获取 posts
  3. 调用 products-service 获取 favoriteProducts
  4. 合并成一个响应

高级特性

@requires(跨服务字段验证)

type Post @key(fields: "id") {
  id: ID!
  title: String!
  author: User!
  isPublic: Boolean!  # 只有 author 标记为 public 时才返回
}

extend type User @key(fields: "id") {
  id: ID! @external
  isVerified: Boolean! @external
}

extend type Post @key(fields: "id") {
  id: ID! @external
  isPublic: Boolean! @requires(fields: "author { isVerified }")
}

@provides(减少跨服务调用)

extend type User @key(fields: "id") {
  id: ID! @external
  email: String! @external
  posts: [Post!]! @provides(fields: "title")
}

性能优化

1. 数据加载器

import DataLoader from 'dataloader';

// 批量加载,避免 N+1
const userLoader = new DataLoader(async (ids) => {
  const users = await db.users.findMany({
    where: { id: { in: ids as string[] } },
  });
  return ids.map(id => users.find(u => u.id === id));
});

2. 持久化查询

// 客户端使用预编译的查询 ID
const QUERY_GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      posts { title }
    }
  }
`;

// 通过 Apollo Studio 配置允许的查询
// 防止恶意查询

3. 缓存

// Gateway 级别缓存
const { ApolloServer } = require('@apollo/server');
const responseCachePlugin = require('@apollo/server-plugin-response-cache').default;

const server = new ApolloServer({
  schema,
  plugins: [
    responseCachePlugin({
      sessionId: async ({ request }) => request.headers.get('user-id'),
      maxAge: 300, // 5 分钟
    }),
  ],
});

5 个常见坑

坑 1:Entity 字段不一致

// ❌ User.id 在不同 subgraph 是 string vs int
// Type mismatch 错误

// ✅ 统一约定(全部 string)

坑 2:循环依赖

// ❌ User 需要 Post,Post 需要 User.author
// → 死循环

// ✅ 拆解依赖
// User 只负责用户信息
// Post.author 是单独查询

坑 3:N+1 问题

// ❌ 1000 个 Post,每个都单独查 author
const Post = {
  author: async (post) => db.users.findUnique({ where: { id: post.authorId } }),
};

// ✅ 用 DataLoader 批处理
const userLoader = new DataLoader(...);
const Post = {
  author: async (post) => userLoader.load(post.authorId),
};

坑 4:版本管理

// 不同 subgraph 独立部署
// 但 schema 必须保持兼容

// ✅ 用 Federation SDL 描述
// ✅ CI 检查 schema 兼容性

坑 5:监控困难

分布式 GraphQL 调试比单体复杂:
- 一个查询跨多个服务
- 需要 tracing

✅ 用 Apollo Studio / OpenTelemetry

何时用 Federation

✅ 适合

  • 团队 > 10 人,多团队协作
  • 不同团队维护不同业务模块
  • 已有 GraphQL 经验

❌ 不适合

  • 小项目(单体 GraphQL 足够)
  • 团队不熟悉 GraphQL
  • 强一致性要求(分布式 GraphQL 弱一致性)

我的看法

GraphQL Federation 不是"GraphQL 替代品",而是"GraphQL 演进":

  1. 小项目:单体 GraphQL(一个团队)
  2. 中项目:模块化 GraphQL(一个仓库,多模块)
  3. 大项目:Federation(多团队)

不要为了"分布式"硬上 Federation

  • 团队 < 5 人,单体足够
  • 没有跨团队 schema 合并需求,先单团队 GraphQL
  • 迁移时机:团队拆分成多个独立小组

未来值得关注:

  • Federation 3.0:更简化的 subgraph 协议
  • Cosmo Router:开源 Federation Router
  • GraphQL Fusion:新规范,可能统一 Federation 生态

参考


本文示例基于 Apollo Federation 2.5 + GraphQL 16.x,2026 年 6 月最新版本。

📚 同主题文章

⚙️ 后端 / 架构 分类更多