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

6.0 KiB
Raw Blame History

tags, create time
tags create time
http
retry
jitter
backoff
distributed-system
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

关联笔记