重试模式:构建具韧性的应用程序

  1. 情境与问题
  2. 解决方案
  3. 重试策略
  4. 实现考量
  5. 测试与验证
  6. 何时使用此模式
  7. 与断路器结合
  8. 相关模式
  9. 参考资料

当你的应用程序与远程服务通信时——数据库、API、消息队列——可能会出错。网络中断、服务器忙碌或暂时性超时都可能导致请求失败。重试模式帮助你的应用程序优雅地处理这些暂时性故障,将潜在的失败转化为成功。

情境与问题

分布式系统经常面临暂时性故障:

  • 网络连接中断:组件之间的短暂断线
  • 服务不可用:部署或重启期间的暂时性服务中断
  • 超时:服务在高负载下响应时间过长
  • 节流:服务在过载时拒绝请求

这些故障通常会自我修正。暂时过载的数据库可能现在拒绝你的连接,但在清除积压工作后一秒钟就会接受。如果没有重试机制,你的应用程序会将这些暂时性问题视为永久性故障,不必要地降低用户体验。

解决方案

设计你的应用程序以预期暂时性故障并透明地处理它们。重试模式引入一种机制,自动重试失败的操作,将对业务功能的影响降到最低。

graph LR A["应用程序"] --> B["重试逻辑"] B --> C["远程服务"] C -->|"成功"| D["返回结果"] C -->|"暂时性故障"| E["等待并重试"] E --> C C -->|"超过最大重试次数"| F["处理异常"] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#ffe1e1 style D fill:#d3f9d8

💡 内置重试机制

许多现代客户端库和框架都包含可配置的重试逻辑。在实现自定义重试代码之前,请先查看你的库文档。

!!!

重试策略

根据故障类型和应用程序需求选择重试策略:

1. 取消

何时使用:故障表示永久性问题或即使重试也不会成功的操作。

示例

  • 身份验证失败
  • 无效的请求参数
  • 找不到资源错误

操作:立即取消操作并报告异常。

2. 立即重试

何时使用:故障不寻常或罕见,例如网络数据包损坏。

示例

  • 随机网络传输错误
  • 暂时性连接重置

操作:立即重试请求,不延迟。

3. 延迟后重试

何时使用:故障常见且与连接或服务负载相关。

示例

  • 连接超时
  • 服务忙碌响应
  • 节流错误

操作:等待后再重试,使用以下延迟策略之一:

固定延迟:每次重试之间等待相同的时间。

尝试 1 → 等待 2 秒 → 尝试 2 → 等待 2 秒 → 尝试 3

递增延迟:线性增加等待时间。

尝试 1 → 等待 2 秒 → 尝试 2 → 等待 4 秒 → 尝试 3 → 等待 6 秒 → 尝试 4

指数退避:每次失败后将等待时间加倍。

尝试 1 → 等待 1 秒 → 尝试 2 → 等待 2 秒 → 尝试 3 → 等待 4 秒 → 尝试 4 → 等待 8 秒 → 尝试 5

带抖动的指数退避:在指数延迟中加入随机性,以防止多个客户端同时重试("惊群"问题)。

实现考量

日志策略

适当记录故障以避免警报疲劳:

  • 早期故障:记录为信息条目
  • 成功重试:记录在调试级别
  • 最终故障:仅在所有重试都用尽后记录为错误

这种方法为操作人员提供可见性,而不会用自我修正问题的警报淹没他们。

graph TB A["请求失败"] --> B["日志:INFO - 尝试 1 失败"] B --> C["等待并重试"] C --> D["请求再次失败"] D --> E["日志:INFO - 尝试 2 失败"] E --> F["等待并重试"] F --> G{"成功?"} G -->|"是"| H["日志:DEBUG - 尝试 3 成功"] G -->|"否(最大重试次数)"| I["日志:ERROR - 所有重试已用尽"] style A fill:#ffe1e1 style H fill:#d3f9d8 style I fill:#ff6b6b

性能影响

调整重试策略以符合业务需求:

