本文目录导读:

- 核心重试策略 (Backoff Strategy)
- 重试上限
- 失败类型判别 (何时应该重试)
- 幂等性保障 (Idempotency)
- 优雅的代码实现架构
- 日志与监控
- 兜底策略:死信队列 (Dead Letter Queue)
- 一个设计清单
设计一个健壮的脚本失败重试机制,核心在于解决 “重试什么”、“何时重试”、“如何重试” 以及 “什么时候放弃” 这四个问题。
一个成熟的方案通常包含以下设计要素:
核心重试策略 (Backoff Strategy)
这是重试机制的灵魂,决定了失败后的等待时间。
- 固定间隔 (Fixed Interval):每次重试前等待固定时间(如 5 秒)。
- 优点:实现简单。
- 缺点:如果失败是瞬时的(如网络抖动),固定等待太长浪费性能;如果系统正在过载,固定短间隔会加剧雪崩(Thundering Herd Problem)。
- 线性递增 (Linear Backoff):等待时间线性增长(如 1s, 2s, 3s...)。
- 适用:对等待时间不太敏感的场景。
- 指数退避 (Exponential Backoff) - 推荐:每次重试等待时间翻倍(如 1s, 2s, 4s, 8s...)。
- 优点:在资源紧张时迅速降低重试频率,避免对下游系统造成更大压力。
- 指数退避 + 随机抖动 (Exponential Backoff with Jitter) - 最推荐:在指数退避的基础上,引入随机因子(如 0~1 随机值)。
- 公式:
sleep = min(cap, base * 2^retryCount * random.uniform(0.5, 1.5)) - 效果:时间不再是固定倍数,而是在一个区间内随机抖动。这几乎是分布式系统中避免“惊群效应”的标准解法。
- 公式:
重试上限
必须设定明确的最大重试次数 或 总超时时间,防止脚本陷入无限重试。
- 最大次数:例如重试 3 次,适合大多数场景。
- 最大时间:5 分钟内最多重试,适合任务有截止时间的场景。
- 组合使用:
while retry_count < MAX_RETRIES AND total_time_elapsed < MAX_DURATION
失败类型判别 (何时应该重试)
不是所有失败都应该重试。 需要对异常进行分级,区分“可以重试的错误”和“必须立即停止的错误”。
- 可重试错误 (Retryable):
- 网络瞬态错误:连接超时、DNS 解析临时失败、HTTP 503 (Service Unavailable)、500 (Internal Server Error)。
- 资源竞争:数据库死锁、乐观锁更新失败。
- 服务限流:HTTP 429 (Too Many Requests)。
- 不可重试错误 (Non-Retryable):
- 代码逻辑错误:HTTP 400 (Bad Request)、401 (Unauthorized)、403 (Forbidden)、404 (Not Found)。
- 语法错误、参数校验失败。
- 磁盘空间不足(除非等待扩容)、内存溢出。
设计建议:定义一个枚举或接口,让调用方明确告知“这个错误可以重试吗?”。
class RetryableError(Exception):
"""表示此异常可以通过重试解决"""
pass
class FatalError(Exception):
"""表示此异常无法通过重试解决,应立即终止"""
pass
幂等性保障 (Idempotency)
这是重试机制能安全运行的前提。 如果脚本执行的操作不是幂等的(扣款”),重试会导致重复执行和业务错误。
- 设计原则:让操作本身是幂等的。
- 数据库:使用
INSERT ... ON DUPLICATE KEY UPDATE或UPSERT。 - 消息队列:使用去重 ID(如消息 ID 或业务 ID)。
- API 调用:客户端生成一个全局唯一的
idempotency_key,服务端保存结果并返回。
- 数据库:使用
优雅的代码实现架构
建议将重试逻辑与业务逻辑解耦,通常有三种模式:
-
模式 A:装饰器模式 (Decorator)
-
适用:单个函数或方法的简单重试。
-
示例 (Python):
import time import random from functools import wraps def retry(max_attempts=3, base_delay=1, backoff=2, jitter=True): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): last_exception = None for attempt in range(max_attempts): try: return func(*args, **kwargs) except RetryableError as e: last_exception = e if attempt < max_attempts - 1: delay = base_delay * (backoff ** attempt) if jitter: delay *= random.uniform(0.5, 1.5) time.sleep(delay) except FatalError as e: raise e raise last_exception return wrapper return decorator
-
-
模式 B:循环 + 上下文管理器 (Loop + Context Manager)
- 适用:需要更多控制(如记录重试原因、更新进度)的复杂场景。
retry_policy = RetryPolicy(max_retries=3, backoff='exponential', jitter=True)
with retry_policy as retrier: while retrier.should_retry(): try: result = execute_complex_script() break except RetryableError as e: retrier.record_failure(e) wait_time = retrier.get_wait_time() log.warning(f"Attempt {retrier.attempt} failed. Retrying in {wait_time}s...") time.sleep(wait_time)
- 适用:需要更多控制(如记录重试原因、更新进度)的复杂场景。
-
模式 C:消息队列 + 独立重试队列 (SAQ / Dead Letter Queue)
- 适用:长时间运行或分布式的脚本任务,这是最健壮但复杂度最高的方案。
日志与监控
没有监控的重试机制是危险的。
- 日志:每次失败和即将重试时,必须记录:
- 失败的具体原因 (异常堆栈)。
- 这是第几次重试 (attempt)。
- 等待多久 (wait_time)。
- 重试开始时间。
- 告警:
- 状态码:监控重试成功率,如果重试 3 次后依然失败,应该触发告警。
- 指标 (Metrics):记录
retry.attempts(分布)、retry.success(是否最终成功)、retry.abandoned(超过上限放弃)。
兜底策略:死信队列 (Dead Letter Queue)
当重试达到上限后,脚本不应该被简单丢弃。
- 操作:将失败的任务信息(任务数据 + 失败原因 + 重试历史)发送到一个专用的“死信队列”。
- 处理:人工或专门的补偿脚本分析死信队列,进行数据修正或回滚后手动重试。
一个设计清单
- 判断错误类型:只对网络、限流、死锁等瞬态错误重试,业务逻辑错误、参数错误立即报错。
- 选择退避策略:“指数退避 + 随机抖动” 是默认首选。
- 设定重试上限:3-5 次或固定时限。
- 确保幂等:全局 ID + UPSERT 是常见手段。
- 日志埋点:记录每一次失败和重试。
- 设置兜底:重试超限后,写入死信队列或触发人工告警。
- 考虑熔断:如果脚本连续失败达到某个阈值(如 50 次/分钟),立即停止所有重试,进入熔断状态,等到一定时间后再尝试恢复,防止对后端系统造成持续冲击。
一个最简单的实践是:从装饰器 + 指数退避 + 3 次重试开始,随着脚本复杂度和依赖的增加,逐步引入死信队列和熔断机制。