引言:当自动化遇到现实挑战
在运维工作中,我们常常需要管理大量服务器。最近我遇到一个实际需求:需要快速测试一批服务器的ssh连接性,所有服务器都使用相同的root账号和密码。这听起来应该很简单,但实施过程中却遇到了各种意想不到的问题。本文将详细记录这次实战经历,分享完整的解决方案,并总结出可靠的最佳实践。
需求分析:明确目标与约束条件
原始需求
- 测试一批ip地址的22端口ssh连接
- 使用固定账号:root
- 使用固定密码:mn150@2099a
- 需要超时控制(2-3秒)
- 结果分类:成功与失败的ip分别保存
- 支持批量处理(ip数量可能达到数百个)
技术约束
- 运行环境:centos 7
- 网络条件:可能存在网络延迟和防火墙限制
- 安全考虑:虽然使用密码认证,但这是测试环境
技术选型:多种方案的对比
方案一:sshpass + 循环(最直接)
sshpass -p '密码' ssh -o connecttimeout=5 root@ip "命令"
优点:简单直接,无需额外依赖
缺点:缺乏并行处理,速度慢
方案二:expect脚本(更健壮)
expect << eof
spawn ssh root@ip
expect "password:" { send "密码\r" }
expect eof
eof
优点:处理交互更稳定
缺点:语法复杂,调试困难
方案三:python + paramiko(功能强大)
import paramiko ssh = paramiko.sshclient() ssh.connect(hostname=ip, username='root', password='密码', timeout=3)
优点:功能全面,错误处理完善
缺点:需要python环境
方案四:并行处理(效率最高)
cat iplist.txt | parallel -j 20 "测试命令"
优点:大幅提升测试速度
缺点:并发控制复杂,容易出错
实战过程:从简单到复杂的演进
第一阶段:基础脚本开发
最初的脚本很简单:
#!/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
这个脚本能工作,但存在几个问题:
- 没有进度显示
- 错误处理不完善
- 串行执行,速度慢
第二阶段:添加功能增强
改进版本增加了以下功能:
#!/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头。
解决方案:
# 检查文件编码 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)
- 文件中有特殊字符
- 读取逻辑有缺陷
解决方案:
# 检查文件格式 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选项会禁用密码认证!
错误配置:
# 这个会失败 sshpass -p '密码' ssh -o batchmode=yes root@ip "命令" # 正确的应该是 sshpass -p '密码' ssh -o batchmode=no root@ip "命令" # 或者干脆不要batchmode选项
坑4:并发写入冲突
现象:并行脚本运行异常,结果不完整
原因:多个进程同时写入同一个文件
解决方案:
# 每个进程写入独立文件,最后合并 temp_dir=$(mktemp -d) # 每个进程写入 $temp_dir/result_$pid # 最后 cat $temp_dir/* > final_result.txt
第四阶段:最终稳定版本
经过多次调试,最终得到了稳定可靠的版本:
#!/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文件处理
# 正确处理各种格式的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选项配置
# 推荐的ssh选项组合
sshpass -p "$password" ssh \
-o connecttimeout=$timeout \ # 连接超时
-o stricthostkeychecking=no \ # 不检查主机密钥(测试环境)
-o passwordauthentication=yes \ # 允许密码认证
-o batchmode=no \ # 不禁用密码认证!
-o loglevel=error \ # 只显示错误日志
root@"$ip" "命令"
3. 合理的错误处理
# 使用timeout防止命令挂起
timeout $timeout 命令
# 检查退出码
if [ $? -eq 0 ]; then
# 成功
elif [ $? -eq 124 ]; then
# 超时
elif [ $? -eq 5 ]; then
# 认证失败
else
# 其他错误
fi
4. 进度反馈机制
# 显示进度百分比
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的测试,顺序执行太慢。以下是安全的并行方案:
#!/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
资源限制
# 避免过多的并发连接 ulimit -n 4096 # 增加文件描述符限制 # 限制每个进程的资源使用 prlimit --nproc=100 --nofile=100 --cpu=10 sshpass ...
安全注意事项
尽管这是测试脚本,但仍需注意安全:
- 密码处理:不要在脚本中硬编码密码,考虑使用环境变量或配置文件
- 结果保护:测试结果可能包含敏感信息,妥善保管
- 权限控制:脚本应运行在受控环境中
- 日志清理:定期清理日志文件
# 更安全的方式:从环境变量读取密码
password=${ssh_password:-'default_password'}
# 或者从加密文件读取
if [ -f ~/.ssh_test_pass ]; then
password=$(openssl enc -d -aes-256-cbc -in ~/.ssh_test_pass)
fi
扩展功能
1. 添加重试机制
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. 收集系统信息
# 测试时顺便收集信息
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报告
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连接虽然看起来简单,但实际实施时会遇到各种预料之外的问题。通过系统的分析和逐步的改进,我们最终得到了一个稳定、高效、功能完善的解决方案。这个过程不仅解决了眼前的问题,也为类似任务积累了宝贵经验。
记住:好的自动化脚本不是一蹴而就的,而是在不断遇到问题、解决问题的过程中逐渐完善起来的。每个错误和调试过程都是学习和提高的机会。
到此这篇关于ssh高效实现批量连接服务器的最佳实践与避坑指南的文章就介绍到这了,更多相关ssh批量连接服务器内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论