qinghuan
← 回到知识探索

后端现场 / 可下载小项目

大家都等一秒再试,为什么服务还是被挤满?

把重试时间画出来,区分等得更久、错开时间和真正减少尝试次数。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 把间隔拉长,还要检查是否仍然挤在一起
  4. 给每次等待留一点各自选择的空间
  5. 先看时间表,不冒充压力测试
  6. 上面重试一次,下面可能又忙一轮
  7. 恢复需要空间,重试也需要出口
  8. 记住这三点
  9. 别把例子用过头
  10. 自己试一小步
  11. 查原始资料
  12. 找一份资料继续练
开始前,会这些就够了
  • 认识请求与响应;能运行一个 Python 文件
遇到不熟悉的词,先看这 2 个解释
重试
遇到可能只是暂时性的失败后,按预定规则再次尝试同一项操作;规则通常限制次数、等待间隔和总耗时,也要考虑重复执行会不会多做一次业务。
退避与抖动
退避是失败后先等一段时间再试,通常还会逐步增加等待上限。抖动让每次等待带一点随机差异,减少所有客户端按同一张时间表一起回来的机会。
打开完整术语小抄 ↗

假设八个客户端一起遇到临时故障,都约好一百毫秒后重试。它们确实没有立即再发,但一百毫秒后又一起回来。再把间隔翻倍,如果大家继续使用同一张时间表,就可能只是把拥挤搬到了更晚的时刻。

上一篇重试文章关注是否会重复下单。这里假设业务已允许重试,继续问另一个问题:这些重试会在什么时候到达,会不会让正在恢复的服务更难恢复?

想进一步把等待放进整次请求的时间账,可以下载下面的离线练习包。只改总预算,再观察哪一次尝试被挡住;所有时刻都是程序计算的虚拟时间。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本;制作时实测 Python 3.14.0,无第三方依赖。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • demo.py:两种等待策略与总预算的虚拟实验
  • verify.py:19 组验证,含 800 种参数组合
  • README.md:中文步骤、观察任务与模型边界
  • demo.expected.txt:默认完整输出对照
  • SHA256SUMS.txt:前四个文件的内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B demo.py
python -B demo.py --budget 800
python -B verify.py

解压后在含 demo.py 的文件夹打开终端。先看默认时间表,再只改总预算;记录本轮为什么继续或停止。次数包括首试,等待和预定调用耗时都计入同一预算。程序只做虚拟时间加法,不联网、不睡眠,也不代表真实服务压测。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

每层都“再试一下”,最里面会收到多少次?

人为设定所有尝试都失败,上一层每重试一次,下一层就重新用满自己的次数。这里只算这种最坏构造。

“3 次尝试”包括第一次,因此最多再重试 2 次。

  1. 第 1 层3 次1 × 3
  2. 第 2 层9 次3 × 3
  3. 第 3 层27 次9 × 3

最内层最多收到 27 次尝试,来自同一个最外层请求。

真实请求可能提前成功、碰到不应重试的错误,或先耗尽时间预算,实际次数会不同。减少重试层数、限制总预算与打散重试时间是不同措施;这个计数实验没有模拟并发、抖动或服务器吞吐量。

把间隔拉长,还要检查是否仍然挤在一起

退避就是失败后先等一段时间;指数退避常让等待上限逐步变大,再设一个封顶值。本例第一次重试的等待上限是一百,第二次二百,第三次四百。单位是虚拟毫秒,不是推荐线上配置。

如果八个客户端每次都等待相同上限,并暂时忽略执行耗时,它们分别在第 100、300、700 毫秒再次出发。这里的数字是累计时刻:第二次等待二百,不代表从最开始算到二百就发送。

给每次等待留一点各自选择的空间

抖动就是让等待时间带有随机变化。本例采用 full jitter:每个客户端每次在零到本轮上限之间,独立选择一个整数等待值。上限仍增长,但它们不再被一张固定时间表强制绑在一起。

随机不保证每次都错开,也可能抽到零或相同值。这个程序固定种子,只为让学习者复现输出;真实客户端若全复用相同种子和相同抽取顺序,反而可能同步。本文算法也不等同于 gRPC 文档中的正负百分之二十抖动,不应把不同策略当成同一公式。

先看时间表,不冒充压力测试

程序安排八个客户端,每个最多四次尝试:第一次加三次重试。它只输出假设所有尝试均失败时的重试时刻,没有发送请求、处理队列或测量吞吐量。对照两张时间表,可以讨论时间分散,却不能宣布“服务器性能提高了多少”。

