6.0 KiB
6.0 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-06-01 10:30 |
Jitter 抖动与重试策略
概述
当客户端收到 429 响应后,简单地重试是不够的——如果所有客户端在同一个时刻同时重试,会造成"惊群效应"(thundering herd),让刚恢复的服务再次被打垮。Jitter(随机抖动) 是让分布式系统优雅退避的关键技术。
对应薄弱点 F4:指数退避 + Jitter 的组合使用。
一、为什么需要 Jitter?
问题场景
假设你的 API 限流阈值是 100 QPS,有 100 个客户端共享这个配额:
t = 0s: 所有客户端发现超限 → 等待 5 秒 (Retry-After: 5)
t = 5s: 100 个客户端同时重试 → 瞬间又达到 100+ QPS → 再次被限流
t = 10s: 再次全部超时...
这就是惊群效应——大量客户端在同一时刻涌入,效果相当于人为制造了一次流量脉冲。
解决方案
flowchart TD
subgraph "无 Jitter(糟糕)"
C1["Client 1"] -->|wait 5s| S1["t=5s<br/>⚡ 100 clients fire at once"]
C2["Client 2"] -->|wait 5s| S1
C3["Client N"] -->|wait 5s| S1
S1 --> FAIL["🔴 Server overwhelmed again"]
end
subgraph "有 Jitter(正常)"
D1["Client 1"] -->|"wait 5s, jitter +0.3s"| D4["t=5.3s<br/>😊 gradual arrival"]
D2["Client 2"] -->|"wait 5s, jitter -0.7s"| D5["t=4.3s"]
D3["Client N"] -->|"wait 5s, jitter +1.2s"| D6["t=6.2s"]
D4 --> OK["🟢 Server handles gracefully"]
D5 --> OK
D6 --> OK
end
style FAIL fill:#ffebee
style OK fill:#e8f5e9
二、指数退避 + Jitter 公式
2.1 基础公式
// 第 attempt 次重试的等待时间(简单加减 Jitter 实现)
// wait = base × 2^attempt ± random(0, base),上限不超过 Retry-After
wait := min(base * math.Pow(2, float64(attempt)) + (-jitter ~ +jitter), retryAfter)
分解说明:
| 部分 | 作用 | 示例值 |
|---|---|---|
base |
基础等待时间 | 1 秒 |
2^attempt |
指数增长因子 | attempt=0→1x, 1→2x, 2→4x, 3→8x |
randomJitter |
随机抖动,防止同步 | ±[0, base] 秒 |
retryAfter |
服务端上限,不违反约定 | Retry-After 头部的值 |
2.2 具体数字示例
假设 base = 1s,Retry-After = 10s:
| 重试次数 | 指数退避 | +Jitter(±1s) | 实际等待 | capped to Retry-After |
|---|---|---|---|---|
| #1 | 1 × 2⁰ = 1s | 1s ± 1s | [0, 2s] | min([0,2], 10) = [0, 2] |
| #2 | 1 × 2¹ = 2s | 2s ± 1s | [1, 3s] | min([1,3], 10) = [1, 3] |
| #3 | 1 × 2² = 4s | 4s ± 1s | [3, 5s] | min([3,5], 10) = [3, 5] |
| #4 | 1 × 2³ = 8s | 8s ± 1s | [7, 9s] | min([7,9], 10) = [7, 9] |
| #5 | 1 × 2⁴ = 16s | — | — | min(16, 10) = 10s ← 不再增长 |
[!tip]- 为什么要取 min(..., Retry-After)?
Retry-After是服务器给出的硬性上限,客户端不应超过它。但如果指数退避还没到 Retry-After 的时间,也应该遵循服务器的建议等待。
三、Go 实现
import (
"math/rand"
"time"
)
// calculateRetryDelay 计算带 Jitter 的指数退避等待时间
func calculateRetryDelay(attempt int, retryAfterSec int64) time.Duration {
// 1. 指数退避
base := time.Second
wait := base * time.Duration(math.Pow(2, float64(attempt)))
// 2. 添加 Jitter:±[0, base] 的随机偏移
jitter := time.Duration(rand.Int63n(int64(base)))
if rand.Intn(2) == 0 {
jitter = -jitter // 50% 概率减,50% 加
}
wait = wait + jitter
// 3. 不超过 Retry-After
if retryAfterSec > 0 {
maxWait := time.Duration(retryAfterSec) * time.Second
if wait > maxWait {
wait = maxWait
}
}
return wait
}
// 使用示例
func callWithRetry(ctx context.Context, maxRetries int, callFunc func() error) error {
for attempt := 0; attempt <= maxRetries; attempt++ {
err := callFunc()
if err == nil {
return nil
}
// 检查是否是 429
if isRateLimited(err) {
retryAfter := getRetryAfterHeader() // 从响应头读取
wait := calculateRetryDelay(attempt, retryAfter)
select {
case <-time.After(wait):
continue // 继续重试
case <-ctx.Done():
return ctx.Err()
}
}
// 非限流错误,直接返回
return err
}
return fmt.Errorf("max retries (%d) exceeded", maxRetries)
}
四、常见 Jitter 算法对比
| 算法 | 公式 | 优点 | 缺点 |
|---|---|---|---|
| Full Jitter(AWS 推荐) | random(0, min(cap, base × 2^attempt)) |
分布均匀,去抖效果好 | 可能等待很短导致过早重试 |
| Equal Jitter | min(cap, base × 2^attempt / 2 + random(0, base × 2^attempt / 2)) |
介于两者之间 | 略复杂 |
| Decorrelated Jitter(AWS 默认) | min(cap, random(base, prev_wait × 3)) |
快速分散,适应性强 | 参数调优门槛高 |
| 简单加减 Jitter | base × 2^attempt ± random(0, base) |
实现最简单 | 抖动范围小 |
[!tip]- 推荐方案
对于限流场景,简单加减 Jitter(上面 Go 代码的实现)已经足够。如果你的系统规模极大(数千客户端),建议用 Full Jitter(AWS 推荐)。
五、完整流程总结
flowchart TD
Req["请求发出"] --> Resp{"收到响应?"}
Resp -- "200" --> Success["✅ 成功"]
Resp -- "429" --> Read["读取 Retry-After 头部"]
Read --> Calc["计算 wait = 指数退避 + Jitter"]
Calc --> Wait["⏳ 等待"]
Wait --> Retry{"还有重试次数?"}
Retry -- "是" --> Req
Retry -- "否" --> Fail["❌ 最终失败,上报告警"]
style Success fill:#c8e6c9
style Fail fill:#ffebee