实战指南:高效批量测试SSH连接的最佳实践与避坑手册

实战指南:高效批量测试SSH连接的最佳实践与避坑手册

引言:当自动化遇到现实挑战

在运维工作中,我们常常需要管理大量服务器。最近我遇到一个实际需求:需要快速测试一批服务器的SSH连接性,所有服务器都使用相同的root账号和密码。这听起来应该很简单,但实施过程中却遇到了各种意想不到的问题。本文将详细记录这次实战经历,分享完整的解决方案,并总结出可靠的最佳实践。

需求分析:明确目标与约束条件

原始需求

  • 测试一批IP地址的22端口SSH连接
  • 使用固定账号:root
  • 使用固定密码:mn150@2099A
  • 需要超时控制(2-3秒)
  • 结果分类:成功与失败的IP分别保存
  • 支持批量处理(IP数量可能达到数百个)

技术约束

  • 运行环境:CentOS 7
  • 网络条件:可能存在网络延迟和防火墙限制
  • 安全考虑:虽然使用密码认证,但这是测试环境

技术选型:多种方案的对比

方案一:sshpass + 循环(最直接)

bash 复制代码
sshpass -p '密码' ssh -o ConnectTimeout=5 root@IP "命令"

优点 :简单直接,无需额外依赖
缺点:缺乏并行处理,速度慢

方案二:expect脚本(更健壮)

bash 复制代码
expect << EOF
spawn ssh root@IP
expect "password:" { send "密码\r" }
expect eof
EOF

优点 :处理交互更稳定
缺点:语法复杂,调试困难

方案三:Python + Paramiko(功能强大)

python 复制代码
import paramiko
ssh = paramiko.SSHClient()
ssh.connect(hostname=ip, username='root', password='密码', timeout=3)

优点 :功能全面,错误处理完善
缺点:需要Python环境

方案四:并行处理(效率最高)

bash 复制代码
cat iplist.txt | parallel -j 20 "测试命令"

优点 :大幅提升测试速度
缺点:并发控制复杂,容易出错

实战过程:从简单到复杂的演进

第一阶段:基础脚本开发

最初的脚本很简单:

bash 复制代码
#!/bin/bash
PASSWORD='mn150@2099A'

while read ip; do
    sshpass -p "$PASSWORD" ssh -o ConnectTimeout=3 root@$ip "echo ok"
    if [ $? -eq 0 ]; then
        echo "$ip" >> success.txt
    else
        echo "$ip" >> fail.txt
    fi
done < iplist.txt

这个脚本能工作,但存在几个问题:

  1. 没有进度显示
  2. 错误处理不完善
  3. 串行执行,速度慢

第二阶段:添加功能增强

改进版本增加了以下功能:

bash 复制代码
#!/bin/bash
# 配置参数
PASSWORD='mn150@2099A'
TIMEOUT=3
IP_FILE="iplist.txt"
SUCCESS_FILE="success_ips.txt"
FAILED_FILE="failed_ips.txt"

# 统计和进度
TOTAL=$(wc -l < "$IP_FILE")
COUNT=0

echo "开始测试 $TOTAL 个IP..."

while read ip; do
    COUNT=$((COUNT + 1))
    echo -n "[$COUNT/$TOTAL] 测试 $ip ... "
    
    timeout $TIMEOUT sshpass -p "$PASSWORD" ssh \
        -o ConnectTimeout=$TIMEOUT \
        -o StrictHostKeyChecking=no \
        root@"$ip" "exit" 2>/dev/null
    
    if [ $? -eq 0 ]; then
        echo "成功"
        echo "$ip" >> "$SUCCESS_FILE"
    else
        echo "失败"
        echo "$ip" >> "$FAILED_FILE"
    fi
done < "$IP_FILE"

这个版本已经有了基本框架,但仍然不够理想。

第三阶段:遇到的坑与解决方案

坑1:脚本编码问题

