在 REST API 上实施速率限制相对较为简单,因为我们通常使用 URI 路径表示特定的 API 资源,并通过 HTTP 方法表示对资源的操作,代理层可以根据这些信息轻松执行预先设定的速率限制规则。然而,对于 GraphQL API 来说,情况就复杂得多,接下来我们将探讨如何克服这些挑战。
简单场景
与 REST API 不同,GraphQL 使用专有的查询语言,不再通过路径和 HTTP method 获取和操作资源,而是将数据查询与操作统一为 Query 查询 和 Mutation 突变,其中 Query 用于获取数据,Mutation 用于操作数据,例如创建更新和删除。
1GET /users
2GET /users/1
3POST /users
4PUT /users/1
5DELETE /users/1
6
7query { users { fullName } }
8query { user(id: 1) { fullName } }
9mutation { createUser(user: { lastName: "Jack" }) { id, fullName } }
10mutation { updateUser (id: 1, update: { lastName: "Marry" }) { fullName } }
11mutation { deleteUser (id: 1) }
上述例子对比了两种 API 查询方法的变化。相比于 REST,GraphQL 更像是在调用资源上的函数并传入所需的输入参数,在响应中会包含查询的数据。而除了查询方法上的不同,调用方式也有差别,GraphQL 暴露的 API 通常通过单一端点暴露(例如 /graphql),查询和输入参数则通过 POST body 发送。
举个例子:
1query {
2 users {
3 fullName
4 }
5 photos {
6 url
7 uploadAt
8 }
9}
在上述例子中,我们模拟一个相册的主页场景,向 /graphql 端点发送一个 API 调用以同时查询用户列表和照片列表。思考一下,传统的反向代理以请求维度执行的策略是否仍然适用于 GraphQL API?
答案是不能。传统的反向代理服务器实际上无法深入处理包含查询本身的 GraphQL API 调用,因此无法对 API 调用执行策略,例如速率限制。对于 GraphQL API 来说,“HTTP 请求”的粒度显得过于粗糙。
然而,API 网关 Apache APISIX 内置了对 GraphQL to HTTP 能力的支持。管理员可以通过预先配置一个查询语句,使得客户端可以直接通过 HTTP POST 调用它,无需了解 GraphQL 的细节,只需提供所需的输入参数。这不仅有助于安全性,同时也意味着可以在这里应用在 HTTP API 上的策略。
实际上,这将动态查询的 GraphQL 转化为由 API 提供者的知识驱动的静态查询,这既有利有弊。有时候,我们可能不希望牺牲 GraphQL 动态查询的特性,让我们继续其他场景的讨论。
复杂场景
GraphQL 使用其专有的语言进行数据建模和 API 描述,它的数据模型允许嵌套。扩展上面的例子:
1query {
2 photos(first: 10) {
3 url
4 uploadAt
5 publisher {
6 fullName
7 avatar
8 }
9 comments(first: 10) {
10 content
11 sender {
12 fullName
13 avatar
14 }
15 }
16 }
17 // users...
18}
在这个案例中,我们模拟获取照片列表的前 10 条照片数据,包括每张照片的发布者以及每张照片的前 10 条评论及其发送者。这意味着对于后端服务而言,将涉及查询多个数据库表或调用微服务。在这样的嵌套查询场景中,随着查询到的数据数量和嵌套层数的增加,对后端服务和数据库的计算压力将呈指数级上升。
为了防止复杂的查询拖垮服务,我们可能希望在代理层进行检查并阻止这些复杂查询。要应用这样的策略,代理组件必须能够解析查询语句为结构化数据,并遍历以获取查询中包含的嵌套及每层查询的字段。按照 GraphQL 的常见做法,我们可以为字段分配复杂度值,作为查询成本,并在全局层面限制总体查询的复杂度。对于上述查询,我们可以计算每个单一字段的成本假设为 1:
110 * photo (url + uploadAt + publisher.fullName + publisher.avatar + 10 * comment (content + sender.fullName + sender.avatar))
2
310 * (1 + 1 + 1 + 1 + 10 * (1 + 1 + 1)) = 340
总查询成本为 340,看起来是可以接受的,我们可以按照这样的规则为 API 配置查询成本上限。
但是,如果有一个恶意客户端尝试一次性获取 100 张照片的数据,查询成本将大幅上升至 3400,超过预先设定的限额,请求将被拒绝。
除了限制客户端的单次查询最大成本,我们还可以为其附加时间范围上的限制,比如允许客户每一分钟进行总量为 2000 的查询并拒绝多出的部分,这可以阻挡恶意爬虫。
为了实现这样的能力,代理组件必须解析并计算查询成本。API7 企业版支持这样的能力,它可以实现 GraphQL 查询动态解析并按照配置实现对用户的速率限制。
GraphQL API 在代理层面面临的挑战是,传统的反向代理无法有效感知和处理 GraphQL 查询语句中的复杂性和嵌套关系,而 API 网关的技术可以帮助我们克服这些挑战。