如何在存储设备的SSH会话中并行执行多条命令?
优化存储卷批量处理脚本的并行方案
针对4万+存储卷的串行处理效率问题,以下从并行执行、减少SSH开销、代码精简三个维度给出落地优化方案:
一、用GNU Parallel实现SSH并行执行
GNU Parallel是shell环境下轻量高效的并行任务工具,可直接将串行循环改为并行,同时通过并发数控制避免存储设备过载。
改造要点
- 将单卷处理逻辑封装为独立函数
- 读取CSV数据后用Parallel分发任务
- 调整并发数(建议10-20,根据设备负载测试确定)
优化后代码示例
#!/bin/bash PATH="/bin:/usr/bin:/usr/local/bin:/usr/local/sbin:/usr/sbin" export PATH # 单卷处理函数 process_volume() { local proj="$1" local vols=$(echo "$proj" | awk -F: '{print $2}') local ldap_proj=$(echo "$proj" | awk -F: '{print $1}') local ssh_server="" # 用关联数组替代重复的服务器匹配逻辑 declare -A server_map=( [fxn1]=dbcl101 [fxn2]=dbcl201 [fxn3]=dbcl301 [fxn4]=dbcl401 [fxn5]=dbcl501 [fxn6]=dbcl601 [fxn7]=dbcl701 [fxn8]=dbcl804 [fxn017]=dbcl031 [fxn015]=dbcl032 [fxn016]=dbcl034 [fxn014]=dbcl028 ) # 匹配对应存储服务器 for prefix in "${!server_map[@]}"; do if [[ "$vols" =~ "$prefix" ]]; then ssh_server="${server_map[$prefix]}" break fi done [[ -z "$ssh_server" ]] && return # 获取卷类型 volstyle=$(ssh "$ssh_server" "row 0; vol show $vols -fields volume-style-extended" | awk '/fxn/{print $3}') if [[ "$volstyle" == "flexvol" ]]; then echo -n "$ldap_proj $vols " ssh "$ssh_server" "row 0;set -unit MB; \ vol show $vols -fields performance-tier-inactive-user-data; \ vol show-footprint $vols -fields volume-blocks-footprint-bin0,volume-blocks-footprint-bin1,volume-blocks-footprint" \ | awk '/fxn/{print $3, $4, $5}' elif [[ "$volstyle" == "flexgroup" ]]; then echo -n "$ldap_proj $vols " flexgroupvol="${vols}__*" ssh "$ssh_server" "row 0;set -unit MB; \ vol show $vols -fields performance-tier-inactive-user-data; \ vol show-footprint $flexgroupvol -fields volume-blocks-footprint-bin0,volume-blocks-footprint-bin1,volume-blocks-footprint" \ | awk '/^fxn/{ s1+=$3; s2+=$4; s3+=$5; if (FNR == 8) { print $3 } } END { print s1, s2, s3 }' fi } # 保留原日志文件生成逻辑 today=$(date +%Y%m%d) number=0 datafile="/home/archive_dashboard/storage_footprint-$today" while [ -e "$datafile" ]; do printf -v datafile '%s-%02d' "/home/archive_dashboard/storage_footprint-$today" "$(( ++number ))" done printf 'Will use "%s" as filename\n' "$datafile" touch "$datafile" # 并行处理CSV数据,并发数设为15(可调整) awk -F ',' 'BEGIN{getline; NR--} {gsub(/"/,""); printf $2":"$NF"\n"}' volumes.csv | parallel --jobs 15 process_volume {} > "$datafile.log1" # 保留原日志格式化逻辑 sed -i -e 's/\r//g; s/-/0/g; s/MB//g' "$datafile.log1" sed '/^[[:space:]]*$/d' "$datafile.log1"| paste -d " " - - > "$datafile.log2" awk '{$1=$1}1' OFS="," "$datafile.log2" > "${datafile}" sed -i '1s/^/Project_Name,Volume_Name,InactiveTier,PhysicalBlocks,SSDtier,GRIDtier\n/' "${datafile}" rm -rf "$datafile.log2" "$datafile.log1" exit
注意事项
- 提前安装GNU Parallel(
apt install parallel/yum install parallel) - 配置SSH免密登录到所有存储服务器,避免并行时弹出密码提示
- 先小批量测试并发数,避免压垮存储设备
二、批量处理同服务器卷,减少SSH连接开销
原脚本每个卷建立一次SSH连接,4万次握手开销极大。可先按服务器分组,每组一次SSH连接处理所有卷,直接减少99%的连接次数。
改造要点
- 将CSV中的卷按服务器分组,生成每个服务器的卷列表
- 单SSH会话内批量执行所有卷的查询命令
- 远程批量处理,避免重复建立会话
核心分组逻辑示例
# 按服务器分组生成临时文件 awk -F ',' 'BEGIN{getline; NR--} {gsub(/"/,""); print $2":"$NF}' volumes.csv | while read proj; do vols=$(echo "$proj" | awk -F: '{print $2}') ldap_proj=$(echo "$proj" | awk -F: '{print $1}') ssh_server="" declare -A server_map=( [fxn1]=dbcl101 [fxn2]=dbcl201 [fxn3]=dbcl301 [fxn4]=dbcl401 [fxn5]=dbcl501 [fxn6]=dbcl601 [fxn7]=dbcl701 [fxn8]=dbcl804 [fxn017]=dbcl031 [fxn015]=dbcl032 [fxn016]=dbcl034 [fxn014]=dbcl028 ) for prefix in "${!server_map[@]}"; do if [[ "$vols" =~ "$prefix" ]]; then ssh_server="${server_map[$prefix]}" break fi done [[ -n "$ssh_server" ]] && echo "$ssh_server,$ldap_proj,$vols" >> /tmp/vols_by_server.csv done # 按服务器批量处理 sort -t, -k1,1 /tmp/vols_by_server.csv | awk -F ',' '{ if ($1 != prev_server) { if (prev_server != "") print prev_server, vol_list, proj_list prev_server = $1; vol_list = $3; proj_list = $2 } else { vol_list = vol_list " " $3; proj_list = proj_list " " $2 } } END {print prev_server, vol_list, proj_list}' | while read server vols projs; do # 一次SSH连接处理该服务器所有卷 ssh "$server" "row 0; set -unit MB; \ vol show -volume {$vols} -fields volume,volume-style-extended; \ vol show -volume {$vols} -fields volume,performance-tier-inactive-user-data; \ vol show-footprint -volume {$vols} -fields volume,volume-blocks-footprint-bin0,volume-blocks-footprint-bin1,volume-blocks-footprint" \ | awk -v projs="$projs" ' # 此处编写逻辑将批量返回数据映射到对应项目名,输出符合原格式的内容 # 需根据存储设备的输出格式调整 ' >> "$datafile.log1" done
三、精简原脚本冗余代码
原脚本重复多次[[ $vols =~ fxnX ]]匹配逻辑,改用关联数组可大幅精简代码,减少出错概率:
declare -A server_map=( [fxn1]=dbcl101 [fxn2]=dbcl201 [fxn3]=dbcl301 [fxn4]=dbcl401 [fxn5]=dbcl501 [fxn6]=dbcl601 [fxn7]=dbcl701 [fxn8]=dbcl804 [fxn017]=dbcl031 [fxn015]=dbcl032 [fxn016]=dbcl034 [fxn014]=dbcl028 ) # 匹配服务器 for prefix in "${!server_map[@]}"; do if [[ "$vols" =~ "$prefix" ]]; then ssh_server="${server_map[$prefix]}" break fi done
额外优化建议
- 超时控制:给SSH命令添加超时参数(
ssh -o ConnectTimeout=10 -o ServerAliveInterval=5),避免任务卡住 - 错误日志:添加失败卷的记录逻辑,方便后续重试
- 预缓存卷类型:若卷类型不会频繁变化,可提前导出所有卷的类型到本地文件,避免每次查询
- 负载监控:并行执行时监控存储设备的CPU/内存负载,动态调整并发数
内容的提问来源于stack exchange,提问作者Karn Kumar
相关产品推荐
相关产品推荐

