Files
cs-note/hzh/REDIS/Jitter抖动与重试策略.md
T

181 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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(正常)"
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 基础公式
```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
```
## 关联笔记
- [[分布式限流]] — 限流架构中的客户端重试策略
- [[多级限流架构]] — 各层超时配置与降级策略设计