Chapter 09

性能优化:N+1 与缓存

全面优化 GraphQL API 性能,从查询解析到缓存策略

查询深度与复杂度限制

防止恶意深层查询

GraphQL 的灵活性允许客户端构造任意深度的嵌套查询,如果不加限制,攻击者可以构造 O(n^k) 级别的数据库查询:

# 恶意查询示例:指数级复杂度
{
  user(id: "1") {
    friends {
      friends {
        friends {
          friends {
            name  # 深度4,如果每个用户有100个朋友,这是 100^4 次查询!
          }
        }
      }
    }
  }
}
import { createComplexityLimitRule } from 'graphql-query-complexity';
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    // 限制查询深度最多10层
    depthLimit(10),
    // 限制查询复杂度
    createComplexityLimitRule(1000, {
      // 为不同字段设置复杂度权重
      estimators: [
        fieldExtensionsEstimator(),
        simpleEstimator({ defaultComplexity: 1 }),
      ],
    }),
  ],
});

在 Schema 中定义字段复杂度

const typeDefs = `
  type Query {
    users(limit: Int): [User!]! @complexity(value: 5, multipliers: ["limit"])
    expensiveOperation: String! @complexity(value: 100)
  }
`;

持久化查询(Automatic Persisted Queries)

APQ 通过 hash 代替完整查询字符串,减少网络传输量,并允许 CDN 缓存 GET 请求:

// 客户端(Apollo Client)自动处理 APQ
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';

const persistedQueriesLink = createPersistedQueryLink({ sha256 });
const httpLink = new HttpLink({ uri: '/graphql' });

const client = new ApolloClient({
  link: persistedQueriesLink.concat(httpLink),
  cache: new InMemoryCache(),
});

// 首次请求:发送 hash + 查询字符串(服务器缓存查询)
// 后续请求:只发送 hash(节省网络流量)

@cacheControl 指令

type Query {
  # 缓存5分钟(300秒),公开可缓存
  publicPosts: [Post!]! @cacheControl(maxAge: 300, scope: PUBLIC)

  # 不缓存(动态数据)
  notifications: [Notification!]! @cacheControl(maxAge: 0)
}

type Post @cacheControl(maxAge: 3600) {
  id: ID!
  title: String!
  # 动态字段覆盖类型级别的缓存设置
  viewCount: Int! @cacheControl(maxAge: 30)
}
import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    ApolloServerPluginCacheControl({
      defaultMaxAge: 0,      // 默认不缓存
      calculateHttpHeaders: true,  // 自动设置 Cache-Control HTTP 头
    }),
  ],
});

Redis 查询结果缓存

import { KeyvAdapter } from '@apollo/utils.keyvadapter';
import Keyv from 'keyv';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';

// 使用 Redis 作为缓存后端
const server = new ApolloServer({
  typeDefs,
  resolvers,
  cache: new KeyvAdapter(new Keyv('redis://localhost:6379')),
});

// 在 Resolver 中使用缓存
const resolvers = {
  Query: {
    popularPosts: async (_, __, { cache }) => {
      const cacheKey = 'popular-posts';
      const cached = await cache.get(cacheKey);
      if (cached) return JSON.parse(cached);

      const posts = await db.posts.findMany({
        orderBy: { views: 'desc' },
        take: 10,
      });

      await cache.set(cacheKey, JSON.stringify(posts), { ttl: 300 });
      return posts;
    },
  },
};

性能优化策略分层

应用层优化(最高优先级)
DataLoader 消除 N+1 问题(数量级减少查询);按需查询(用 info 参数检查客户端是否请求了某关联字段,避免无效 JOIN);查询复杂度/深度限制防止恶意请求耗尽服务器资源。
缓存层优化
DataLoader 请求级缓存(避免同一请求重复查询同一 ID);@cacheControl 设置 HTTP Cache-Control 头(公开数据可被 CDN 缓存);Redis 缓存热点 Resolver 结果(减少数据库压力);APQ 让 CDN 能缓存 GET 格式的 GraphQL 查询。
数据库层优化
外键字段添加索引(authorId, categoryId 等);分页字段添加复合索引(createdAt + id 用于 Cursor 分页);避免 SELECT *,只查询需要的字段;连接池配置合理(Prisma 默认 5 个连接)。
监控与诊断
Apollo Studio 实时追踪慢查询和高复杂度查询;每个 Resolver 添加 Trace ID 便于关联日志;记录 DataLoader 批处理命中率,评估批处理效果。

