
大数据集群批量分发脚本从手敲 scp 到一套体面的 xsync先说一个我踩过的坑。某次业务上线前需要临时把一份配置文件同步到集群里所有节点。当时集群才六台机器我图省事一台一台 scp敲到第五台的时候第六台报了No route to host。排查了半天发现其中一台机器的端口被占用sshd 重启了但服务没拉起来。最后上线时间被耽误了二十分钟全组人等我一个人敲命令。自那之后我把“同步一份文件到 N 台机器”这件事彻底脚本化了。今天这篇文章就从“写一个真正能用的集群分发脚本”讲起把从设计思路、核心细节到踩坑实录的全过程都梳理一遍。不管你是刚接触大数据集群还是已经在维护几十台节点的集群这套脚本和背后的思路都能直接用。适合谁来读刚入门 Hadoop、Spark 等分布式环境需要批量部署配置的人和已经有一定集群规模但还在用“手动 scp 手动 ssh”来维护环境的运维开发都适合。我会把脚本的每一段都拆开讲明白附上完整的可直接复制的代码块并把为什么这么写的逻辑也讲透。1. 内容整体设计与思路拆解1.1 核心需求分发脚本到底要解决什么问题先给“集群分发脚本”下个定义它是一段运行在集群任一节点上的程序用户只需要指定“要分发的文件/目录路径”和“目标节点列表”它就能自动完成从当前节点到所有目标节点的传输并且在传输完成后可选项地执行远程命令比如刷新配置、重启服务。听起来很简单但实际写的时候三个核心需求必须想清楚连接与传输怎么建立连接用什么协议传输。这是最底层的能力。目标管理节点列表怎么维护。写死在脚本里一定不是长久之计但过度设计比如接数据库对大多数场景也是负担。错误处理与反馈有些节点传输失败了怎么办是全盘重来还是记录失败节点继续执行最终如何让用户清楚地知道“哪些成功了、哪些失败了”围绕这三个需求我建议把脚本设计成“一个入口脚本 一个远程回调脚本”的组合。入口脚本负责读参数、循环连节点、发起传输远程回调脚本通过 ssh 在目标节点上执行负责收尾动作。这种拆分的好处是后续想加功能比如校验、告警、并行分发只需要改对应脚本不会互相干扰。1.2 方案选型为什么不用 Ansible而自己写一个可能有人问现成的 Ansible、SaltStack 不就能做批量分发吗为什么还要自己写这里要先说个前提脚本化分发和配置管理工具并不冲突是在不同场景下的选择。轻量场景临时同步一个配置文件、分发一个 JDK 安装包这种一次性的操作拉起 Ansible 的成本太高了——它需要你维护 inventory、编写 playbook、处理 Python 依赖环境前戏比正事还长。依赖控制很多大数据组件的部署脚本本身就对“手动执行哪些命令”有强诉求自己写的分发脚本可以直接嵌入到部署流程里没有一个额外的抽象层。学习原理自己写一遍分发脚本你能彻底理解 ssh 免密、scp/rsync 传输、远程命令执行这些底层机制。以后再碰到 Ansible 报错你排查起来会快很多因为你清楚下面发生的是什么。当然如果你的集群规模到了一百台以上分发逻辑变得复杂按角色分组、增量更新、版本回滚、审计那我建议你还是用 Ansible 这类成熟工具。脚本方案适合集群规模在几台到几十台的中小规模场景这个定位要想清楚。1.3 整体流程一个分发动作的完整生命周期我用一张简单的流程描述把最终脚本的行为说清楚让你在阅读代码前先有一个整体认知用户执行xsync 文件名 [目标主机组]脚本解析参数校验文件/目录是否存在从主机列表配置中读取目标节点 IP逐个节点检查 ssh 连通性和免密状态用rsync传输文件到目标节点的同路径目录传输结束后通过 ssh 在目标节点执行回调脚本可选汇总所有节点的成败状态输出结果摘要流程里最关键的是第 5 步和第 6 步。第 5 步决定了分发效率第 6 步决定了分发完成后“节点上的状态”是否真的和预期一致。这两个环节我会在后面的章节里重点拆解。2. 核心细节解析与实操要点2.1 免密登录这是分发脚本能跑起来的前提如果每次 rsync 都要输密码那脚本就失去了意义。ssh 免密的原理不复杂把当前节点的公钥追加到目标节点的authorized_keys文件里之后 ssh/scp/rsync 连接时目标节点就能通过公钥认证直接放行。设置方法非常固定四步走# 第 1 步生成密钥对如果还没有 ssh-keygen -t rsa -b 4096 -P -f ~/.ssh/id_rsa # 第 2 步拷贝公钥到目标节点首次需要输密码 ssh-copy-id root192.168.1.101 ssh-copy-id root192.168.1.102 # 第 3 步验证免密 ssh root192.168.1.101 echo ok # 第 4 步给 ~/.ssh 目录设置权限 # 服务端chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys我在实际配置中强烈建议在每台节点上都执行一遍同样的操作让所有节点之间两两免密而不只是“主节点 → 其他节点”。原因后面会讲当你的入口脚本需要支持“任意节点触发分发”时这个细节会省掉你大量排错时间。有一个权限细节必须注意很多新手在目标节点上把authorized_keys的权限设成了 644然后用StrictModes yes的 sshd 就会拒绝读取表现为“明明密钥加进去了但免密就是不生效”。这个坑我踩过不止一次~/.ssh目录 700、authorized_keys文件 600缺一不可。2.2 rsync 与 scp 的选择增量同步才是分布式环境的正道既然要写分发脚本传输工具选型就是绕不开的问题。scp 和 rsync 的选择我直接说结论优先选 rsync除非目标环境里连 rsync 都没装。原因有三增量传输rsync 通过对比源文件和目标文件的元数据与校验和只传输变化的部分。同步一个 2GB 的安装包第一次全面传输后第二次你哪怕只改了里面一个字节rsync 也能快速完成而 scp 每次都是全量拷贝。压缩传输-z参数会在传输过程中做压缩对文本类配置文件的同步效果尤其明显能减少网络传输量。保留属性-a归档模式可以保留权限、属主、时间戳、软链接等属性对同步配置文件来说非常实用——不会因为传输导致权限变化引发服务启动问题。rsync 的传输方向我习惯写成“从当前节点推送到目标节点”的形态即源路径在当前节点、目标路径在远端节点。这种形态配合循环天然支持把主节点上的文件推到所有从节点。2.3 主机列表的管理配置文件与命令行参数双轨制节点列表是分发脚本的“地图”设计得好不好直接决定你用起来顺不顺手。我的方案是维护一个独立的hosts.conf配置文件 支持命令行临时指定目标群组。hosts.conf的格式如下# 按角色分组一行为一组IP 以空格分隔 [hadoop] 192.168.1.101 192.168.1.102 192.168.1.103 [hbase] 192.168.1.101 192.168.1.102 192.168.1.104 192.168.1.105分发时默认目标为hadoop组也可以临时指定其他组xsync /opt/soft/hbase-2.4.11.tar.gz hbase这种“分组”设计比单纯一个“所有IP列表”更贴近真实集群的角色划分而且不会因为某组节点的变更而修改脚本本身。2.4 超时、重试与并发一个可靠分发脚本的核心品质传输一个文件到几十台机器如果串行执行每台耗时 10 秒总共就是几百秒看起来还能接受但如果你传输的是一个大目录里面几百个小文件串行处理可能让你等上十几分钟甚至更久。所以需要在脚本里引入并发控制。这里要区别一下“并发”和“并行”bash 脚本里通过后台进程加上wait来实现的是并发——多个任务在同一个 CPU 核上轮流切换但对 IO 密集型的文件传输来说这种并发已经能带来非常明显的速度提升。一个实用的并发数量可以设置成 5~10 个不要贪多。如果一次性发起几十个并发 ssh 进程不仅对当前节点的 CPU 有压力对目标节点的sshd也会造成负载冲击反而导致部分连接超时。建议设置一个并发上限用jobs命令实时查看后台进程数超过阈值就等待。同时每个节点的 ssh 连接都需要设置超时时间。比如ssh -o ConnectTimeout5 -o StrictHostKeyCheckingno rootIP commandConnectTimeout5表示建立 TCP 连接的超时时间为 5 秒StrictHostKeyCheckingno避免首次连接时弹出交互确认。这两个参数能显著减少“某个不可达节点拖累整个分发任务”的几率。2.5 日志与状态反馈越详细越好但输出别太啰嗦我是从“脚本报错但不知道错在哪台机器”这个痛苦经历中理解日志的重要性的。最终我采用的标准是每种状态前缀区分[INFO][OK][FAIL]每条记录都带节点 IP 和时间戳所有原始输出追加到日志文件控制台只显示摘要比如[INFO] 2025-01-15 10:23:01 开始分发文件到 3 个节点 [OK] 192.168.1.101 传输成功耗时 3.2s [FAIL] 192.168.1.102 传输失败Connection timed out [OK] 192.168.1.103 传输成功耗时 2.8s [INFO] 完成成功 2 台失败 1 台失败信息要能直接告诉你“哪台、什么原因、耗时多少”而不是一个笼统的ERROR。这能让你在处理几十台节点时一眼定位问题。3. 实操过程与核心环节实现3.1 基础版一个存储路径的同步脚本在给出完整生产级脚本前先放一个最基础的版本。这个版本没有并发、没有回调但已经把“循环 分发 路径处理”的核心框架拉起来了。#!/bin/bash # # 基础版 xsync将文件/目录分发到 hadoop 组所有节点 # 用法./xsync_base.sh /path/to/file_or_dir # # 1. 判断参数 if [ $# -lt 1 ]; then echo 用法: $0 文件或目录路径 exit 1 fi # 2. 获取源文件的绝对路径 src_file$1 if [ ! -e $src_file ]; then echo 错误: 文件 $src_file 不存在 exit 1 fi # 取绝对路径 src_file$(cd $(dirname $src_file) pwd)/$(basename $src_file) # 获取父目录用于在目标节点上创建结构 src_parent$(dirname $src_file) src_name$(basename $src_file) # 3. 节点列表示例 nodes(192.168.1.101 192.168.1.102 192.168.1.103) # 4. 分发逻辑 for node in ${nodes[]}; do echo 正在同步到 $node # 在目标节点上确保父目录存在 ssh -o ConnectTimeout5 -o StrictHostKeyCheckingno root$node \ mkdir -p $src_parent # 用 rsync 增量传输 rsync -avz --delete -e ssh -o StrictHostKeyCheckingno \ $src_file root$node:$src_parent/ if [ $? -eq 0 ]; then echo $node 同步成功 else echo $node 同步失败 fi done注意这个版本里的几个关键点src_file$(cd $(dirname $src_file) pwd)/$(basename $src_file)这行把传入的相对路径统一转成绝对路径防止在不同目录下执行脚本时把文件分发到错误的位置。mkdir -p确保目标目录存在否则 rsync 会直接报错。--delete参数表示删除目标端多余的文件用于“让目标端和源端完全一致”的场景。注意如果你不是想要目标端严格镜像就别加这个参数否则会把目标节点上的其他文件删掉。这是个很有威力的参数但也是很多人踩坑的地方。3.2 进阶版支持分组、并发控制、失败汇总基础版写出来能用但真到生产环境还是不够。我在这里给出一个我实际在用的进阶版它的核心特点有三个支持按配置文件分组支持后台并发传输但设置并发上限汇总成功和失败的节点便于排查#!/bin/bash # # xsync.sh 进阶版集群分发脚本 # 用法: # ./xsync.sh /opt/soft/xxx.tar.gz # ./xsync.sh /opt/soft/xxx.tar.gz hbase # # ---------------- 配置区 ---------------- CONF_FILE/etc/xsync/hosts.conf DEFAULT_GROUPhadoop SSH_USERroot SSH_PORT22 CONCURRENCY5 TIMEOUT5 # ---------------------------------------- # 用法提示 usage() { echo 用法: $0 文件或目录 [目标分组] echo 示例: $0 /opt/soft/jdk-8u202-linux-x64.tar.gz echo $0 /etc/hbase/conf/hbase-site.xml hbase exit 1 } # 读取分组配置 load_group() { local group$1 grep -A100 ^\[$group\] $CONF_FILE | grep -v ^\[ | grep -v ^$ | grep -v ^# | tr \n } # 主逻辑 if [ $# -lt 1 ]; then usage fi src_file$1 group$2 if [ -z $group ]; then group$DEFAULT_GROUP fi if [ ! -e $src_file ]; then echo 错误: 文件 $src_file 不存在 exit 1 fi # 绝对路径处理 src_file$(cd $(dirname $src_file) pwd)/$(basename $src_file) src_parent$(dirname $src_file) src_name$(basename $src_file) # 获取节点列表 nodes$(load_group $group) if [ -z $nodes ]; then echo 错误: 分组 $group 没有配置任何节点 exit 1 fi echo echo [INFO] 开始分发: $src_file echo [INFO] 目标分组: $group echo [INFO] 并发上限: $CONCURRENCY echo # 状态记录 success_nodes() fail_nodes() # 分发函数后台并发执行 send_one() { local node$1 local start_time$(date %s) ssh -o ConnectTimeout$TIMEOUT -o StrictHostKeyCheckingno \ ${SSH_USER}$node mkdir -p $src_parent 2/dev/null rsync -avz --timeout$TIMEOUT -e \ ssh -o ConnectTimeout$TIMEOUT -o StrictHostKeyCheckingno \ $src_file ${SSH_USER}$node:$src_parent/ /tmp/rsync_${node}.log 21 local status$? local end_time$(date %s) local cost$((end_time - start_time)) if [ $status -eq 0 ]; then echo [OK] $node 传输成功耗时 ${cost}s success_nodes($node) else echo [FAIL] $node 传输失败耗时 ${cost}s fail_nodes($node) fi } # 并发控制循环 running0 for node in $nodes; do # 如果达到并发上限等待最先完成的进程 while [ $running -ge $CONCURRENCY ]; do wait -n 2/dev/null running$((running - 1)) done send_one $node running$((running 1)) done # 等待所有后台进程结束 wait # 打印汇总结果 echo echo [INFO] 分发完成成功 ${#success_nodes[]} 台失败 ${#fail_nodes[]} 台 if [ ${#fail_nodes[]} -gt 0 ]; then echo [WARN] 失败节点列表: ${fail_nodes[*]} exit 1 fi echo 这里我特别想聊聊wait -n这个用法。在 bash 4.3 之前wait只能等待所有后台进程结束或者等待指定 PID。wait -n可以等待“任意一个后台进程结束”这样就能在循环里动态释放并发名额否则你只能“先启动 5 个进程 → 全部跑完 → 再启动 5 个”串行化明显效率低很多。如果你的 bash 版本低于 4.3可以把wait -n替换成wait但并发效率会降低。关于并发数我再强调一次测试时从 3~5 开始。不要一开始就设置 30 并发否则你会发现大量节点同时报Connection refused或者Timeout不是网络问题是本地 ssh 进程瞬间把文件描述符和 CPU 打满了。3.3 进阶版加入校验与远程执行回调分发文件只是第一步分发完还要确认节点上的内容确实和源端一致。我经验中最稳的校验方式是传输后立即做 md5 对比不一致就标记失败。在send_one函数里追加一个校验步骤# 源文件 md5只对文件有效目录则需要统计 src_md5$(md5sum $src_file | awk {print $1}) # 目标节点计算的 md5 dst_md5$(ssh ${SSH_USER}$node md5sum $src_parent/$src_name | awk {print $1}) if [ $src_md5 $dst_md5 ]; then echo [OK] $node 校验通过 else echo [FAIL] $node 校验不一致 fi不过对目录来说md5sum 大目录的开销不小。实际场景里对配置文件这类小文件可以全量校验对大型安装包几百 MB 甚至几个 GB建议改用rsync -c或直接依赖 rsync 自身的传输校验机制最后比对文件大小和数量即可。回调脚本的应用场景更常见分发完配置文件之后需要重启服务才能生效。你可以直接在send_one里追加# 可选远程执行重启命令 ssh ${SSH_USER}$node systemctl restart xxx.service但更规范的做法是把“重启”动作放到一个独立的remote_callback.sh脚本里入口脚本在传输完成后调用它。这样入口脚本保持纯粹只负责分发回调脚本可以针对不同组件写不同的逻辑ssh ${SSH_USER}$node bash -s ./remote_callback.shbash -s配合标准输入执行远程脚本的好处是不需要先把回调脚本传到目标节点也避免脚本内容在命令行里被大量转义弄乱。这是我在处理“分发并生效”场景时非常喜欢的一种方式。3.4 目录结构的一致性说明上面脚本都在处理“单文件分发”但实际中更大比例的场景是同步整个目录比如把整个hadoop/conf目录分发到所有节点。这时候rsync的优势体现得最明显。# 同步整个目录注意源路径结尾有没有斜杠的区别 rsync -avz --delete /opt/soft/hbase/conf/ rootnode:/opt/soft/hbase/conf/源路径为conf/带尾部斜杠表示同步“目录里的内容”不会在目标端创建一个额外的 conf 层。源路径为conf不带尾部斜杠表示同步“目录本身”目标端会得到/opt/soft/hbase/conf/conf结构。这是 rsync 最容易踩的坑没有之一。你写脚本时如果没注意尾部斜杠分发出来的目录结构就会多一层或少一层轻则路径不对重则配置目录互相覆盖导致服务起不来。我的经验是在脚本中显式判断并统一处理尾部斜杠避免依赖用户输入的精确性。4. 常见问题与排查技巧实录4.1 免密失效但密钥明明放好了这是群里问得最多的一个问题。现象是ssh-copy-id成功执行第一次免密也正常但过了一段时间或者换了用户后免密突然不生效了。排查步骤我按经验整理如下先看服务端日志tail -f /var/log/secureCentOS/RHEL或/var/log/auth.logUbuntu/Debian观察登录时报的什么错。检查目标节点~/.ssh和authorized_keys的属主是否正确。如果是用 root 执行ssh-copy-id但之后用普通用户登录那肯定不匹配。确认当前节点的~/.ssh/id_rsa.pub是否确实在目标节点的authorized_keys中。因为多次执行ssh-copy-id会追加不会去重但如果你手动编辑过这个文件很可能覆盖了原有内容。查看 sshd 配置/etc/ssh/sshd_config中是否启用了PubkeyAuthentication yes。如果被设成no免密肯定失效。我在生产环境维护脚本时把这四步写成了一个一键诊断脚本ssh_check.sh在免密失效时可以快速定位原因而不用每次手动敲四五条命令。4.2 传输时间长串行 vs 并发如果你用了并发模式依然慢先检查是不是执行了没必要的全量传输。比如 rsync 默认会在每台机器上对比文件状态但如果目标端的文件时间戳和大小和源端一致rsync 就会跳过传输。问题是如果你在目标端执行过某些“修改文件访问时间”的操作比如ls -l、catrsync 的元数据判断可能被干扰。这种情况下可以明确使用-c参数让 rsync 基于校验和判断是否跳过rsync -avzc ...但要注意-c会扫描所有文件计算校验和对超大目录反而更慢。所以我的取舍是默认用-avz只有在怀疑“文件内容变了但大小和时间戳没变”时才加-c。真正导致传输慢的常见原因反而是这三点源节点带宽瓶颈所有分发都从同一台主节点拉取主节点的上行带宽就是天花板。sshd 的并发连接限制同一时刻大量并发连接涌入目标节点需要排队认证。磁盘 IO 抖动目标节点正在跑业务磁盘 IO 繁忙时接收文件的写入速度会明显变慢。分布式传输的另一个思路是“级联分发”第一个节点从主节点拉取文件第二个节点从第一个节点拉取依此类推。这种方式能把负载分散开适合超大文件分发到上百台节点的场景。4.3 脚本里有特殊字符引号嵌套与变量展开陷阱写分发脚本时最痛苦的莫过于在同一行里处理多层引号。比如ssh rootnode grep key /opt/xxx/config | awk -F {print \$2}这里的\$2是为了防止本地 shell 先展开把$2当成脚本的第二个参数。一旦你不小心少写了一个转义符你传过去的命令就会被解析成完全不同的意思而且大概率不会报错只是结果错误——这种错误极其隐蔽。我的一个实用经验是复杂命令不在命令行里拼而是用 heredoc 配合bash -s。举个例子ssh rootnode bash -s EOF grep key /opt/xxx/config | awk -F {print $2} EOF注意 heredoc 的分隔符EOF前后必须加单引号否则本地的$2还是会被展开。这个写法把本地和远端的变量空间隔离开从源头上解决了引号嵌套问题。4.4 文件同步过去后服务还是用旧的这是最诡异的问题之一rsync 明明提示传输成功文件也同步过去了但服务运行行为还是旧配置。排查之后发现大概率不是 rsync 的问题而是目标节点上的服务进程早就把旧配置加载进内存了你改了文件不重启服务当然不会生效。所以分发脚本在真实运维流程中通常都要配套“分发后重启”的动作。我的一般做法是分发和重启分成两个阶段操作。先批量分发、校验确认所有节点的文件都无误后再统一重启服务。如果同步重启很可能出现“前半部分节点已经用新配置启动了后半部分节点还没来得及更新”的中间状态这在分布式系统里意味着短暂的不一致严重时可能引发异常。回滚也是一个不能忽略的设计点在分发前把旧版本文件在目标节点上备份为xxx.conf.bak。这样一旦新配置有问题可以用同一个分发脚本快速回滚而不用再到每台机器上手动恢复。4.5 Windows 下写的脚本在 Linux 上报错如果你习惯在 Windows 上编辑脚本文件再传到 Linux 上执行八成会遇到这个经典报错-bash: ./xsync.sh: /bin/bash^M: bad interpreter: No such file or directory原因就是 Windows 的换行符是\r\nLinux 只认\n。脚本的 shebang 行被解析成/bin/bash^M自然找不到解释器。解决方案一行命令搞定sed -i s/\r$// xsync.sh或者用dos2unix xsync.sh没有这个命令就先 yum install dos2unix / apt install dos2unix。我现在的习惯是所有脚本都在 Linux 上直接用 vim 编写彻底规避这个坑。如果跨平台协作那就在 Git 提交时统一配置core.autocrlfinput让检出到 Linux 的文件保持 LF 结尾。5. 用分发脚本还能顺带做的事从工具到小体系的扩展分发脚本写完之后你会发现它的能力边界远不止“分发文件”只要把“远程执行回调脚本”这个能力用好它就能延伸出很多功能。第一个扩展是把分发脚本变成环境一致性检查工具。你可以在回调脚本里检查目标节点上的 JDK 版本、磁盘空间、关键进程状态然后把结果回传到主节点统一汇总。这样你就能在几分钟内确认整个集群的状态是否符合预期而不是逐台登录去查。第二个扩展是配置基线部署。把所有节点的/etc/hosts、/etc/profile、/etc/security/limits.conf这类基础配置文件整理成模板用分发脚本批量下发。配合版本管理工具每次变更都有迹可循出问题了可以直接回滚到上一个版本。这个做法可以帮你把“配置漂移”这个分布式集群的经典难题控制在一个比较小的范围内。第三个比较实用的扩展是对目录同步的定时化。把分发脚本挂到 crontab 里做定时配置分发# 每 5 分钟检查一次主节点上的配置目录发生变化就分发到集群 */5 * * * * /usr/local/bin/xsync.sh /opt/config/ hbase /var/log/xsync.log 21这时候你就能体会到前面说的“rsync 增量传输”有多重要了——即使全量扫描所有文件rsync 也只会传输真正变化的部分网络开销保持在很低的水平。我个人在实际维护中还有一个特别受用的习惯每次执行分发前先用rsync --dry-run预演一遍。--dry-run参数只输出“会传输哪些文件”不实际执行相当于演习。这能让你在真正分发之前先确认源端和目标端的差异避免因为手误导致批量覆盖。rsync -avz --dry-run --delete /opt/config/ rootnode:/opt/config/如果预演结果里出现的文件列表和你预期不一致那八成是路径写错了或者尾部斜杠问题这时候停下检查比传输后再回滚要轻松得多。这些扩展方向不需要新工具、新框架就靠一套脚本加上几个回调脚本组合出来对于中小规模的集群来说已经足够日常工作使用了。最后再分享一个小技巧把xsync.sh放到/usr/local/bin/目录下并赋予可执行权限这样你在集群里任何一个目录都能直接执行xsync 文件名不用每次带全路径。配合前面说的所有节点两两免密这套分发方案算是彻底融入日常操作了用起来会非常顺手。