如何使用SSH批量管理多台服务器?密钥认证与并发执行方案

机器一多,"挨个登录"就成了效率黑洞。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 密钥;~/.ssh 700、authorized_keys 600 是密钥认证生效的硬性条件,不满足会静默退回密码认证。
  • ~/.ssh/config 配别名和 ControlMaster 复用连接,能大幅简化命令并提升批量执行速度。
  • BatchMode=yes + ConnectTimeout 是脚本友好性的关键,避免卡在密码提示或失联主机上;并发数经验上从 10-30 起步。
  • 机器多到几十上百台时应转向 Ansible;改 SSH 配置前务必保留可用会话并先验证密钥登录,sshd -t 校验后再 reload,出问题可快速回滚。