批量跑任务最怕什么?不怕它慢,就怕它超时后直接放弃,特别是关键的数据同步、健康检查、接口调用这类必须成功的场景。bash脚本配合timeout命令,可以让任务超时后自动重试,直到成功或达到重试上限才退出——这篇文章介绍3种常见写法,各有各的适用场景。
先搞清楚timeout命令的行为
timeout是GNU coreutils里的命令,作用是给进程发送指定信号,超时后强制终止。基本用法是:
timeout 30s curl -s https://api.example.com/health
这条命令的意思是:curl这个进程最多跑30秒,超时了就杀掉。timeout命令的退出码规则如下:
- 退出码124:进程超时被强制终止
- 退出码125:timeout本身执行失败(如信号无法发送)
- 退出码137:收到SIGKILL(128+9)
- 其他退出码:进程正常退出,timeout透传原始退出码
重试逻辑的核心就是:检查退出码是否为124或137,如果是就说明超时,触发重试。
方案一:while循环 + timeout(最通用)
这是最直观的写法,适合大多数场景。思路是:用while循环包裹timeout命令,超时就继续重试,不超时就break退出。
#!/bin/bash
MAX_RETRIES=5
RETRY_DELAY=3
TIMEOUT_SEC=30
RETRY_COUNT=0
while true; do
RETRY_COUNT=$(($RETRY_COUNT + 1))
echo "[$(date '+%H:%M:%S')] 第${RETRY_COUNT}次尝试..."
timeout ${TIMEOUT_SEC}s curl -s -f https://api.example.com/health
EXIT_CODE=$?
if [[ $EXIT_CODE -eq 0 ]]; then
echo "✅ 任务执行成功"
break
elif [[ $EXIT_CODE -eq 124 ]]; then
echo "⏰ 第${RETRY_COUNT}次超时"
if [[ $RETRY_COUNT -ge $MAX_RETRIES ]]; then
echo "❌ 达到最大重试次数 ${MAX_RETRIES},放弃"
exit 1
fi
echo " 等待${RETRY_DELAY}秒后重试..."
sleep $RETRY_DELAY
else
echo "❌ 任务执行失败,退出码: ${EXIT_CODE}"
exit $EXIT_CODE
fi
done
echo "🎉 全部完成,共尝试 ${RETRY_COUNT} 次"
这种写法的优点是逻辑清晰,容易扩展日志和告警。缺点是如果任务本身就很快失败(非超时),循环会在第一次就退出——这个行为有时候是我们想要的,有时候不是,可以根据需要调整判断条件。
方案二:for循环精确控制重试次数
有时候你不关心"多久成功",只关心"最多试几次"。for循环写法更简洁,适合对重试次数有严格限制的场景。
#!/bin/bash
MAX_RETRIES=3
TIMEOUT_SEC=20
for i in $(seq 1 $MAX_RETRIES); do
echo "[$(date '+%H:%M:%S')] 尝试 $i/${MAX_RETRIES}"
timeout ${TIMEOUT_SEC}s curl -s -f https://api.example.com/sync
RET=$?
if [[ $RET -eq 0 ]]; then
echo "✅ 同步成功"
exit 0
fi
if [[ $RET -eq 124 ]]; then
echo "⏰ 超时"
else
echo "❌ 失败,退出码 $RET"
fi
if [[ $i -lt $MAX_RETRIES ]]; then
sleep 2
fi
done
echo "❌ 全部重试失败"
exit 1
这个脚本的逻辑非常直接:最多试三次,每次间隔2秒。适合那种"尽力而为、试够次数就算"的任务。注意这里用了curl的-f参数,当HTTP状态码>=400时会返回22退出码,被for循环捕获后同样会重试。
方案三:指数退避重试(专业场景)
有些任务不急着马上重试,而是希望间隔时间逐渐拉长——这就是指数退避(Exponential Backoff)。网络不稳定时,频繁重试往往会加剧问题,指数退避是更合理的策略。
#!/bin/bash
TIMEOUT_SEC=15
BASE_DELAY=2
MAX_RETRIES=5
delay=$BASE_DELAY
for attempt in $(seq 1 $MAX_RETRIES); do
echo "[$(date '+%H:%M:%S')] 第${attempt}次尝试,超时${TIMEOUT_SEC}秒"
timeout ${TIMEOUT_SEC}s curl -s -f https://api.example.com/health > /tmp/health_check.json
RET=$?
if [[ $RET -eq 0 ]]; then
echo "✅ 健康检查通过"
cat /tmp/health_check.json
rm -f /tmp/health_check.json
exit 0
fi
if [[ $RET -eq 124 ]]; then
echo "⏰ 第${attempt}次超时"
else
echo "❌ 失败,退出码 $RET"
fi
if [[ $attempt -lt $MAX_RETRIES ]]; then
echo " ${delay}秒后重试(指数退避)..."
sleep $delay
delay=$(($delay * 2))
[[ $delay -gt 60 ]] && delay=60
fi
done
echo "❌ 重试${MAX_RETRIES}次后仍失败"
exit 1
指数退避的关键就是让等待时间每次翻倍:delay=$(($delay * 2))。这个脚本的重试间隔是2秒、4秒、8秒、16秒,最大不超过60秒。对API调用、远程同步这类场景,指数退避能有效避免雪崩效应。
三种方案怎么选
总结一下适用场景:
- while循环:需要明确区分超时失败和其他失败,想做详细日志记录和告警通知
- for循环:对重试次数有严格限制,任务本身失败也要重试,简单粗暴
- 指数退避:网络相关任务、API调用、不希望频繁重试加剧服务端压力的场景
实际项目中,这三种写法可以根据具体需求组合。比如用while循环做外层控制,for循环做内层重试,指数退避控制间隔时间。
和xargs的timeout重试有什么不同
之前文章里写过用xargs配合timeout做并行任务的重试,那套方案的核心优势是并行处理。而本文的循环重试是串行的,优势在于逻辑简单、状态可控,适合那些"必须成功、不接受跳过"的单任务场景。两者不是替代关系,而是互补关系:xargs负责大规模并行,循环重试负责精细化控制。
搞懂了这三种写法,下次遇到批量任务超时问题,就不用再手动盯着了。
相关阅读:
版权声明
本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论