现象 :脚本只运行了4个IP就停止
错误信息 :./ssh_batch_test.sh: line 1: i#!/bin/bash: No such file or directory

根本原因:脚本文件编码问题,可能是从Windows复制过来,或者编辑器保存时加入了BOM头。

解决方案:

bash 复制代码
# 检查文件编码
file ssh_batch_test.sh

# 修复编码问题
sed -i '1s/^.*#!/#!/' ssh_batch_test.sh
# 或者重新创建脚本
cat > new_script.sh << 'EOF'
#!/bin/bash
# 正确内容
EOF
坑2:IP文件格式问题

现象:虽然IP文件有200多个IP,但脚本只处理了4个

可能原因:

  1. Windows换行符(CRLF)
  2. 文件中有特殊字符
  3. 读取逻辑有缺陷

解决方案:

bash 复制代码
# 检查文件格式
cat -A iplist.txt | head -10

# 修复换行符
dos2unix iplist.txt
# 或者使用sed
sed -i 's/\r$//' iplist.txt

# 清理文件
grep -v '^[[:space:]]*$' iplist.txt | grep -v '^#' > iplist_clean.txt
坑3:SSH选项冲突

现象:手动测试成功,但脚本测试失败

关键发现 :-o BatchMode=yes选项会禁用密码认证!

错误配置:

bash 复制代码
# 这个会失败
sshpass -p '密码' ssh -o BatchMode=yes root@IP "命令"

# 正确的应该是
sshpass -p '密码' ssh -o BatchMode=no root@IP "命令"
# 或者干脆不要BatchMode选项
坑4:并发写入冲突

现象:并行脚本运行异常,结果不完整

原因:多个进程同时写入同一个文件

解决方案:

bash 复制代码
# 每个进程写入独立文件,最后合并
TEMP_DIR=$(mktemp -d)
# 每个进程写入 $TEMP_DIR/result_$PID
# 最后 cat $TEMP_DIR/* > final_result.txt

第四阶段:最终稳定版本

经过多次调试,最终得到了稳定可靠的版本:

bash 复制代码
#!/bin/bash
# SSH批量测试脚本 - 最终稳定版

set -u  # 使用未定义变量时报错
set -e  # 遇到错误时退出

# 配置参数
PASSWORD='mn150@2099A'
CONNECT_TIMEOUT=3
IP_FILE="${1:-iplist.txt}"

# 生成带时间戳的结果文件
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
SUCCESS_FILE="success_ips_${TIMESTAMP}.txt"
FAILED_FILE="failed_ips_${TIMESTAMP}.txt"

# 验证环境
validate_environment() {
    # 检查sshpass
    if ! command -v sshpass &> /dev/null; then
        echo "错误: sshpass 未安装"
        echo "请执行: sudo yum install -y epel-release sshpass"
        exit 1
    fi
    
    # 检查IP文件
    if [ ! -f "$IP_FILE" ]; then
        echo "错误: IP文件 $IP_FILE 不存在"
        exit 1
    fi
    
    # 检查文件是否可读
    if [ ! -r "$IP_FILE" ]; then
        echo "错误: 无法读取IP文件 $IP_FILE"
        exit 1
    fi
}

# 清理和验证IP列表
prepare_ip_list() {
    local input_file="$1"
    local output_file="$2"
    
    # 清空输出文件
    > "$output_file"
    
    # 处理IP文件:移除注释、空行、换行符
    while IFS= read -r line || [ -n "$line" ]; do
        # 移除换行符和回车符
        line=$(echo "$line" | tr -d '\r')
        
        # 跳过空行和注释
        if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then
            continue
        fi
        
        # 提取IP(移除可能的后缀和注释)
        ip=$(echo "$line" | sed 's/[[:space:]].*$//' | sed 's/#.*$//')
        
        # 验证IP格式
        if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "$ip" >> "$output_file"
        else
            echo "警告: 跳过无效IP格式: $ip" >&2
        fi
    done < "$input_file"
}

