2026-06-01 00:35:50 +08:00
|
|
|
|
---
|
|
|
|
|
|
tags: [http, retry, jitter, backoff, distributed-system]
|
|
|
|
|
|
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: 再次全部超时...
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
这就是**惊群效应**——大量客户端在同一时刻涌入,效果相当于人为制造了一次流量脉冲。
|
|
|
|
|
|
|
|
|
|
|
|
### 解决方案
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
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(正常)"
|
2026-06-01 10:06:16 +08:00
|
|
|
|
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"]
|
2026-06-01 00:35:50 +08:00
|
|
|
|
D4 --> OK["🟢 Server handles gracefully"]
|
|
|
|
|
|
D5 --> OK
|
|
|
|
|
|
D6 --> OK
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
style FAIL fill:#ffebee
|
|
|
|
|
|
style OK fill:#e8f5e9
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 二、指数退避 + Jitter 公式
|
|
|
|
|
|
|
|
|
|
|
|
### 2.1 基础公式
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 第 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 实现
|
|
|
|
|
|
|
|
|
|
|
|
```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 推荐)。
|
|
|
|
|
|
|
|
|
|
|
|
## 五、完整流程总结
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
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
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 关联笔记
|
|
|
|
|
|
|
|
|
|
|
|
- [[分布式限流]] — 限流架构中的客户端重试策略
|
|
|
|
|
|
- [[多级限流架构]] — 各层超时配置与降级策略设计
|