0

bash timeout自动重试循环脚本:3种写法让你的任务超时不再放弃

2026.07.11 | youres | 27次围观

批量跑任务最怕什么?不怕它慢,就怕它超时后直接放弃,特别是关键的数据同步、健康检查、接口调用这类必须成功的场景。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辅助作者原创,未经许可,转载请保留原文链接。

发表评论