# 测试单个IP
test_single_ip() {
    local ip="$1"
    
    # 使用timeout防止命令挂起
    timeout ${CONNECT_TIMEOUT} sshpass -p "$PASSWORD" ssh \
        -o ConnectTimeout=${CONNECT_TIMEOUT} \
        -o StrictHostKeyChecking=no \
        -o PasswordAuthentication=yes \
        -o BatchMode=no \
        -o LogLevel=ERROR \
        root@"$ip" "exit 0" 2>/dev/null
    
    return $?
}

# 主函数
main() {
    echo "========================================"
    echo "SSH批量连接测试工具"
    echo "========================================"
    echo "账号:        root"
    echo "超时时间:    ${CONNECT_TIMEOUT}秒"
    echo "IP文件:      $IP_FILE"
    echo "结果文件:"
    echo "  成功IP:    $SUCCESS_FILE"
    echo "  失败IP:    $FAILED_FILE"
    echo "========================================"
    
    # 验证环境
    validate_environment
    
    # 准备IP列表
    local clean_ip_file="/tmp/clean_ips_$$.txt"
    prepare_ip_list "$IP_FILE" "$clean_ip_file"
    
    local total_ips=$(wc -l < "$clean_ip_file" 2>/dev/null || echo 0)
    if [ $total_ips -eq 0 ]; then
        echo "错误: 没有有效的IP地址可以测试"
        exit 1
    fi
    
    echo "找到 $total_ips 个有效IP地址"
    echo ""
    
    # 清空结果文件
    > "$SUCCESS_FILE"
    > "$FAILED_FILE"
    
    # 开始测试
    echo "开始测试..."
    echo "----------------------------------------"
    
    local tested=0
    local success=0
    local failed=0
    
    while read ip; do
        tested=$((tested + 1))
        
        # 显示进度(每10个IP显示一次)
        if [ $((tested % 10)) -eq 0 ] || [ $tested -eq $total_ips ]; then
            printf "\r进度: %d/%d (%.1f%%)" $tested $total_ips $(echo "scale=1; $tested*100/$total_ips" | bc)
        fi
        
        # 测试连接
        if test_single_ip "$ip"; then
            echo "$ip" >> "$SUCCESS_FILE"
            success=$((success + 1))
        else
            echo "$ip" >> "$FAILED_FILE"
            failed=$((failed + 1))
        fi
    done < "$clean_ip_file"
    
    echo ""
    echo "----------------------------------------"
    echo ""
    
    # 生成报告
    echo "测试完成!"
    echo "========================================"
    echo "统计结果:"
    echo "  总IP数:    $total_ips"
    echo "  成功数:    $success"
    echo "  失败数:    $failed"
    
    if [ $total_ips -gt 0 ]; then
        local success_rate=$(echo "scale=2; $success * 100 / $total_ips" | bc)
        echo "  成功率:    ${success_rate}%"
    fi
    
    echo ""
    echo "结果文件已生成:"
    echo "  $SUCCESS_FILE"
    echo "  $FAILED_FILE"
    
    # 显示示例结果
    if [ -s "$SUCCESS_FILE" ]; then
        echo ""
        echo "成功IP示例(前5个):"
        head -5 "$SUCCESS_FILE" | cat -n
    fi
    
    # 清理临时文件
    rm -f "$clean_ip_file"
}

# 执行主函数
main

关键技术与最佳实践

1. 健壮的IP文件处理

