一、问题的起点在分布式系统中网络抖动、服务限流、数据库超时无处不在。面对失败最直觉的做法是立刻重试。但这恰恰是最危险的做法。设想一台后端服务因为短暂过载而返回503此时同时连接它的 1000 个客户端立刻全部重试——这一波整齐划一的报复性请求会再次压垮服务周而复始形成重试风暴Retry Storm让本可自愈的系统永远无法恢复。随机退避Random Backoff正是解决这一问题的标准武器。二、核心思路2.1 固定等待治标不治本importtime,requestsdefretry_fixed(url,retries5,wait2.0):forattemptinrange(retries):try:resprequests.get(url,timeout5)resp.raise_for_status()returnrespexceptExceptionase:print(f第{attempt1}次失败{e})ifattemptretries-1:time.sleep(wait)# 所有客户端同步等待 2 秒再同步冲击raiseRuntimeError(超过最大重试次数)固定等待解决了立即重试的问题但每个客户端等待时长完全相同重试依然是同步的——洪峰被推迟了但并没有打散。2.2 指数退避让等待时间增长defretry_exponential(url,retries6,base0.5,cap30.0):forattemptinrange(retries):try:resprequests.get(url,timeout5)resp.raise_for_status()returnrespexceptExceptionase:waitmin(cap,base*(2**attempt))print(f第{attempt1}次失败等待{wait:.2f}s)ifattemptretries-1:time.sleep(wait)raiseRuntimeError(超过最大重试次数)等待时间0.5 → 1 → 2 → 4 → 8 → 16秒指数递增给下游更多喘息时间。但问题仍在所有客户端的等待序列完全一样重试依然扎堆。2.3 随机抖动Jitter打散洪峰最重要的一步在等待时间中引入随机性importrandomdefretry_with_jitter(url,retries6,base0.5,cap30.0,jitter1.0): 指数退避 全随机抖动Full Jitter wait random(0, min(cap, base * 2^attempt)) forattemptinrange(retries):try:resprequests.get(url,timeout5)resp.raise_for_status()returnrespexceptExceptionase:ceilingmin(cap,base*(2**attempt))# Full Jitter等待时间在 [0, ceiling] 内均匀随机waitrandom.uniform(0,ceiling)print(f第{attempt1}次失败等待{wait:.2f}s上限{ceiling:.1f}s)ifattemptretries-1:time.sleep(wait)raiseRuntimeError(超过最大重试次数)1000 个客户端同时失败后每个人的等待时间各不相同重试请求被均匀散布在整个时间窗口内后端看到的是平稳的流量而非脉冲。三、四种 Jitter 策略对比AWS 在 2015 年发表了一篇经典博文系统比较了四种策略这里用 Python 还原它们importrandom,mathdefbackoff_naive(attempt,base0.5,cap30.0):无抖动纯指数returnmin(cap,base*(2**attempt))defbackoff_full_jitter(attempt,base0.5,cap30.0):全随机wait ∈ [0, min(cap, base·2ⁿ)]returnrandom.uniform(0,min(cap,base*(2**attempt)))defbackoff_equal_jitter(attempt,base0.5,cap30.0):等量抖动保留一半随机一半vmin(cap,base*(2**attempt))/2returnvrandom.uniform(0,v)defbackoff_decorrelated(prev_wait,base0.5,cap30.0):去相关等待时间由上一次决定彻底打破相关性returnmin(cap,random.uniform(base,prev_wait*3))# 演示prev0.5foriinrange(6):fjbackoff_full_jitter(i)ejbackoff_equal_jitter(i)dcbackoff_decorrelated(prev)prevdcprint(f第{i1}次 | Full{fj:.2f}s | Equal{ej:.2f}s | Decorelated{dc:.2f}s)策略特点适用场景Naive纯指数等待时间可预期无随机性不推荐用于多客户端Full Jitter分布最均匀总体负载最低高并发场景首选Equal Jitter保证最低等待不会太激进对最大等待有下界要求时Decorrelated最彻底的去相关等待可能更长极端高并发、防惊群四、生产级封装实际项目中推荐将重试逻辑抽象为装饰器与业务代码解耦importtime,random,functoolsfromtypingimportTuple,Typedefwith_backoff(retries:int5,base:float0.5,cap:float30.0,exceptions:Tuple[Type[Exception],...](Exception,),): 指数退避 Full Jitter 重试装饰器 用法 with_backoff(retries4, base1.0, exceptions(IOError, TimeoutError)) def call_api(): ... defdecorator(func):functools.wraps(func)defwrapper(*args,**kwargs):last_excNoneforattemptinrange(retries):try:returnfunc(*args,**kwargs)exceptexceptionsasexc:last_excexcifattemptretries-1:breakceilingmin(cap,base*(2**attempt))waitrandom.uniform(0,ceiling)print(f[Retry{attempt1}/{retries}]{type(exc).__name__}:{exc}f→ 等待{wait:.2f}s)time.sleep(wait)raiselast_excreturnwrapperreturndecorator# 使用示例importrequestswith_backoff(retries5,base0.5,cap20.0,exceptions(requests.RequestException,))deffetch_data(url:str)-dict:resprequests.get(url,timeout5)resp.raise_for_status()returnresp.json()# 直接调用重试由装饰器透明处理datafetch_data(https://api.example.com/data)五、使用tenacity库开箱即用手写重试逻辑适合理解原理生产中更推荐使用经过充分测试的库tenacityfromtenacityimport(retry,stop_after_attempt,wait_exponential_jitter,retry_if_exception_type,before_sleep_log,)importlogging,requests loggerlogging.getLogger(__name__)retry(stopstop_after_attempt(5),waitwait_exponential_jitter(initial0.5,max30,jitter1),retryretry_if_exception_type(requests.RequestException),before_sleepbefore_sleep_log(logger,logging.WARNING),reraiseTrue,)deffetch_with_tenacity(url:str)-dict:resprequests.get(url,timeout5)resp.raise_for_status()returnresp.json()wait_exponential_jitter内部实现正是min(max, initial × 2ⁿ) random(0, jitter)与我们手写的逻辑完全一致同时还提供了日志钩子、事件回调、上下文感知等企业级特性。六、工程细节避免常见陷阱1. 必须设置重试上限cap不设上限时第 10 次重试的等待时间可能长达0.5 × 2¹⁰ 512 秒完全不可接受。cap30是常见的合理值。2. 区分可重试与不可重试的错误# HTTP 429 Too Many Requests、503 才应重试# HTTP 400 Bad Request、401 Unauthorized 永远不要重试——参数错了重试多少次都没用RETRYABLE_STATUS{429,500,502,503,504}defshould_retry(exc:requests.HTTPError)-bool:returnexc.response.status_codeinRETRYABLE_STATUS3. 在协程asyncio中使用importasyncio,randomimportaiohttpasyncdeffetch_async(session,url,retries5,base0.5,cap20.0):forattemptinrange(retries):try:asyncwithsession.get(url,timeoutaiohttp.ClientTimeout(total5))asresp:resp.raise_for_status()returnawaitresp.json()exceptaiohttp.ClientErrorase:ifattemptretries-1:raisewaitrandom.uniform(0,min(cap,base*(2**attempt)))awaitasyncio.sleep(wait)# 非阻塞等待不会卡死事件循环4. 结合断路器Circuit Breaker退避解决偶发失败断路器解决持续失败。两者配合使用当连续失败次数超过阈值时断路器断开后续请求直接失败fast-fail不再等待重试保护调用方资源。七、小结层次机制解决的问题基础立即重试偶发瞬时错误进阶固定间隔避免立即冲击标准指数退避给下游充足恢复时间最佳实践指数退避 Jitter打散洪峰防止惊群生产级断路器 退避持续故障的快速熔断随机退避看似是一个简单的加个random背后却体现了分布式系统中一个深刻的原则客户端之间的协调往往不需要显式通信只需要引入随机性。一点点随机就能让整个系统从同步振荡走向平稳自愈。