用 Bash 實作簡單的任務佇列
在 Linux 系統管理與自動化運算中,我們經常面臨一個常見的需求:如何有效地管理多個同時執行的背景任務?當你需要處理大量檔案、執行網路請求或進行批次運算時,若完全放任系統並行處理,可能會導致資源耗盡(如記憶體溢出或 CPU 飢餓);若完全串行執行,又會浪費寶貴的等待時間。
為了解決這個兩難,「任務佇列」(Task Queue)的概念便應運而生。今天我們將使用純 Bash 腳本,實作一個簡單但極具實用價值的任務佇列系統。這個方案不需要安裝 Redis 或 RabbitMQ 等複雜的外部依賴,僅依賴檔案系統與 wait 指令,即可在 Ubuntu 22.04 或 Debian 12 上輕鬆運行。
核心原理與設計思路
Bash 本身是一個強大的編程語言,具備處理背景程序(Background Processes)的能力。我們的核心邏輯如下:
- 佇列儲存:使用一個純文字檔案來儲存待處理的任務清單。
- 消費者模型:啟動固定數量的背景工作程序(Workers),每個 Worker 不斷從佇列檔案中讀取任務。
- 互斥機制:利用
flock指令確保同一時間只有一個 Worker 能讀取並刪除任務,避免重複執行。 - 執行與回收:執行任務後,記錄結果並繼續等待下一個任務。
實作步驟
首先,請建立一個工作目錄並準備我們的腳本。我們將整個系統封存在一個腳本中,方便測試與部署。
#!/bin/bash
# 設定檔路徑
QUEUE_FILE="/tmp/task_queue.txt"
LOG_FILE="/tmp/worker.log"
MAX_WORKERS=3
LOCK_FILE="/tmp/task_queue.lock"
# 初始化佇列檔案
> "$QUEUE_FILE"
> "$LOG_FILE"
# 模擬耗時任務函數
process_task() {
local task_id="$1"
local task_cmd="$2"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Worker [$$] 開始執行任務: $task_id ($task_cmd)" >> "$LOG_FILE"
# 執行實際指令
eval "$task_cmd"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Worker [$$] 完成任務: $task_id" >> "$LOG_FILE"
}
# 主程式:啟動 Worker
start_workers() {
echo "啟動 $MAX_WORKERS 個 Worker..."
for ((i=1; i<=MAX_WORKERS; i++)); do
(
while true; do
# 使用 flock 確保原子性讀取
if flock -n 200; then
# 檢查佇列是否為空
if [ ! -s "$QUEUE_FILE" ]; then
flock -u 200
break # 佇列空了,結束 Worker
fi
# 讀取第一行並刪除它
task=$(head -n 1 "$QUEUE_FILE")
sed -i '1d' "$QUEUE_FILE"
flock -u 200
# 解析任務 (格式: ID|COMMAND)
task_id=$(echo "$task" | cut -d'|' -f1)
task_cmd=$(echo "$task" | cut -d'|' -f2-)
# 執行任務
process_task "$task_id" "$task_cmd"
else
# 若無法獲得鎖,短暫休眠後重試
sleep 0.1
fi
done
) &
done
# 等待所有背景 Worker 結束
wait
echo "所有任務處理完畢!"
}
# 新增任務至佇列
add_task() {
local task_id="$1"
local task_cmd="$2"
echo "${task_id}|${task_cmd}" >> "$QUEUE_FILE"
echo "已新增任務: $task_id"
}
# --- 主執行區 ---
# 1. 新增一些測試任務
add_task "001" "sleep 2 && echo '任務 001 完成'"
add_task "002" "sleep 1 && echo '任務 002 完成'"
add_task "003" "sleep 3 && echo '任務 003 完成'"
add_task "004" "echo '任務 004 瞬間完成'"
add_task "005" "sleep 1 && echo '任務 005 完成'"
# 2. 啟動佇列處理
start_workers
# 3. 查看日誌
echo "--- 執行日誌 ---"
cat "$LOG_FILE"
# 4. 清理
rm -f "$QUEUE_FILE" "$LOG_FILE" "$LOCK_FILE"
將上述程式碼儲存為 task_queue.sh,並賦予執行權限 chmod +x task_queue.sh,然後執行 ./task_queue.sh。你會看到三個 Worker 並行處理這五個任務,總執行時間將接近最長任務的時間(約 3-4 秒),而非串行執行的 7 秒以上。
常見問題與優化建議
1. 如何處理任務失敗或重試機制?
在上述範例中,若某個任務執行失敗,它不會被重新加入佇列。在生產環境中,建議在 process_task 函數中加入錯誤檢查。若 eval "$task_cmd" 回傳值非零,可將任務重新寫入佇列檔案(需再次獲取鎖),或寫入一個獨立的 failed_tasks.txt 以供後續人工介入或定時重試。
2. flock 的鎖釋放問題
我們的範例使用 flock -n 200 進行非阻塞鎖定。若腳本異常終止,鎖檔案可能會殘留。雖然 flock 在程序結束時會自動釋放鎖,但在極端情況下(如系統崩潰),建議定期清理 /tmp 下的鎖定檔案,或改用更嚴謹的鎖管理庫。此外,對於高併發場景,建議將佇列檔案從本地磁碟移至記憶體磁碟(如 tmpfs),以減少 I/O 瓶頸。
小結
透過 Bash 實作簡單的任務佇列,我們不僅能精確控制並行數量,避免伺服器資源過載,還能利用純原生工具實現複雜的自動化流程。這種方法特別適合中小規模的批次處理任務,或是作為微服務架構前的原型驗證。
雖然 Bash 並非為高併發設計,但結合 flock 與背景程序管理,它展現了 Linux 工具鏈的靈活與強大。下次當你面對一堆需要順序或有限並行處理的任務時,不妨試試這個輕量級的解決方案。