交互式应用程序(Web 应用程序、移动应用程序):

  • 快速失败,较少重试次数
  • 尝试之间使用短延迟
  • 显示用户友好消息(“请稍后再试”)

批处理应用程序(数据处理、ETL 作业):

  • 使用更多重试尝试
  • 采用指数退避与较长延迟
  • 优先考虑完成而非速度

⚠️ 避免激进重试

激进的重试策略(许多重试且延迟最小)可能会使情况恶化:

  • 进一步降低已经过载的服务
  • 降低应用程序的响应性
  • 在系统中造成级联故障

考虑在重试之外实现断路器模式,以防止压垮失败的服务。

!!!

幂等性

在应用重试之前,确保操作是幂等的(多次执行是安全的)。非幂等操作可能导致意外的副作用:

问题情境

  1. 服务接收请求并成功处理
  2. 服务因网络问题无法发送响应
  3. 客户端重试,导致重复处理

解决方案

  • 设计操作为自然幂等
  • 使用唯一请求标识符来检测重复
  • 实现服务器端去重逻辑

异常类型

不同的异常需要不同的重试策略:

异常类型 重试策略 示例
暂时性网络错误 延迟后重试 连接超时、DNS 解析失败
服务忙碌/节流 指数退避重试 HTTP 429、HTTP 503
身份验证失败 立即取消 无效凭据、过期令牌
无效请求 立即取消 HTTP 400、格式错误的数据
找不到资源 立即取消 HTTP 404

事务一致性

在事务中重试操作时:

  • 微调重试策略以最大化成功概率
  • 最小化回滚事务步骤的需求
  • 考虑分布式场景的补偿事务
  • 确保重试逻辑不违反事务隔离级别

测试与验证

🧪 测试检查清单

  • 针对各种故障条件进行测试(超时、连接错误、服务不可用)
  • 验证正常和故障场景下的性能影响
  • 确认下游服务没有过度负载
  • 检查并发重试的竞争条件
  • 验证不同故障阶段的日志输出
  • 测试事务回滚场景

!!!

嵌套重试策略

避免分层多个重试策略:

问题:任务 A(有重试策略)调用任务 B(也有重试策略)。这会造成指数级的重试尝试和不可预测的延迟。

解决方案:配置低级别任务快速失败并报告故障。让高级别任务根据自己的策略处理重试。

graph TB A["任务 A
(重试策略)"] --> B["任务 B
(无重试)"] B -->|"快速失败"| A A -->|"根据策略重试"| B style A fill:#e1f5ff style B fill:#fff4e1

何时使用此模式

使用重试模式当

  • 你的应用程序与远程服务或资源交互
  • 故障预期是暂时性且短暂的
  • 重复失败的请求有很大机会成功
  • 操作是幂等的或可以变成幂等的

不要使用重试模式当

  • 故障可能是长期的(改用断路器)
  • 处理非暂时性故障(业务逻辑错误、验证失败)
  • 解决可扩展性问题(改为扩展服务)
  • 操作有重大副作用且不是幂等的

与断路器结合

重试和断路器模式相辅相成:

  • 重试:通过再次尝试操作来处理暂时性故障
  • 断路器:当已知服务停机时防止重试
stateDiagram-v2 [*] --> Closed: 正常运作 Closed --> Open: 超过故障阈值 Open --> HalfOpen: 超时已过 HalfOpen --> Closed: 成功 HalfOpen --> Open: 失败 note right of Closed 请求通过 失败时重试 end note note right of Open 请求立即失败 不尝试重试 end note note right of HalfOpen 允许有限请求 测试服务恢复 end note

这些模式一起提供全面的故障处理:

  1. 重试处理暂时性故障
  2. 断路器防止压垮失败的服务
  3. 即使在长时间中断期间,系统仍保持响应

相关模式

断路器:防止应用程序重复尝试执行可能失败的操作,使其能够继续运作而无需等待故障修复。

节流:控制应用程序实例、服务或租户的资源消耗。

速率限制:管理发送到服务的请求速率,以避免压垮它。

参考资料

分享到