APQ 工作流程详解

APQ(Automatic Persisted Queries,自动持久化查询)是一个两步协议:

第一次请求(cache miss)
客户端发送查询的 SHA-256 哈希值;服务器找不到对应查询,返回错误;客户端重新发送哈希 + 完整查询字符串;服务器缓存查询(以哈希为 key),正常响应。
后续请求(cache hit)
客户端只发送哈希值(可用 GET 请求);服务器找到缓存的查询,直接执行;网络传输量大幅减少,查询字符串可长达数 KB。
CDN 缓存加速
使用 GET 方式的 APQ 查询可被 CDN 缓存,配合 @cacheControl 设置缓存时间,纯读查询可完全绕过后端服务器。

性能测量:Apollo Tracing

import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace';

// 开发环境:在响应中内联追踪数据
const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    // 每个 Resolver 的执行时间都会被记录
    ApolloServerPluginInlineTrace(),
  ],
});

// 响应中会附加 extensions.tracing 字段:
// {
//   "data": { ... },
//   "extensions": {
//     "tracing": {
//       "version": 1,
//       "startTime": "2024-01-01T00:00:00.000Z",
//       "duration": 12345678,
//       "execution": {
//         "resolvers": [
//           { "path": ["posts"], "parentType": "Query", "duration": 5000000 },
//           { "path": ["posts", "0", "author"], "parentType": "Post", "duration": 200000 }
//         ]
//       }
//     }
//   }
// }

@defer 与 @stream:增量数据交付

@defer@stream 是 GraphQL 规范实验性指令,用于解决单次查询中某些字段响应慢的问题:不等所有字段就绪,先把已准备好的部分数据推送给客户端,慢速字段在准备好后再追加发送。协议层使用 HTTP Multipart 响应(multipart/mixed)实现。

@defer
标记某个片段或字段"延迟发送"。初始响应先包含未被 @defer 的字段,被 @defer 的字段在服务端就绪后通过后续 chunk 推送。适合同一查询中有快速字段和慢速字段(如文章正文 + 评论列表)。
@stream
标记列表字段"流式发送"——列表中每个元素就绪后立即推送一个 chunk,而不是等整个列表完成。适合大列表的逐步渲染(如搜索结果流式返回),让用户更快看到第一条结果。
HTTP Multipart(多部分响应)
@defer/@stream 的传输机制。服务端返回 Content-Type: multipart/mixed 响应,通过 boundary 分隔多个数据块。Apollo Client 4 原生支持解析此格式;旧版客户端需要升级。

使用 @defer 延迟慢速字段

query ArticleDetail($id: ID!) {
  article(id: $id) {
    id
    title
    content         # 快速字段:立即返回
    author { name } # 快速字段:立即返回

    # @defer:comments 较慢(需要多次数据库查询),延迟发送
    ... on Article @defer(label: "comments") {
      comments {
        content
        author { name }
        likes
      }
    }

    # @defer:推荐文章需要 ML 计算,延迟更久
    ... on Article @defer(label: "recommendations") {
      relatedArticles(limit: 5) {
        id
        title
      }
    }
  }
}
# 客户端收到 3 个响应 chunk:
# 1. 初始响应:{ article: { id, title, content, author } }
# 2. 延迟 chunk(comments):{ data: { comments: [...] }, path: ["article"] }
# 3. 延迟 chunk(recommendations):{ data: { relatedArticles: [...] }, path: ["article"] }

使用 @stream 流式返回列表

query SearchResults($term: String!) {
  search(term: $term) {
    # @stream:列表中每个元素就绪后立即发送,initialCount=3 表示前3个在初始响应中
    results @stream(initialCount: 3) {
      id
      title
      excerpt
      score
    }
  }
}
# 用户可以在所有结果返回前就看到前 3 条,提升感知速度
@defer/@stream 的生产环境注意事项

