实战指南:高效批量测试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
这个脚本能工作,但存在几个问题:
- 没有进度显示
- 错误处理不完善
- 串行执行,速度慢
第二阶段:添加功能增强
改进版本增加了以下功能:
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个
可能原因:
- Windows换行符(CRLF)
- 文件中有特殊字符
- 读取逻辑有缺陷
解决方案:
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 ...
安全注意事项
尽管这是测试脚本,但仍需注意安全:
- 密码处理:不要在脚本中硬编码密码,考虑使用环境变量或配置文件
- 结果保护:测试结果可能包含敏感信息,妥善保管
- 权限控制:脚本应运行在受控环境中
- 日志清理:定期清理日志文件
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
}
总结与建议
经过这次实战,我总结了以下经验:
核心原则
- 简单可靠胜过复杂先进:先确保基础功能稳定
- 逐步增强:从简单版本开始,逐步添加功能
- 充分测试:小范围测试验证后再批量运行
- 错误处理:预见到可能的问题并提前处理
技术建议
- 使用
set -euo pipefail让脚本更健壮 - 总是验证输入数据的有效性
- 为长时间运行的任务添加进度反馈
- 使用临时文件处理并发写入问题
- 清理临时文件和资源
运维建议
- 保持脚本的日志记录能力
- 考虑添加监控和告警机制
- 定期回顾和优化脚本
- 文档化脚本的使用和维护方法
批量测试SSH连接虽然看起来简单,但实际实施时会遇到各种预料之外的问题。通过系统的分析和逐步的改进,我们最终得到了一个稳定、高效、功能完善的解决方案。这个过程不仅解决了眼前的问题,也为类似任务积累了宝贵经验。
记住:好的自动化脚本不是一蹴而就的,而是在不断遇到问题、解决问题的过程中逐渐完善起来的。每个错误和调试过程都是学习和提高的机会。