2026.08.03 | youres | 81次围观
批量任务跑久了,总会遇到那么几次网络抖动、接口限流、临时不可用。如果一失败就放弃,要么重复造数据,要么任务白跑。其实一个成熟的自动化脚本,都离不开一套指数退避重试策略——失败后等一等再试,越挫越长的间隔,给后端喘气的时间,也给自己留条活路。
什么是指数退避?
指数退避(Exponential Backoff)的核心逻辑很简单:每次失败后,等待时间按指数增长。第一次等 1 秒,第二次等 2 秒,第三次等 4 秒,以此类推。配合最大重试次数和最大等待时间上限,防止无限制等下去。
为什么不用固定间隔?固定间隔的问题是:如果大量客户端同时遇到故障,大家都在同一个时间点重试,会把服务器瞬间打爆,形成所谓的雷暴效应(Thundering Herd)。指数增长天然把重试请求分散开,服务器压力小得多。
三个核心参数
写指数退避之前,先定好三个参数,否则写出来就是无头苍蝇:
- 初始间隔(base):第一次重试前等多久,通常 1~2 秒。太小容易打爆服务器,太大反应慢。
- 指数因子(factor):每次失败后间隔乘以多少,常见 2(翻倍)或 1.5(温和增长)。
- 最大等待时间(max):间隔上限,防止等太久影响整体任务进度,通常 30~60 秒。
- 最大重试次数(max_retries):放弃前的重试次数,通常 3~5 次。
- 随机抖动(jitter):在计算间隔上加一点随机偏移,把同一个时刻发起请求的多个客户端打散,这是防止雷暴效应的关键。
Shell脚本实现:基础版
先写一个最朴素的版本,理解核心逻辑:
#!/bin/bash
base=1
factor=2
max_delay=30
max_retries=5
retry_with_backoff() {
local n=0
while true; do
if your_command; then
echo "Success"
return 0
fi
if [ $n -ge $max_retries ]; then
echo "Max retries reached, giving up"
return 1
fi
local delay=$(( base * (factor ** n) ))
[ $delay -gt $max_delay ] && delay=$max_delay
echo "Attempt $((n+1)) failed, retrying in ${delay}s..."
sleep $delay
((n++))
done
}
这段代码能跑,但有两个问题:bash 不支持 ** 幂运算,而且没有 jitter。
Shell脚本实现:生产级版本
实际生产环境用这个,带完整日志和抖动:
#!/bin/bash
# 指数退避重试函数
# 用法: retry_with_backoff "你的命令" [最大重试次数] [初始间隔秒] [最大延迟秒]
retry_with_backoff() {
local cmd="$1"
local max_retries=${2:-5}
local base=${3:-1}
local max_delay=${4:-30}
local n=0
local output
local exit_code
while true; do
output=$(eval "$cmd" 2>&1)
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "[$n] Success"
return 0
fi
if [ $n -ge $max_retries ]; then
echo "[$n] Max retries ($max_retries) reached. Giving up."
echo "Last output: $output"
return 1
fi
# 核心公式: base * factor^n,factor=2 即每次翻倍
local raw_delay=$(( base * (2 ** n) ))
[ $raw_delay -gt $max_delay ] && raw_delay=$max_delay
# 加随机抖动,打散同一时刻的重试请求
local jitter=$((RANDOM % raw_delay))
local delay=$(( raw_delay / 2 + jitter ))
echo "[$n] Failed (exit $exit_code), retrying in ${delay}s (raw: ${raw_delay}s, jitter: +${jitter}s)..."
sleep $delay
((n++))
done
}
封装成可复用的脚本模板
上面是函数,实际使用时一般写成完整脚本模板,方便不同场景复用:
#!/bin/bash
set -euo pipefail
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
# ===== 参数配置 =====
MAX_RETRIES=5
BASE_DELAY=1
MAX_DELAY=30
# ====================
retry_with_backoff() {
local n=0
while true; do
if curl -sf http://your-api.example.com/health > /dev/null 2>&1; then
log "Service is up"
return 0
fi
if [ $n -ge $MAX_RETRIES ]; then
log "Max retries reached, aborting"
return 1
fi
local raw_delay=$(( BASE_DELAY * (2 ** n) ))
[ $raw_delay -gt $MAX_DELAY ] && raw_delay=$MAX_DELAY
local jitter=$((RANDOM % raw_delay))
local delay=$(( raw_delay / 2 + jitter ))
log "Attempt $((n+1)) failed, retrying in ${delay}s..."
sleep $delay
((n++))
done
}
retry_with_backoff
log "Task completed"
与timeout配合:防止重试卡死
指数退避管的是"两次重试之间等多久",但单次请求本身也需要有超时保护,否则网络慢的时候重试间隔等到了、命令本身还在卡着,白白浪费时间。把 timeout 命令和退避策略结合起来,是批量任务的标准配置:
#!/bin/bash
MAX_RETRIES=4
BASE=1
MAX_DELAY=30
for i in $(seq 1 $MAX_RETRIES); do
if timeout 10 curl -sf http://api.example.com/data > result.json 2>&1; then
echo "Success"
break
fi
exit_code=$?
if [ $i -eq $MAX_RETRIES ]; then
echo "All retries failed"
exit 1
fi
# timeout 超时的退出码是 124,也算失败,按指数退避处理
delay=$(( BASE * (2 ** (i-1)) ))
[ $delay -gt $MAX_DELAY ] && delay=$MAX_DELAY
jitter=$((RANDOM % delay))
delay=$(( delay / 2 + jitter ))
echo "Retry $i in ${delay}s (exit code: $exit_code)..."
sleep $delay
done
写在最后
指数退避不是什么高深的技术,但用好它能让批量任务从"一遇到问题就挂"变成"遇到问题自己会扛"。记住三个要点:设置上限防止无限等、加随机抖动防止打堆重试、配合timeout防止单次请求卡死。这三板斧抡下去,你的自动化脚本就具备了生产级容错能力。
版权声明
本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论