Shell 脚本 while 循环只执行一次的问题

最近在做 Prometheus 告警的自动化运维场景,我通过 Shell 脚本循环处理多个 long_uptime 告警。然而,脚本在首次执行 SSH 命令后直接退出循环,导致后续告警未被处理。本文记录完整的排错过程。

问题现象

原始代码片段:

while read -r alert; do
    # 提取告警信息...
    ssh "$node" "docker restart $container_name"
    # 其他逻辑...
done <<< "$alerts"

表现

  • 循环仅处理第一个告警后退出,未遍历所有符合条件的告警。
  • 取消注释 ssh 命令后问题消失,证明与 SSH 执行相关。

原因分析

1. SSH 阻塞导致循环中断

  • 默认行为:SSH 会读取标准输入(stdin),可能导致后续的 read 命令获取到空值
  • 错误传播:若 SSH 连接失败且未处理退出码,Bash 可能因 set -e 或默认行为终止脚本

2. 缺乏并发与超时控制

  • 串行执行:每个 SSH 命令需等待前一个完成,若节点响应慢,总耗时过长
  • 超时缺失:网络波动或节点宕机时,SSH 可能无限期挂起

解决方案

1. 非阻塞 SSH 执行

ssh -n -o ConnectTimeout=10 "$node" "docker restart $container_name" </dev/null &>/dev/null &
  • 关键参数
    • -n:禁用 stdin 输入
    • ConnectTimeout=10:10秒连接超时
    • &:后台执行,立即返回控制权

2. 并发控制与错误处理

max_jobs=5  # 最大并发数
while read -r alert; do
    # 处理告警逻辑...
    ssh "$node" "docker restart $container_name" &
    
    # 控制并发
    if [[ $(jobs -r -p | wc -l) -ge $max_jobs ]]; then
        wait -n  # 等待任意一个任务完成
    fi
done <<< "$alerts"
wait  # 等待所有后台任务
  • jobs -r -p:获取当前运行的后台任务 PID
  • wait -n:避免资源耗尽,动态控制并发数

3. 错误容忍设计

if ! ssh "$node" "docker restart $container_name"; then
    echo "Failed to restart $container_name on $node" >&2
    continue  # 跳过失败任务,继续处理后续告警
fi
  • continue:即使单个 SSH 失败,仍继续循环

验证步骤

  1. 模拟多告警输入

    alerts='[
      {"labels": {"category": "long_uptime", "name": "app1", "node": "node1"}},
      {"labels": {"category": "long_uptime", "name": "app2", "node": "node2"}}
    ]'
    

    确认脚本处理所有告警

  2. 压力测试
    使用 tc 模拟高延迟网络,观察脚本是否仍正常执行

    tc qdisc add dev eth0 root netem delay 2000ms
    

最终优化代码

while read -r alert; do
    # 提取变量(省略部分代码)
    if [[ "$category" == "long_uptime" ]]; then
        # 并行执行 SSH 命令
        ssh -n -o ConnectTimeout=10 "$node" "docker restart $container_name" </dev/null &>/dev/null &
        
        # 控制并发(示例:最大 10 个并行任务)
        if [[ $(jobs -r -p | wc -l) -ge 10 ]]; then
            wait -n
        fi
    fi
done <<< "$alerts"
wait  # 等待所有后台任务完成

Prometheus 数据保留时间配置 多维度优化降低Prometheus资源消耗的实践