0

bash随机抖动jitter防止重试风暴实战:让批量任务的等待策略不再集体踩踏

2026.08.05 | youres | 65次围观

你有没有遇到过这种情况:一批脚本同时跑,突然某个接口抖动了一下,结果所有脚本同时失败,又同时在第30秒、60秒、120秒发起重试——流量撞在一起,雪球越滚越大。这就是典型的「惊群效应」,也叫重试风暴(Retry Storm)

解决思路其实很简单:给每次重试间隔加一点随机性,让大家的等待时间错开。这就是本文要讲的 jitter(随机抖动)

为什么指数退避还不够

先看一个典型的指数退避脚本:

#!/bin/bash
max_retries=5
delay=2
attempt=0
while ! your_command; do
  attempt=$((attempt + 1))
  if [ $attempt -ge $max_retries ]; then
    echo "Max retries reached"
    exit 1
  fi
  echo "Retry $attempt in ${delay}s..."
  sleep $delay
  delay=$((delay * 2))
done

问题在哪?如果100台服务器同时检测到同一API故障,它们会在同一时刻开始重试。30秒后,100台服务器同时发出第二次请求。60秒后,又同时发出第三次请求。

指数退避解决的是「不要频繁重试」的问题,但没有解决「所有节点同时重试」的问题。

什么是 jitter

jitter 就是在固定间隔(或指数间隔)的基础上,加一个随机偏移量,让各个客户端的重试时间自然错开。常见的三种策略:

  • Full jittersleep = random(0, base_delay) —— 在0到基础延迟之间随机
  • Equal jittersleep = base_delay / 2 + random(0, base_delay / 2) —— 保证至少有一半延迟
  • Decorrelated jittersleep = random(base_delay, prev_sleep * 3) —— 延迟不单调收敛

三种策略从保守到激进,适合不同场景。

5种实战写法

方法一:随机比例抖动(推荐)

在指数退避的基础上,每次延迟乘以一个随机系数(0.5~1.5),简单又有效:

#!/bin/bash
retry_with_jitter() {
  local max_retries="$1"
  local base_delay=2
  local delay=$base_delay
  local attempt=0
  while ! your_command; do
    attempt=$((attempt + 1))
    if [ $attempt -ge $max_retries ]; then
      echo "[$$] Max retries reached"
      return 1
    fi
    # 随机抖动:基础延迟乘以50%~150%的随机比例
    ratio=$((50 + RANDOM % 101))  # 50~150的随机整数
    sleep_time=$((delay * ratio / 100))
    [ $sleep_time -lt 1 ] && sleep_time=1
    echo "[$$] Retry $attempt, waiting ${sleep_time}s (jitter ${ratio}%)"
    sleep $sleep_time
    delay=$((delay * 2))
  done
  return 0
}

方法二:纯整数随机抖动(最简洁)

不想用浮点运算?纯整数也能做,效果足够实用:

#!/bin/bash
retry_with_jitter() {
  local max_retries="$1"
  local base=2
  local attempt=0
  while ! your_command; do
    attempt=$((attempt + 1))
    [ $attempt -ge $max_retries ] && return 1
    # RANDOM是bash内置变量,范围0~32767
    sec=$((base + RANDOM % base))  # base ~ 2*base秒随机
    echo "Retry $attempt, sleep ${sec}s (jitter)"
    sleep $sec
    base=$((base * 2))
  done
  return 0
}

方法三:xargs批量任务中统一注入抖动

在批量处理场景下,可以让每个子任务在执行前随机等待,避免集体冲击目标服务:

#!/bin/bash
export -f do_task
do_task() {
  local target="$1"
  # 每个任务开始前随机等待0~3秒,分散并发压力
  sleep $((RANDOM % 4))
  curl -sf --fail "https://$target/health" || return 1
  echo "OK: $target"
}
cat targets.txt | xargs -P 10 -I{} bash -c 'do_task "{}"'

方法四:装饰性抖动 Decorrelated Jitter 完整封装

#!/bin/bash
retry_jitter() {
  local cmd="$1"
  local max_attempts="${2:-5}"
  local base="${3:-2}"
  local attempt=0
  local prev_delay=$base
  while true; do
    attempt=$((attempt + 1))
    if eval "$cmd"; then
      return 0
    fi
    [ $attempt -ge $max_attempts ] && { echo "Failed after $max_attempts attempts"; return 1; }
    # 装饰性抖动:延迟在[base, prev*3]之间随机,不单调收敛
    max_delay=$((prev_delay * 3))
    [ $max_delay -lt $base ] && max_delay=$((base * 10))
    jitter_delay=$((base + RANDOM % (max_delay - base + 1)))
    prev_delay=$jitter_delay
    echo "Attempt $attempt failed, retry in ${jitter_delay}s (decorrelated jitter)"
    sleep $jitter_delay
  done
}

# 使用示例
retry_jitter "curl -sf http://your-api.com/health" 5 3

方法五:与 trap 配合,中断时保留重试状态

#!/bin/bash
ATTEMPT=0
MAX_RETRIES=5
BASE=2

cleanup() {
  echo "[$$] Interrupted at attempt $ATTEMPT, saving state..."
  echo "ATTEMPT=$ATTEMPT" > /tmp/retry_state_$$
  exit 0
}
trap cleanup INT TERM

[ -f /tmp/retry_state_$$ ] && source /tmp/retry_state_$$

retry_loop() {
  while ! your_command; do
    ATTEMPT=$((ATTEMPT + 1))
    [ $ATTEMPT -ge $MAX_RETRIES ] && return 1
    jitter=$((RANDOM % (BASE * 2) + 1))
    echo "Attempt $ATTEMPT, jittered delay: ${jitter}s"
    sleep $jitter
    BASE=$((BASE * 2))
  done
}

retry_loop && rm -f /tmp/retry_state_$$

三种抖动策略对比

策略 公式 特点 适用场景
随机比例抖动 delay * random(0.5~1.5) 实现简单,效果稳定 日常批量任务重试
Full jitter random(0, delay) 随机范围大,扩散最彻底 高并发API保护
Decorrelated jitter random(base, prev*3) 延迟不单调收敛,抗惊群更强 分布式系统协调

一个真实案例:批量健康检查不再雪崩

某服务器上有20个定时任务同时检测同一个API,以前的写法是:失败后每30秒重试一次。结果某天API抖动30秒,20个任务同时在第30秒发出大量请求,差点把API打挂。

加上 jitter 后,每个任务的等待时间在15~45秒之间随机分散,同一秒内的重试压力从20个请求降到了平均不到2个,API再也没有因为批量重试而崩溃。

小结

jitter 是解决重试风暴的经典手法,核心就是一句话:在每次重试延迟里加一个随机量,让大家的等待时间自然错开。几种写法的选择建议:

  • 追求简洁实用 → 方法二(整数随机抖动)
  • 追求通用可复用 → 方法四(完整函数封装)
  • 批量任务防冲击 → 方法三(xargs 统一注入)

与其等故障发生后再扩容,不如一开始就把重试策略写对。

相关文章

版权声明

本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论