API 网关专栏 · 第 47

API 网关超时与重试:安全策略与 APISIX 配置

2026年09月07日
API 网关超时与重试:安全策略与 APISIX 配置

安全的 API 网关重试策略应从端到端截止时间出发,为连接、发送请求和等待响应分配有界时间,并且只重试那些重复执行不会造成意外二次副作用的操作。尝试次数要少,必须在调用方剩余时间内停止,并将重试与健康检查、容量保护和可观测性结合起来。

重试可以掩盖短暂的节点故障,也可能重复支付、放大事故,或让请求在调用方已经放弃后才完成。因此,“所有 5xx 都重试三次”不是可靠性策略。

核心要点

  • 从调用方端到端截止时间开始,再分配更短的下游时间预算。
  • 显式配置连接、发送与读取超时,它们分别保护不同阶段。
  • 除非应用能够证明重放安全,否则不要自动重试非幂等操作。
  • 同时限制尝试次数和总重试时间;还要考虑客户端、Service Mesh、程序库和上游服务发起的重试。
  • 健康检查用于避免反复选择已知不健康节点,不是无限重试的许可。
  • 上线前测试结果不明确的故障、慢响应、连接拒绝和变更操作。

理解三类上游超时

API 网关通常会分别暴露网关与上游之间各阶段的超时:

超时限制的阶段常见故障表现
连接建立上游连接节点不可达、连接被拒绝、网络路径问题
发送向上游发送请求上游接收路径缓慢或阻塞,尤其是带请求体时
读取上游响应连续两次读取之间的等待时间处理缓慢、响应停滞或流式传输中断

这些超时并不自动等同于一次请求的总截止时间。一次重试可能再次消耗连接和读取时间,而排队、TLS、插件与下游传输还会占用其他时间。应根据实测延迟和调用方预算推导各阶段数值,而不是把所有字段都设置为同一个较大值。

例如,调用方会在三秒后放弃请求时,网关不能安全地先读取三秒,再用三秒重试。还必须为网关处理、网络波动和响应路径预留时间。如果剩余预算不足以完成另一次有价值的尝试,应直接失败而不是重试。

根据语义判断是否允许重试

RFC 9110 第 9.2.2 节允许在通信失败后自动重试幂等请求,同时规定代理不得自动重试非幂等请求。当网关无法判断连接失败前上游是否已经执行操作时,这一点尤其重要。

应使用策略矩阵,而不是简单的方法列表:

操作默认网关策略可以支持重试的条件
只读 GETHEAD一次有界重试可能合理仍有截止时间、存在其他健康节点、故障条件经过测试
幂等 PUTDELETE按资源评估应用语义确实容许重复执行
创建订单、扣款或消息的 POST不自动重试应用原子执行幂等键,或能证明请求未被执行
流式传输或大文件上传通常不透明重试协议和应用明确支持可恢复重放

HTTP 方法本身不是证据。设计不当的 GET 可能带有副作用;正确实现幂等键的 POST 也可以安全重放。服务负责人必须定义不变量,网关策略则必须保持该不变量。

限制多层放大效应

重试会相乘。如果客户端尝试三次,网关为每次客户端请求尝试三次,上游程序库也如此,一次用户操作就会产生 27 次下游调用。

列出每一个会重试的层,并为每个故障边界指定一个负责人。实用的网关策略通常包括:

  • 零次或一次重试,而不是无限次数;
  • 总重试时间短于请求剩余预算;
  • 在实现支持的情况下选择另一个健康节点;
  • 下游响应开始后不再重试;
  • 使用并发和速率控制限制事故期间的放大;
  • 由稍后发起端到端尝试的客户端执行带抖动退避。

网关内立即进行的故障转移与稍后的客户端重试用途不同。前者可能以很小延迟避开一个故障节点;后者应退避,让系统有时间恢复。不要为了让网关重试看起来像客户端退避,就在请求内增加等待。

为 APISIX 配置有界读取重试

Apache APISIX 3.18 在 Upstream 上提供 retriesretry_timeout 以及 timeout.connecttimeout.sendtimeout.read。当前 Admin API 文档说明,retries: 0 会禁用重试,否则 APISIX 使用底层 NGINX 机制。省略 retries 时,默认值取决于可用后端节点,因此当请求语义很重要时,应显式设置。

下面的示例 Upstream 只适用于可以安全重放的读取操作。示例数值必须替换为实际服务的测量结果。

1curl "http://127.0.0.1:9180/apisix/admin/upstreams/orders-read" \
2  -X PUT \
3  -H "X-API-KEY: ${admin_key}" \
4  -d '{
5    "type": "roundrobin",
6    "nodes": {
7      "orders-1.internal:8080": 1,
8      "orders-2.internal:8080": 1
9    },
10    "retries": 1,
11    "retry_timeout": 2,
12    "timeout": {
13      "connect": 0.5,
14      "send": 1,
15      "read": 2
16    },
17    "checks": {
18      "active": {
19        "type": "http",
20        "http_path": "/health",
21        "timeout": 1,
22        "healthy": {
23          "interval": 5,
24          "successes": 2
25        },
26        "unhealthy": {
27          "interval": 2,
28          "http_failures": 3,
29          "tcp_failures": 2,
30          "timeouts": 2
31        }
32      }
33    }
34  }'

只把可安全重放的路由关联到该 Upstream:

1curl "http://127.0.0.1:9180/apisix/admin/routes/orders-read" \
2  -X PUT \
3  -H "X-API-KEY: ${admin_key}" \
4  -d '{
5    "uri": "/orders/*",
6    "methods": ["GET", "HEAD"],
7    "upstream_id": "orders-read"
8  }'

