查询深度与复杂度限制
防止恶意深层查询
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;
},
},
};
性能优化策略分层
APQ 工作流程详解
APQ(Automatic Persisted Queries,自动持久化查询)是一个两步协议:
性能测量: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 延迟慢速字段
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 条,提升感知速度
截至 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 的微服务架构方案。
子图 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 适合大型团队(5人以上)分别维护不同业务域的 API,或需要将现有微服务逐步迁移为统一 GraphQL API 的场景。对于小型项目或单团队项目,Federation 的复杂度远超收益——建议直接用单体 Apollo Server 加 DataLoader 解决。GitHub、PayPal、Wayfair 等公司在生产中使用 Federation 管理数十个子图。
- 使用 DataLoader 消除 N+1 问题(最重要,可减少 90%+ 的数据库查询)
- 设置查询深度限制(建议 ≤ 10)和复杂度限制(防 DoS)
- 启用 APQ 减少网络传输并支持 CDN 缓存(GET 方式)
- 使用
@cacheControl为静态内容设置 HTTP 缓存头 - 对热点数据使用 Redis 缓存 Resolver 结果(TTL 300-3600s)
- 数据库层面为外键和排序字段添加合适的索引
- 使用 Apollo Tracing 定位慢 Resolver,针对性优化
- 考虑使用 @defer 延迟慢速字段,让用户更快看到关键内容
本章小结
- N+1 问题:GraphQL 逐字段解析机制导致关联查询产生 1+N 次数据库请求;DataLoader 通过批处理将其降为 2 次查询,是 GraphQL 性能优化的首要手段。
- 查询限制:graphql-depth-limit 限制嵌套深度(防止指数级查询);graphql-query-complexity 按字段复杂度计分(超出阈值拒绝请求)。生产环境必须配置。
- APQ 机制:两步握手协议(哈希 → 哈希+查询 → 仅哈希),减少重复传输查询字符串;GET 请求格式支持 CDN 缓存,配合 @cacheControl 可大幅降低服务器负载。
- @defer/@stream 增量交付:@defer 延迟发送慢速字段(先返回快速字段,慢速字段就绪后追加);@stream 流式返回列表元素(每个元素就绪即发送);通过 HTTP Multipart 实现,大幅提升首屏感知速度。
- Apollo Federation v2:用 @key 声明实体主键,各子图可扩展同一类型;Apollo Router 自动合并多子图查询,客户端无感知;适合大型团队分布式 GraphQL 架构,不适合小型项目。
- 性能优化优先级:DataLoader(应用层)> 数据库索引(存储层)> Resolver 缓存(缓存层)> APQ/CDN(网络层);应先用 Apollo Tracing 定位瓶颈,再有针对性优化。