截至 2025 年,@defer/@stream 在 GraphQL 规范中仍处于 RFC 阶段(Stage 3),各服务端实现(Apollo Server、GraphQL Yoga、Hot Chocolate)已基本支持,但 Apollo Client 需要 3.7+,且需明确启用增量交付。使用前需确认:① HTTP 代理/CDN 不缓冲整个响应(nginx 需关闭 proxy_buffering);② 客户端框架版本兼容;③ 部署平台支持 chunked transfer encoding(AWS Lambda 部分不支持)。

Apollo Federation v2:分布式 GraphQL

Apollo Federation 允许将一个大型 GraphQL API 拆分为多个独立的"子图"(Subgraph),每个子图由不同团队维护,由 Apollo Router(网关)自动合并为统一的 API。这是 GraphQL 的微服务架构方案。

子图(Subgraph)
独立部署的 GraphQL 服务,拥有自己的 Schema 子集(如 Users 子图负责用户相关类型,Products 子图负责商品类型)。子图之间可以通过 @key 指令声明"实体",实现跨子图关联。
Supergraph(超级图)
所有子图合并后对外暴露的统一 Schema。客户端只知道 Supergraph,不知道内部分拆结构。Apollo Router 在运行时将查询拆解、路由到对应子图,再合并结果返回给客户端。
@key 指令
声明类型的主键字段。同一类型可以在不同子图中扩展(@extends)——Users 子图提供 User 基础字段,Orders 子图通过 @key 引用同一 User 类型并添加 orders 字段。
Apollo Router
Federation v2 的网关组件(Rust 实现,高性能)。接受客户端查询,生成查询计划(Query Plan),并行请求涉及的子图,合并结果。替代了 Federation v1 的 Apollo Gateway。

子图 Schema 示例

# Users 子图 Schema(负责用户相关)
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0",
        import: ["@key", "@shareable"])

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

type Query {
  user(id: ID!): User
  me: User
}
# Orders 子图 Schema(负责订单相关,引用 User 类型)
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0",
        import: ["@key", "@external"])

# 引用 Users 子图中的 User 类型,扩展 orders 字段
type User @key(fields: "id") {
  id: ID!          # @key 字段必须声明(即使此子图不提供它)
  orders: [Order!]! # 此字段由 Orders 子图提供
}

type Order @key(fields: "id") {
  id: ID!
  total: Float!
  status: OrderStatus!
  user: User!
}
// Orders 子图中的 __resolveReference(Federation 特有)
// 当其他子图传来 User 的 @key 值时,此函数负责加载完整数据
const resolvers = {
  User: {
    // reference 包含 @key 字段值({ __typename: 'User', id: 'u1' })
    __resolveReference: async (reference: { id: string }, { db }) => {
      return db.users.findUnique({ where: { id: reference.id } });
    },
    // 此子图扩展 User 的 orders 字段
    orders: async (parent: { id: string }, __, { db }) => {
      return db.orders.findMany({ where: { userId: parent.id } });
    },
  },
};

查询跨子图数据

# 客户端向 Apollo Router 发送此查询
# Router 自动:
# 1. 将 user(id)/name/email 路由到 Users 子图
# 2. 将 orders 路由到 Orders 子图(带 user.id)
# 3. 合并两个子图的结果
query UserProfile($id: ID!) {
  user(id: $id) {
    name
    email
    orders {         # 来自 Orders 子图
      id
      total
      status
    }
  }
}
Federation 的适用场景

Federation 适合大型团队(5人以上)分别维护不同业务域的 API,或需要将现有微服务逐步迁移为统一 GraphQL API 的场景。对于小型项目或单团队项目,Federation 的复杂度远超收益——建议直接用单体 Apollo Server 加 DataLoader 解决。GitHub、PayPal、Wayfair 等公司在生产中使用 Federation 管理数十个子图。

GraphQL 性能优化清单

本章小结

本章核心要点