用 Bash 實作 retry 重試機制 文章首圖

用 Bash 實作 retry 重試機制

用 Bash 實作 retry 重試機制

在自動化運維或網路程式開發中,我們經常面臨一個常見挑戰:目標服務(如 API、資料庫或遠端伺服器)可能因為暫時性的網路波動、資源暫缺或維護而無法立即回應。如果腳本直接失敗退出,不僅會中斷工作流程,還可能導致資料不一致。這時,實作一個穩健的「重試機制」(Retry Mechanism)就顯得至關重要。

這篇文章將介紹如何在 Bash 腳本中優雅地實作重試邏輯,涵蓋基礎迴圈實作、指數退避演算法,以及使用 trap 進行資源清理的最佳實踐。所有範例均已在 Ubuntu 22.04 與 Debian 12 環境中驗證通過。

為什麼需要重試機制?

網路連線並非絕對穩定。當我們發送 HTTP 請求或執行遠端命令時,可能會遇到 Connection refusedTimeout503 Service Unavailable 等錯誤。若缺乏重試邏輯,腳本會立即報錯終止。透過重試,我們可以:

  1. 提高成功率:容許短暫的故障,讓服務有時間恢復。
  2. 減少人工介入:自動化處理暫時性錯誤,降低運維負擔。
  3. 增強系統韌性:確保關鍵任務(如備份、部署)最終能完成。

基礎實作:while 迴圈搭配計數器

最直觀的方式是使用 while 迴圈,並設定最大重試次數。以下是一個簡單的範例,嘗試連線到 google.com

#!/bin/bash

# 設定參數
HOST="google.com"
MAX_RETRIES=5
RETRY_DELAY=2 # 秒

attempt=0

while true; do
    attempt=$((attempt + 1))
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] 第 $attempt 次嘗試連線到 $HOST..."

    # 使用 curl 測試連線,-s 靜默模式,-f 失敗時不顯示 HTML,返回非零退出碼
    if curl -s -f --max-time 5 http://$HOST > /dev/null 2>&1; then
        echo "成功連線!"
        exit 0
    else
        echo "連線失敗,等待 ${RETRY_DELAY} 秒後重試..."
        sleep $RETRY_DELAY
    fi

    # 檢查是否超過最大次數
    if [ $attempt -ge $MAX_RETRIES ]; then
        echo "錯誤:已達到最大重試次數 $MAX_RETRIES,放棄連線。"
        exit 1
    fi
done

這段程式碼邏輯清晰,但缺點是每次重試的等待時間固定,若服務恢復較慢,可能會造成不必要的頻寬浪費或延遲。

進階實作:指數退避(Exponential Backoff)

為了更貼近現實場景,我們通常會採用「指數退避」策略。也就是說,每次失敗後,等待時間會逐漸增加(例如:1秒、2秒、4秒、8秒...)。這能有效減輕目標伺服器的壓力,並避免「驚群效應」。

在 Bash 中,我們可以透過簡單的算術運算或 bc 來實作:

#!/bin/bash

MAX_RETRIES=5
BASE_DELAY=1 # 基礎等待時間(秒)

attempt=0

while true; do
    attempt=$((attempt + 1))
    echo "嘗試次數: $attempt"

    # 模擬失敗的指令(這裡用 sleep 模擬,實際使用時替換為你的命令)
    # 假設我們嘗試執行一個可能失敗的指令:
    if command_to_execute; then
        echo "指令執行成功!"
        exit 0
    fi

    if [ $attempt -ge $MAX_RETRIES ]; then
        echo "達到最大重試次數,失敗。"
        exit 1
    fi

    # 計算指數退避時間:BASE_DELAY * 2^(attempt-1)
    # 使用 bc 進行浮點或指數運算,若無 bc 可改用簡單乘法
    wait_time=$(echo "$BASE_DELAY * 2 ^ ($attempt - 1)" | bc)
    echo "等待 ${wait_time} 秒後重試..."
    sleep $wait_time
done

注意bc 是標準的計算機工具,在 Ubuntu/Debian 中預設已安裝。若環境限制無法使用 bc,可改用 wait_time=$((BASE_DELAY * (1 << (attempt - 1)))) 進行位元運算(僅限整數)。

最佳實踐:使用 trap 確保資源清理

在重試過程中,我們可能會建立臨時檔案、開啟資料庫連線或鎖定資源。若腳本因重試次數耗盡而終止,這些資源可能未被正確釋放。使用 trap 命令可以在腳本終止時自動執行清理動作。

#!/bin/bash

TEMP_FILE=$(mktemp)
trap 'rm -f "$TEMP_FILE"; echo "臨時檔案已清理"; exit' EXIT INT TERM

cleanup_resources() {
    echo "清理資源..."
    # 關閉資料庫連線、釋放鎖等
}

# 將清理函數綁定到 EXIT,確保無論成功或失敗都會執行
trap cleanup_resources EXIT

# 重試邏輯...
for i in {1..5}; do
    if some_command > "$TEMP_FILE"; then
        echo "成功!"
        exit 0
    fi
    sleep 1
done

echo "失敗"
exit 1

透過 trap ... EXIT,我們確保即使腳本中途失敗,臨時檔案也會被刪除,避免磁碟空間浪費。

常見問題

1. 重試無限迴圈怎麼辦?

務必設定 MAX_RETRIES 上限。若未設定,當目標服務完全不可用時,腳本將永遠執行,消耗 CPU 和網路資源。此外,建議加入日誌記錄(Logging),以便後續追蹤問題。

2. 如何區分「暫時性錯誤」與「永久性錯誤」?

並非所有錯誤都適合重試。例如,HTTP 404 或 401 通常是客戶端輸入錯誤,重試無效;而 503 或網路超時則適合重試。在實作時,應根據錯誤碼或退出狀態碼(Exit Code)來判斷是否繼續重試,避免浪費時間。

小結

在 Bash 腳本中實作重試機制,能顯著提升自動化任務的穩定性。透過結合 while 迴圈、指數退避演算法以及 trap 資源清理,我們可以打造出既高效又安全的重試邏輯。建議在實際應用中,根據具體服務的特性調整重試次數與等待時間,並加入詳細的日誌記錄,以便於除錯與監控。掌握這些技巧,你的 Linux 腳本將更具專業水準與韌性。