bash 复制代码
# 正确处理各种格式的IP文件
while IFS= read -r line || [ -n "$line" ]; do
    # 处理换行符
    line=$(echo "$line" | tr -d '\r')
    
    # 验证IP格式
    if [[ "$line" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
        # 有效IP
    fi
done < ipfile.txt

2. 适当的SSH选项配置

bash 复制代码
# 推荐的SSH选项组合
sshpass -p "$PASSWORD" ssh \
    -o ConnectTimeout=$TIMEOUT \      # 连接超时
    -o StrictHostKeyChecking=no \     # 不检查主机密钥(测试环境)
    -o PasswordAuthentication=yes \   # 允许密码认证
    -o BatchMode=no \                 # 不禁用密码认证!
    -o LogLevel=ERROR \               # 只显示错误日志
    root@"$IP" "命令"

3. 合理的错误处理

bash 复制代码
# 使用timeout防止命令挂起
timeout $TIMEOUT 命令

# 检查退出码
if [ $? -eq 0 ]; then
    # 成功
elif [ $? -eq 124 ]; then
    # 超时
elif [ $? -eq 5 ]; then
    # 认证失败
else
    # 其他错误
fi

4. 进度反馈机制

bash 复制代码
# 显示进度百分比
printf "\r进度: %d/%d (%.1f%%)" $current $total $(echo "scale=1; $current*100/$total" | bc)

# 或使用进度条
draw_progress_bar() {
    local current=$1
    local total=$2
    local width=50
    local percent=$((current * 100 / total))
    local completed=$((current * width / total))
    
    printf "\r["
    printf "%${completed}s" | tr ' ' '='
    printf "%$((width - completed))s" | tr ' ' '.'
    printf "] %3d%%" $percent
}

性能优化技巧

并行处理方案

对于大量IP的测试,顺序执行太慢。以下是安全的并行方案:

bash 复制代码
#!/bin/bash
# 安全的并行测试

PASSWORD='mn150@2099A'
TIMEOUT=3
MAX_JOBS=10

# 创建临时目录存放结果
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT

# 测试函数
test_ip() {
    local ip=$1
    local pid=$$
    
    timeout $TIMEOUT sshpass -p "$PASSWORD" ssh \
        -o ConnectTimeout=$TIMEOUT \
        -o StrictHostKeyChecking=no \
        root@"$ip" "exit" 2>/dev/null
    
    if [ $? -eq 0 ]; then
        echo "$ip" >> "$TEMP_DIR/success_$pid"
    else
        echo "$ip" >> "$TEMP_DIR/failed_$pid"
    fi
}

# 导出函数
export -f test_ip
export PASSWORD TIMEOUT TEMP_DIR

# 并行执行
cat iplist.txt | xargs -P $MAX_JOBS -I {} bash -c 'test_ip "$@"' _ {}

# 合并结果
cat "$TEMP_DIR"/success_* 2>/dev/null | sort -u > success.txt
cat "$TEMP_DIR"/failed_* 2>/dev/null | sort -u > failed.txt

资源限制

bash 复制代码
# 避免过多的并发连接
ulimit -n 4096  # 增加文件描述符限制

# 限制每个进程的资源使用
prlimit --nproc=100 --nofile=100 --cpu=10 sshpass ...

安全注意事项

尽管这是测试脚本,但仍需注意安全:

  1. 密码处理:不要在脚本中硬编码密码,考虑使用环境变量或配置文件
  2. 结果保护:测试结果可能包含敏感信息,妥善保管
  3. 权限控制:脚本应运行在受控环境中
  4. 日志清理:定期清理日志文件
bash 复制代码
# 更安全的方式:从环境变量读取密码
PASSWORD=${SSH_PASSWORD:-'default_password'}

# 或者从加密文件读取
if [ -f ~/.ssh_test_pass ]; then
    PASSWORD=$(openssl enc -d -aes-256-cbc -in ~/.ssh_test_pass)
fi

扩展功能

1. 添加重试机制

bash 复制代码
test_with_retry() {
    local ip=$1
    local max_retries=2
    
    for ((i=1; i<=max_retries; i++)); do
        if test_single_ip "$ip"; then
            return 0
        fi
        sleep 1
    done
    return 1
}

2. 收集系统信息

bash 复制代码
# 测试时顺便收集信息
collect_info() {
    local ip=$1
    sshpass -p "$PASSWORD" ssh root@"$ip" "
        echo '=== 系统信息 ==='
        uname -a
        echo ''
        echo '=== 内存信息 ==='
        free -h
        echo ''
        echo '=== 磁盘信息 ==='
        df -h
    " > "info_${ip}.txt" 2>/dev/null
}

3. 生成HTML报告

bash 复制代码
generate_html_report() {
    cat > report.html << EOF
<html>
<head>
    <title>SSH测试报告</title>
    <style>
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; }
        th { background-color: #f2f2f2; }
        .success { background-color: #dff0d8; }
        .failed { background-color: #f2dede; }
    </style>
</head>
<body>
    <h1>SSH连接测试报告</h1>
    <p>生成时间: $(date)</p>
    <p>总计: $total_ips 个IP</p>
    <p>成功: $success 个</p>
    <p>失败: $failed 个</p>
    
    <h2>成功IP列表</h2>
    <table>
        <tr><th>序号</th><th>IP地址</th></tr>
        $(cat success.txt | awk '{print "<tr class=\"success\"><td>"NR"</td><td>"$1"</td></tr>"}')
    </table>
</body>
</html>
EOF
}

总结与建议

经过这次实战,我总结了以下经验:

核心原则

  1. 简单可靠胜过复杂先进:先确保基础功能稳定
  2. 逐步增强:从简单版本开始,逐步添加功能
  3. 充分测试:小范围测试验证后再批量运行
  4. 错误处理:预见到可能的问题并提前处理

技术建议

  1. 使用set -euo pipefail让脚本更健壮
  2. 总是验证输入数据的有效性
  3. 为长时间运行的任务添加进度反馈
  4. 使用临时文件处理并发写入问题
  5. 清理临时文件和资源

运维建议

  1. 保持脚本的日志记录能力
  2. 考虑添加监控和告警机制
  3. 定期回顾和优化脚本
  4. 文档化脚本的使用和维护方法

批量测试SSH连接虽然看起来简单,但实际实施时会遇到各种预料之外的问题。通过系统的分析和逐步的改进,我们最终得到了一个稳定、高效、功能完善的解决方案。这个过程不仅解决了眼前的问题,也为类似任务积累了宝贵经验。

记住:好的自动化脚本不是一蹴而就的,而是在不断遇到问题、解决问题的过程中逐渐完善起来的。每个错误和调试过程都是学习和提高的机会。

相关推荐
weifont3 小时前
自动化抓取和上架商品:能不动手,就少动手
运维·前端·自动化
拂拉氏3 小时前
【知识讲解】 Linux之动静态库的了解
linux·运维·服务器·动静态库
Mikko74 小时前
Linux 重启后日志没了?进程被 OOM 杀了去哪看?logrotate 之后日志还往旧文件写?三台真机实测日志与性能排查
linux·运维·centos
光依旧4 小时前
发版就掉请求?Spring Boot优雅停机的四步
运维·spring boot·nginx·高可用·优雅停机·mcp server
yichengerp5 小时前
电子行业常用erp系统有哪些?推荐适合中小电子厂的易呈erp软件,操作简单,易落地
大数据·运维·人工智能·云计算·制造
天国梦5 小时前
国密门禁品牌怎么选?2026年最新3个避坑要点
运维·服务器·数据库·python
信可维6 小时前
自动化操作多平台账号:登录态、双会话与 User Data 的 3 个坑
运维·自动化
龙仔7257 小时前
RustDesk 自建服务器 Key 强校验配置笔记
运维·服务器·笔记
大侠归来7 小时前
Docker 部署 Dolibarr:从零开始的完整指南
运维·docker·容器
KING-WU5127 小时前
Linux GDB 调试实战指南:从编译到断点调试
linux·运维·服务器