机器一多,"挨个登录"就成了效率黑洞。10 台机器执行同一条命令,手工登 10 次、等 10 遍输出,一上午就没了;200 台更是不敢想。批量管理的核心是两件事:免密登录(不用每次输密码)和并发执行(不用串行等待)。这两件事做好了,批量运维才是真的"批量"。
这篇文章讲怎么配 SSH 密钥认证、怎么用 for 循环和 xargs -P 并发执行、怎么写一个小型批量执行脚本,以及什么时候该放弃自研脚本改用 Ansible 这类专业工具。内容面向中小团队的实际场景,命令都可直接运行。
文中的并发数、超时时间是经验值,请按你机器的网络延迟和目标数量调整。配置 SSH 会涉及 authorized_keys 和 sshd_config,改这些文件有把自己锁在外面的风险,我会给出防护做法和回滚方式。
一、SSH 密钥认证:先解决"免密"
批量执行的前提是免密。SSH 支持两种登录方式:密码认证和密钥认证。密钥认证用一对公私钥——公钥放在服务器上,私钥留在客户端,登录时用私钥完成证明,全程不传密码。它比密码更安全(私钥不上网、无法被暴力穷举),也是自动化必须的一步。
生成密钥对:
# 在"管理端"(你操作的机器)上生成密钥对
# -t 算法 -b 位数 -C 注释(建议用邮箱或用途,便于识别)
ssh-keygen -t ed25519 -C "ops@example.com"
# 如果需要兼容老的服务器/设备,用 RSA 2048 或 4096
ssh-keygen -t rsa -b 4096 -C "ops@example.com"
# 默认生成在 ~/.ssh/id_ed25519(私钥)和 ~/.ssh/id_ed25519.pub(公钥)
# 建议给私钥设置 passphrase(口令),更安全;用 ssh-agent 免重复输入
密钥算法怎么选:
| 算法 | 密钥长度 | 安全性 | 性能 | 建议 |
|---|---|---|---|---|
| RSA | 2048 | 够用 | 较慢 | 老环境兼容用 |
| RSA | 4096 | 高 | 慢 | 兼容性好,慢一些 |
| ECDSA | 256/384 | 高 | 快 | 主流可用 |
| Ed25519 | 固定 | 最高(推荐) | 最快 | 优先选它 |
结论很简单:优先用 Ed25519,只有在目标系统很老、不支持时,才退回 RSA 4096。
把公钥分发到目标服务器。最方便的是 ssh-copy-id:
# 把公钥追加到目标机的 ~/.ssh/authorized_keys(会自动建目录和权限)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@192.168.1.10
# 批量分发:对一批主机循环执行
for h in 192.168.1.{10..20}; do
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@"$h"
done
如果目标机没装 ssh-copy-id,可以手动来:
# 手动分发:把公钥内容追加到目标机
cat ~/.ssh/id_ed25519.pub | ssh user@192.168.1.10 \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
权限是密钥认证最常见的"明明配了却还要密码"的原因。规则是:~/.ssh 必须是 700,authorized_keys 必须是 600,私钥必须是 600,家目录不能对 group/other 可写。任何一条不符,sshd 会拒绝用密钥认证(这是个安全设计)。排查时可以用 ssh -v 看详细握手过程。
# 查看权限是否正确
ls -ld ~/.ssh ~/.ssh/authorized_keys
# 修正
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
二、用 SSH 配置简化多机操作
每台机器都写 user@ip 太啰嗦。用 ~/.ssh/config 给每台机器起别名,之后就能直接 ssh web01。
# ~/.ssh/config
Host web01
HostName 192.168.1.10
User deploy
Port 22
IdentityFile ~/.ssh/id_ed25519
Host web02
HostName 192.168.1.11
User deploy
# 用通配符给整批机器设公共参数
Host web* db* cache*
User deploy
ConnectTimeout 5
ServerAliveInterval 30
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 60s
ControlMaster + ControlPersist 是批量操作提速的关键:它让多条 SSH 连接复用同一条底层 TCP 连接,避免每次连接都重新握手。对 200 台机器、每条执行 3 个 SSH 命令的场景,这个优化能把总耗时压缩好几倍。
日常用的几个命令:
# 批量执行同一命令(串行)
for h in web01 web02 web03; do
echo "==== $h ===="
ssh -o BatchMode=yes "$h" 'uptime'
done
-o BatchMode=yes 表示"禁止交互式提问",密钥没配好就立刻失败而不是卡在密码提示上——这在脚本里非常重要,否则一条命令卡住会拖死整个任务。
三、并发执行:从串行到并行
串行执行 N 台,耗时约等于 N 倍单机耗时。网络好时单机 0.5 秒还无所谓,但如果命令本身要跑几秒,或者机器跨地域、延迟上百毫秒,串行就会慢到无法接受。
常见并发方案对比:
| 方案 | 并发度控制 | 输出收集 | 复杂度 | 适用 |
|---|---|---|---|---|
for 循环 |
无(串行) | 简单 | 最低 | 机器少 |
& + wait |
无(全并发,易冲垮) | 靠临时文件 | 中 | 少量、无脑并发 |
xargs -P N |
可控(N 个并行) | 天然汇总 | 低 | 推荐 |
GNU parallel |
可控、功能全 | 好 | 中 | 需额外安装 |
| Python + 线程池 | 可控 | 结构化 | 中 | 需复杂逻辑 |
| Ansible | 可控(forks) | 结构化 | 低(现成) | 大规模推荐 |
xargs -P 是纯 Shell 环境下最实用的并发工具,因为它系统自带、能控制并发数、输出天然汇总到 stdout。
# 用 xargs 并发对多台机器执行命令,最多 10 个并发
cat hosts.txt | xargs -I{} -P10 -n1 \
ssh -o BatchMode=yes -o ConnectTimeout=5 {} 'hostname; uptime'
# hosts.txt 内容示例:
# web01
# web02
# web03
参数含义:-I{} 把每行内容替换到 {} 位置;-P10 最多 10 个并发进程;-n1 每次传给命令 1 个参数。这三者配合,就是"每行一台机器、最多 10 台同时执行"。
& + wait 的写法更直白,但不控制并发,机器一多会把管理端和目标端都压垮:
# 全并发(不推荐用于大量机器)
for h in web01 web02 web03; do
ssh -o BatchMode=yes "$h" 'uptime' > "/tmp/out.$h" 2>&1 &
done
wait
cat /tmp/out.*
四、写一个批量执行脚本
下面这份脚本综合了免密、并发、超时、结果汇总,可直接用于十几到几十台机器的日常批量操作。
#!/usr/bin/env bash
# 批量执行脚本:sshbatch.sh
# 用法:bash sshbatch.sh <要执行的命令> [并发数]
set -uo pipefail
HOSTS_FILE="${1:?用法: $0 <命令> [并发数]}"
CMD="${2:?缺少要执行的命令}"
PARALLEL="${3:-10}" # 默认 10 并发(经验值)
TIMEOUT=10 # SSH 连接超时(秒)
OUTDIR=$(mktemp -d) # 临时放各机输出
trap 'rm -rf "$OUTDIR"' EXIT
run_one() {
local host="$1"
local out
if out=$(ssh -o BatchMode=yes -o ConnectTimeout="$TIMEOUT" \
-o StrictHostKeyChecking=accept-new \
"$host" "$CMD" 2>&1); then
printf '\n===== [OK] %s =====\n%s\n' "$host" "$out" > "$OUTDIR/$host"
else
printf '\n===== [FAIL] %s =====\n%s\n' "$host" "$out" > "$OUTDIR/$host"
fi
}
export -f run_one
export OUTDIR TIMEOUT CMD
# 并发执行
xargs -I{} -P"$PARALLEL" -a "$HOSTS_FILE" bash -c 'run_one "$@"' _ {}
# 汇总输出(按主机名排序,便于阅读)
echo "############### 执行结果汇总 ###############"
cat "$OUTDIR"/* | sort -t' ' -k2 2>/dev/null | grep -E '=====|.*' || true
echo "############### 结束 ###############"
脚本里的几个要点:BatchMode=yes 保证不卡在密码提示;ConnectTimeout 保证失联主机快速失败;StrictHostKeyChecking=accept-new 在首次连接时自动接受主机指纹(比 no 安全:它会记录,后续变更会拒绝);export -f 把函数导出到 xargs 启动的子 shell,这是用 xargs 调用自定义函数的标准写法。
如果机器达到几百台,别继续堆脚本了,直接用 Ansible。它天然支持并发(forks 参数)、幂等、结果结构化输出,还有现成的模块:
# Ansible 一行命令对一组主机执行命令
ansible web -i inventory.ini -m shell -a 'uptime' -f 20
# -f 20 表示 20 个并发
Ansible 用 inventory.ini 描述主机分组,用 SSH 免密连接,本质是把上面这套脚本的活儿做成了产品。中小团队从"Shell 脚本"升级到"Ansible"的收益,主要体现在可维护性和幂等性上,而不是并发本身。
五、安全配置与防护
批量管理意味着"一把钥匙开很多门",所以这把钥匙的安全格外重要。
第一,私钥要保护好。 私钥泄露等于所有机器失守。措施包括:私钥设 passphrase;私钥文件权限 600;不要把私钥提交到代码仓库;能用 ssh-agent 就用,避免明文到处放。
# 启动 ssh-agent 并加载私钥(登录后执行一次)
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
第二,限制密钥能做什么。 在目标机的 authorized_keys 里,可以给单条公钥加限制,比如禁止转发、禁止分配终端、限定来源 IP:
# ~/.ssh/authorized_keys 中某一行(前置选项)
from="192.168.1.0/24",no-agent-forwarding,no-port-forwarding,no-pty ssh-ed25519 AAAA... ops@example.com
第三,给管理账号做最小权限。 批量执行常用一个统一账号,这个账号如果权限过大(随时能 sudo 全部),风险很高。建议:日常批量操作只授予必要权限;需要提权的命令单独走受限 sudo(在 /etc/sudoers.d/ 里限定具体命令)。
第四,改 sshd 有把自己锁住的风险。 如果要在 /etc/ssh/sshd_config 里禁用密码登录(PasswordAuthentication no),务必先用另一条连接验证密钥能登上,再关闭密码,并且保留当前会话不要断开:
# 建议流程
# 1) 确认密钥能登录(新开一个终端测试,不要关掉当前会话)
ssh -o PubkeyAuthentication=yes -o PasswordAuthentication=no web01 'echo ok'
# 2) 备份配置
cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
# 3) 修改并校验
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sshd -t # 语法校验,必须通过
# 4) 重载(不是重启,重载失败旧配置仍生效)
systemctl reload sshd # Debian/Ubuntu 服务名可能是 ssh
回滚方式:如果改 sshd_config 后登不上,若还有别的会话或控制台可用,用 cp -a /etc/ssh/sshd_config.bak /etc/ssh/sshd_config && systemctl reload sshd 恢复;若已完全登不上,则通过云厂商控制台/救援模式进入修复。所以"保留一个还能登录的会话"是改 SSH 配置的铁律。
六、常见误区
误区一:以为 authorized_keys 权限问题会报错。 实际上它往往表现为"仍然要输密码",而不是明确报权限错。排查要用 ssh -v 看日志里是不是提示 Authentication refused: bad ownership or modes。
误区二:把并发数设得越大越好。 并发过高会同时打开大量 SSH 连接,可能触发目标机的 MaxStartups 限制,反而出现大批连接被拒。经验上 10-30 并发是比较稳妥的起点,随目标数量逐步调。
误区三:忽略输出顺序。 并发执行的输出是乱序的,且多机输出会交叉。所以脚本里要把每台输出先写到单独文件、最后再汇总,否则日志会乱得没法看。
误区四:known_hosts 冲突。 重装系统后主机指纹会变,SSH 会报 REMOTE HOST IDENTIFICATION HAS CHANGED 而拒绝连接。确认是正常重装后,用 ssh-keygen -R 主机名 清掉旧记录再连。注意:这条命令本身不是安全漏洞,但如果指纹变化是你没预期的,要警惕中间人攻击,先核实再清除。
常见问题
Q1:配好了密钥,为什么还提示输密码?
按顺序查:私钥文件是否存在且权限 600;公钥是否正确写入目标机 authorized_keys 且权限 600、~/.ssh 为 700;目标机家目录是否对 group/other 可写(可写会被拒绝);sshd_config 里 PubkeyAuthentication 是否为 yes。用 ssh -v 看握手细节最快。
Q2:xargs -P 和 & + wait 该用哪个?
用 xargs -P。它能控制并发数、自带超时处理、输出汇总方便;裸 & 会无限并发,机器一多容易冲垮连接数限制,且结果收集要自己写,容易出错。
Q3:为什么建议用 ControlMaster 复用连接?
SSH 连接建立(TCP 握手 + 密钥交换 + 认证)本身有开销。批量场景下每台机器可能要执行多条命令,若每条都新建连接会很慢。ControlMaster 复用同一条连接,第二次以后几乎瞬时。它能显著缩短批量任务总时长。
Q4:多台机器怎么统一管理主机清单?
简单场景维护一个 hosts.txt(每行一台)或 ~/.ssh/config 的别名;复杂场景用 Ansible 的 inventory 文件,支持分组、变量、动态清单。原则是"主机清单集中一处维护",不要散落在各个脚本里。
Q5:批量执行时怎么处理某台机器卡住?
两个措施:一是每条 SSH 都加 ConnectTimeout 和执行超时,让它快速失败而不是永远挂起;二是脚本对每台机的输出和退出码单独记录,最后汇总"哪些成功、哪些失败",失败的单独重试。绝不要让"一台卡住拖垮整批"。
总结
- 批量管理 = 免密登录 + 并发执行,前者靠 SSH 密钥认证,后者靠
xargs -P或 Ansible。 - 优先用 Ed25519 密钥;
~/.ssh700、authorized_keys600 是密钥认证生效的硬性条件,不满足会静默退回密码认证。 ~/.ssh/config配别名和ControlMaster复用连接,能大幅简化命令并提升批量执行速度。BatchMode=yes+ConnectTimeout是脚本友好性的关键,避免卡在密码提示或失联主机上;并发数经验上从 10-30 起步。- 机器多到几十上百台时应转向 Ansible;改 SSH 配置前务必保留可用会话并先验证密钥登录,
sshd -t校验后再 reload,出问题可快速回滚。
企业QQ咨询




