SSH批量连接测试实战
运维生涯中最打脸的经历:本以为一行ssh命令就能搞定的事,折腾了整整一下午。200多个IP,脚本跑了4个就卡死;手动测试明明能连上,自动化脚本却疯狂报错;好不容易跑通了,速度又慢得让人崩溃……
这篇文章,记录了我从“这有什么难的”到“原来水这么深”的全过程。如果你也需要批量测试服务器连接性,这篇实战避坑手册能帮你省下至少一个下午的调试时间。
一、需求:一个看似简单的任务
事情是这样的:某天下午,领导扔过来一个IP列表,大概200多行,要求确认这些服务器的SSH连接是否正常。账号密码统一,只需要测试连通性,2-3秒超时,结果按成功/失败分类保存。
听起来很简单对吗?我当时也是这么想的。
原始需求拆解:
测试一批IP的22端口SSH连接
固定账号:root
固定密码:mn150@2099A
超时控制:2-3秒
结果分类输出:成功IP / 失败IP
支持批量:数百个IP
环境约束:
操作系统:CentOS 7
网络:存在不确定延迟和防火墙策略
性质:测试环境,可使用密码认证
二、技术选型
动手之前,先评估一下有哪些路可以走。
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| sshpass + 循环 | sshpass -p '密码' ssh root@IP | 简单直接,无额外依赖 | 串行执行,数百IP耗时数分钟 | 少量IP测试 |
| expect脚本 | spawn ssh+expect "password:" | 交互处理稳定 | 语法晦涩,调试困难 | 需要复杂交互的场景 |
| Python + Paramiko | paramiko.SSHClient().connect() | 功能全面,错误处理完善 | 需要Python环境,依赖安装 | 需要后续二次开发的场景 |
| GNU parallel并行 | parallel -j 20 | 速度极快 | 并发控制复杂,文件写入冲突 | 大量IP,追求效率 |
我最终选择了sshpass + 循环作为基础方案,原因很简单:环境最干净,不需要装任何额外的东西。等基础跑通了,再考虑并行优化。
三、踩坑实录
坑1:脚本只跑了4个IP就停了
现象:执行./ssh_test.sh,只处理了4个IP就莫名其妙退出,没有任何错误提示。
排查:加set -x调试,发现报错:
./ssh_test.sh: line 1: i#!/bin/bash: No such file or directory原因:脚本文件是从Windows记事本复制过来的,保存成了带BOM头的UTF-8格式,#!/bin/bash前面多了看不见的垃圾字符。
解决方案:
# 查看文件编码 file ssh_test.sh # 方法一:用 sed 清除第一行的垃圾字符 sed -i '1s/^.*#!/#!/' ssh_test.sh # 方法二:用 dos2unix 转换(同时修复换行符问题) yum install -y dos2unix dos2unix ssh_test.sh教训:从Windows环境复制脚本到Linux,永远先执行dos2unix。
坑2:IP文件有200行,脚本只读了4行
现象:确认脚本本身没问题,但while read循环只执行了4次就结束了。
原因:IP文件也是从Windows复制过来的,每行末尾带着\r(CRLF换行符)。read命令读到\r后,把它当作行内容的一部分,导致后续逻辑异常。
解决方案:
# 检查文件中的隐藏字符 cat -A iplist.txt | head -5 # 如果看到 ^M$,说明是Windows换行符 # 修复换行符 dos2unix iplist.txt # 或者用 sed sed -i 's/\r$//' iplist.txt教训:任何从外部来的文本文件,先做格式清洗。
坑3:手动SSH能通,脚本却一直失败
现象:手动执行ssh root@IP能正常连接,但脚本里的sshpass命令始终返回非0。
排查:对比手动和脚本的SSH选项差异。最终发现是-o BatchMode=yes这个选项在作祟——它会禁用密码认证,导致sshpass根本无法工作。
解决方案:
# 错误配置 sshpass -p '密码' ssh -o BatchMode=yes root@IP # 正确配置(明确允许密码认证) sshpass -p '密码' ssh \ -o BatchMode=no \ -o PasswordAuthentication=yes \ -o StrictHostKeyChecking=no \ root@IP "exit"教训:SSH选项不是越多越好,BatchMode=yes在自动化密码认证场景下是致命的。
坑4:并发写入导致结果文件错乱
现象:改成并行执行后,结果文件里的IP数量对不上,有的IP重复,有的IP丢失。
原因:多个进程同时向同一个文件追加内容,没有加锁,导致写入冲突。
解决方案:每个进程写入独立的临时文件,最后合并。
TEMP_DIR=$(mktemp -d) # 每个进程写入 $TEMP_DIR/success_$$ # 最后 cat $TEMP_DIR/success_* > success.txt四、最终稳定版脚本
经过四轮迭代,最终版本具备以下特性:
环境预检(sshpass是否安装、IP文件是否存在)
IP文件智能清洗(去注释、去空行、去Windows换行符、格式校验)
进度显示(百分比 + 动态更新)
结果分类输出(带时间戳,防止覆盖)
统计报告(总数、成功数、失败数、成功率)
临时文件自动清理
#!/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() { if ! command -v sshpass &> /dev/null; then echo " sshpass 未安装" echo " 请执行: sudo yum install -y epel-release sshpass" exit 1 fi if [ ! -f "$IP_FILE" ]; then echo "IP文件 $IP_FILE 不存在" exit 1 fi } # ==================== IP列表清洗 ==================== prepare_ip_list() { local input_file="$1" local output_file="$2" > "$output_file" while IFS= read -r line || [ -n "$line" ]; do # 1. 剥离Windows换行符 line=$(echo "$line" | tr -d '\r') # 2. 去除首尾空白 line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # 跳过空行和注释 [[ -z "$line" || "$line" =~ ^# ]] && continue # 提取IP(移除可能的尾随注释) ip=$(echo "$line" | awk '{print $1}') # 校验IPv4格式 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 ${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 "成功输出: $SUCCESS_FILE" echo "失败输出: $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 success=0 failed=0 while read ip; do tested=$((tested + 1)) # 进度显示(每10个或最后一个更新一次) 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 "总IP数: $total_ips" echo "成功: $success" echo "失败: $failed" if [ "$total_ips" -gt 0 ]; then echo "成功率: $(echo "scale=2; $success*100/$total_ips" | bc)%" fi echo "" echo "结果文件:" echo "$SUCCESS_FILE" echo "$FAILED_FILE" # 展示前5个成功的IP if [ -s "$SUCCESS_FILE" ]; then echo "" echo "成功IP示例(前5个):" head -5 "$SUCCESS_FILE" | cat -n fi # 清理临时文件 rm -f "$clean_ip_file" } main五、核心技术要点总结
5.1 健壮的IP文件处理模板
while IFS= read -r line || [ -n "$line" ]; do line=$(echo "$line" | tr -d '\r') # 干掉Windows换行符 line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # 去首尾空白 [[ -z "$line" || "$line" =~ ^# ]] && continue # 跳过空行和注释 ip=$(echo "$line" | awk '{print $1}') # 提取第一列 [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "$ip" done < ipfile.txt5.2 安全稳定的SSH选项组合
| 选项 | 值 | 作用 |
|---|---|---|
ConnectTimeout | 3 | 连接超时,避免卡死 |
StrictHostKeyChecking | no | 测试环境跳过主机密钥确认 |
PasswordAuthentication | yes | 明确允许密码认证 |
BatchMode | no | 不禁用密码认证 |
LogLevel | ERROR | 只输出错误,减少干扰 |
5.3 退出码判断参考
timeout $TIMEOUT sshpass ... case $? in 0) echo "连接成功" ;; 124) echo "连接超时" ;; 5) echo "认证失败(密码错误或账号被禁用)" ;; *) echo "其他错误(网络不通、端口关闭等)" ;; esac六、运维经验沉淀
经过这次实战,总结了几条值得记住的原则:
| 原则 | 说明 |
|---|---|
| 小范围验证 | 先用2-3个IP测试脚本逻辑,确认无误再批量跑 |
| 格式清洗前置 | 任何外部来的文本文件,先dos2unix再处理 |
| 逐层加功能 | 先跑通基础逻辑 → 加进度显示 → 加错误处理 → 加并行 |
| 临时文件隔离 | 并行场景下,每个进程写入独立文件,最后合并 |
| 留下日志 | 脚本执行时间、结果统计、异常信息都记录下来 |
批量测试SSH连接,表面上是技术问题,本质上是对细节的把控,编码格式、换行符、SSH选项、并发冲突,任何一个细节没处理好,整个流程就会卡住。
好的自动化脚本不是一蹴而就的,而是在不断遇到问题、解决问题的过程中逐渐打磨出来的。希望这篇踩坑实录能帮你避开我走过的弯路。