同一列代表同一个客户端,下一行是在它上次尝试时刻上再加等待。代码用一个固定种子的随机数生成器,依次模拟八个客户端的抽取;这不表示真实客户端应共用种子。程序把每次尝试耗时设为零,真实调用花掉的时间也必须计入整次请求预算。

小实验 · python
from random import Random

clients, max_attempts = 8, 4  # Initial attempt plus three retries.
for jitter in (False, True):
    rng = Random(7)
    times = [0] * clients
    print("full jitter" if jitter else "fixed backoff")
    for retry in range(1, max_attempts):
        ceiling = min(400, 100 * 2 ** (retry - 1))
        for client in range(clients):
            wait = rng.randrange(ceiling + 1) if jitter else ceiling
            times[client] += wait
        print("retry", retry, times)
print("three layers, three attempts each, all fail:", 3 ** 3)

怎样运行:Python 3.10+ 标准库;保存为 retry_schedule.py,运行 python retry_schedule.py。

运行后对照结果
fixed backoff
retry 1 [100, 100, 100, 100, 100, 100, 100, 100]
retry 2 [300, 300, 300, 300, 300, 300, 300, 300]
retry 3 [700, 700, 700, 700, 700, 700, 700, 700]
full jitter
retry 1 [41, 19, 50, 83, 6, 9, 68, 12]
retry 2 [134, 168, 64, 212, 60, 18, 90, 123]
retry 3 [348, 203, 187, 258, 342, 235, 120, 412]
three layers, three attempts each, all fail: 27

上面重试一次,下面可能又忙一轮

设入口、服务和存储客户端共三层,每层一次调用最多尝试三次。如果每次下层调用都耗尽尝试后失败,最底层最多会被尝试 3×3×3,也就是二十七次。这里算的是最坏的逐层失败构造,不是每次用户请求都会放大二十七倍。

工程上要先查清客户端库、代理与业务代码谁已经在重试,再给整条调用链定总量和停止条件。单层限制看着不大,叠在一起仍可能超过预算。参数错误通常需要修正参数;结果不确定的业务操作还需要能安全识别重复,随机等一等不会解决这些问题。

恢复需要空间,重试也需要出口

限制最多尝试次数、总等待时间和同时进行的尝试数量,分别控制不同成本。等待超过业务期限后仍继续调用,即使这次成功也可能无人接收结果。应在每轮开始前重新判断是否还有必要、是否还有预算。

观测时同时记原始业务请求数和实际尝试数,才看得见重试增加的工作。某次随机安排比较均匀,只是本次安排的事实;是否改善服务恢复,需要带真实容量与失败模式的测试,本例没有做这类测量。

记住这三点

  • 指数退避拉长等待,抖动让各客户端的等待不必完全相同。
  • 最多四次尝试包含初次;最多四次重试则不包含,配置时要分清。
  • 错开时间不能代替幂等、次数限制和总时间预算。

别把例子用过头

  • 随机抽样仍可能碰撞,full jitter 不是所有客户端库共用的算法。
  • 多层尝试数相乘描述指定失败条件下的上界构造,不是实测流量。
  • 固定种子的学习结果不能作为线上随机策略或容量结论。

自己试一小步

如果所有客户端都恢复为同一个随机种子,并在同样的时间失败,抖动还会带来什么变化?

  1. 为每个客户端分别创建随机数生成器,先预测种子相同与不同的两组结果。
  2. 保持尝试次数、上限和初始失败时刻一致,只改变种子安排。
  3. 把每次尝试的耗时也加入虚拟时间,说明哪些输出变化属于模型的新假设。

做到这些,就算完成

  • 区分单轮等待值与从零开始的累计发送时刻。
  • 没有用一次分散结果证明之后必然不碰撞。
  • 记录总尝试数,并明确初次是否计入。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • Exponential Backoff And Jitter (新标签页)↗AWS Architecture Blog · 英文

    Adding Backoff、Adding Jitter:固定退避仍可形成成组调用,加入随机等待使调度分散。

    已阅读正文及 Full Jitter/Equal Jitter 说明;未复制该文章模拟器的性能比例作为本例结果。

  • gRPC:Retry (新标签页)↗gRPC · 英文

    Retry configuration:maxAttempts、指数退避、可重试状态和该实现的抖动范围。

    公开官方指南;本文自行演示 full jitter,并明确它与指南的抖动配置不同。

  • gRFC A6:Client Retries (新标签页)↗gRPC · 英文

    Maximum Number of Retries:maxAttempts 包含最初请求,调用截止线覆盖全部尝试。

    已打开官方设计文档对应段落;这里只引用次数与截止线含义,不推广其他实现上限。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。