持久化执行前置课(八):重试预算与退避,让恢复不会制造第二场故障
上一篇把超时拆成等待、查回执、安全重试和人工确认。即使某活动允许重试,也不能无限重试:依赖服务故障时,成千上万工作流同步进攻会让短暂抖动变成持续雪崩。
一、痛点:重试会放大原始流量
三层服务各重试三次,最坏会把一次用户请求放大二十七倍。固定间隔会让同时失败的 worker 再次同时唤醒,形成惊群。永久校验错误若被当作暂时故障,则既浪费资源又推迟真正报警。预算必须同时限制单活动次数、单工作流耗时和全局重试容量。
二、原理:分类、退避、抖动与截止时间
退避计划也应可重放,因此抖动值从活动 ID 和尝试次数稳定派生,而非读取随机源。程序计算指数退避并受上限约束;同一输入结果相同,不同活动自然错峰。
importhashlibdefretry_delay(activity_id:str,attempt:int,base:int=2,cap:int=60)->int:ifattempt<1:raiseValueError("attempt_must_be_positive")ceiling=min(cap,base*(2**(attempt-1)))raw=hashlib.sha256(f"{activity_id}:{attempt}".encode()).digest()fraction=int.from_bytes(raw[:4],"big")/(2**32-1)returnmax(1,int(ceiling*(0.5+fraction*0.5)))foractivityin("flight-1","flight-2"):delays=[retry_delay(activity,attempt)forattemptinrange(1,7)]print(activity,delays)assertretry_delay("flight-1",4)==retry_delay("flight-1",4)输出:
flight-1 [1, 2, 4, 11, 16, 35] flight-2 [1, 2, 4, 15, 19, 54]三、实现:预算状态机给出停止理由
预算判断必须在调度新尝试之前持久化。错误先分永久、暂时和不确定;永久错误立即停止,不确定错误转人工,只有暂时错误消费预算。截止时间来自已记录的逻辑时间,重放时不读取当前时钟。
fromdataclassesimportdataclass@dataclass(frozen=True)classBudget:attempts:intmax_attempts:intelapsed:intdeadline:intdefnext_action(budget:Budget,error_class:str)->str:iferror_class=="permanent":return"fail_permanent"iferror_class=="unknown":return"manual_review"iferror_class!="transient":raiseValueError("unknown_error_class")ifbudget.elapsed>=budget.deadline:return"deadline_exceeded"ifbudget.attempts>=budget.max_attempts:return"attempts_exhausted"return"schedule_retry"tests=[(Budget(1,3,5,30),"transient"),(Budget(3,3,5,30),"transient"),(Budget(1,3,31,30),"transient"),(Budget(1,3,5,30),"permanent"),(Budget(1,3,5,30),"unknown"),]forbudget,categoryintests:print(category,next_action(budget,category))输出:
transient schedule_retry transient attempts_exhausted transient deadline_exceeded permanent fail_permanent unknown manual_review四、踩坑:except Exception不是错误分类
HTTP 429、连接重置和参数错误不能套同一策略;即便同为 5xx,支付创建与只读查询的安全性也不同。服务端Retry-After应作为事件记录并纳入计划。另一个坑是每次重启把尝试计数归零,预算必须属于持久化活动,而不是 worker 内存。
抖动不能破坏可解释性。若使用真正随机数,应把选出的延迟写入事件;使用稳定抖动则可复算。全局限流拒绝重试时,不要无成本地立刻重新排队,否则限流器本身成为忙循环。应记录延后原因并设置下一可用时间。
五、验证:证明系统会停
除成功恢复外,必须断言永久错误零重试、次数耗尽有稳定停止码、截止时间先于下一计划时不再调度。以大量活动模拟同刻失败,检查唤醒分布不集中;再验证总重试令牌耗尽时新请求仍保留公平份额。
预算让执行器能够解释为什么继续或停止。下一篇将处理另一种恢复风险:代码已经升级,旧历史却仍要重放;兼容性必须成为显式版本契约。
参考来源
- AWS:指数退避与抖动
- Google SRE:级联故障
- RFC 9110:Retry-After
👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于《持久化执行前置课》系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。