主动健康检查可以帮助 APISIX 在仍有健康替代节点时,停止选择反复无法通过探针的节点。文档说明资源池边界采用 Fail-Open 行为:如果无法选出健康节点,APISIX 会继续访问 Upstream。因此,健康检查无法让所有应用请求都可以安全重放,而且即使健康端点正常,某项操作所需的依赖项也可能已发生故障。

APISIX 还支持根据代理流量进行被动健康检查。其健康检查文档指出,被动检查本身无法把不健康节点重新标记为健康,因为该节点已经不再接收请求;因此通常需要主动检查来检测恢复。

为变更操作禁用自动重试

不要把不安全的变更操作放到继承了未知重试次数的 Upstream 后面。可以使用独立 Upstream,或在路由级设计中明确策略。下面的示例为创建订单禁用重试:

1curl "http://127.0.0.1:9180/apisix/admin/upstreams/orders-write" \
2  -X PUT \
3  -H "X-API-KEY: ${admin_key}" \
4  -d '{
5    "type": "roundrobin",
6    "nodes": {
7      "orders-1.internal:8080": 1,
8      "orders-2.internal:8080": 1
9    },
10    "retries": 0,
11    "timeout": {
12      "connect": 0.5,
13      "send": 1,
14      "read": 2
15    }
16  }'
1curl "http://127.0.0.1:9180/apisix/admin/routes/orders-write" \
2  -X PUT \
3  -H "X-API-KEY: ${admin_key}" \
4  -d '{
5    "uri": "/orders",
6    "methods": ["POST"],
7    "upstream_id": "orders-write"
8  }'

如果应用实现了幂等键,应测试并发重复请求、键到期、同一键下的载荷不匹配,以及结果是否原子持久化。仅转发 Idempotency-Key 请求头并不能保证重放安全。

保守选择重试条件

并非所有失败都适合再次尝试:

  • **在请求执行前连接被拒绝或重置:**另一个健康节点可能成功。
  • **请求发送后的读取超时:**结果可能不明确;只有语义容许重放时才能重试。
  • **HTTP 500:**可能由当前输入确定性触发,在所有节点都会重复。
  • **HTTP 429:**表示准入压力;立即重试通常只会增加负载。
  • **HTTP 503:**可能是暂时故障,也可能表示整个资源池已经饱和。
  • **无效请求或授权失败:**不更改请求就重试没有帮助。

确认正在运行的协议和版本中,APISIX 与 NGINX 的确切重试条件。retries 次数只是行为的一部分;协议、故障阶段、响应状态和底层代理配置也会影响结果。

通过故障注入进行验证

使用至少有两个可区分上游节点的预发布环境。记录处理每次尝试的节点,然后测试:

  1. 拒绝其中一个节点的连接,确认可安全重放的请求最多只进行允许的额外尝试。
  2. 分别延迟连接建立、请求读取和响应生成,确定哪个超时会先到期。
  3. 返回指定的 5xx 响应,确认只重试预期条件。
  4. 执行变更后、返回响应前关闭连接,确认网关不会重放该操作。
  5. 让所有节点都变为不健康,验证 APISIX 文档规定的 Fail-Open 行为、实际节点选择和尝试次数,以及有界总延迟。
  6. 在负载下组合客户端重试与网关重试,确认上游流量仍在容量范围内。

不要为每项实验断言一个固定客户端状态。根据故障阶段和配置,网关可能返回不同的 5xx。应断言真正重要的不变量:最大尝试次数、总延迟、节点选择和不存在重复副作用。

监控重试预算

结合上游与应用遥测,使用 APISIX 的 prometheus 插件跟踪:

  • 请求与上游延迟分布;
  • 网关与上游状态码;
  • 上游健康状态;
  • 客户端、网关和服务的请求量,以发现放大效应;
  • 超时与连接失败计数;
  • 应用侧重复副作用或幂等冲突信号。

针对比率变化告警,而不只是原始错误数。客户端错误小幅增加的同时,上游请求量大幅增加,是典型的重试放大信号。

超时与重试检查清单

  • 调用方的端到端截止时间是多少?
  • 连接、发送、读取、网关工作和响应路径各有多少可用时间?
  • 操作是否已证明可以安全重放,包括结果不明确的情况?
  • 哪些层会重试,最大综合放大倍数是多少?
  • 重试次数和重试时间是否显式受限?
  • 重试能否在不跨越一致性边界的情况下选择另一个健康节点?
  • 速率与并发限制能否防止事故演变成重试风暴?
  • 是否测试了慢速、连接拒绝、部分失败、5xx 和变更操作故障?
  • 指标是否显示尝试和副作用,而不只是最终客户端响应?

常见问题

API 网关应该重试所有 GET 请求吗?

不应该。GET 被定义为幂等,但重试仍消耗时间和容量,而且有些实现会错误地让 GET 产生副作用。只有当操作确实可以安全重放、截止时间足以容纳另一次尝试,并且故障很可能是暂时的,才应重试。

API 网关应该重试多少次?

不存在通用次数。与多次尝试相比,零次或一次是更安全的起点。应根据端到端延迟预算、健康替代节点数量、实测暂时故障率,以及其他层的重试行为确定上限。

健康检查可以代替重试吗?

不能。健康检查减少已知不健康节点被选中的次数,重试处理请求期间发生的部分故障。两者都必须有界,而且都不能让非幂等操作变得可安全重复。

后续步骤

在跨层分配重试职责前,请先了解 API 网关、反向代理与负载均衡器如何组合。如需集中治理 Apache APISIX 部署,可以进一步了解 API7 Enterprise

获取方案