上个月接手了一个特别磨人的运维任务:公司Zabbix监控做机房迁移,新Server部署完成,剩下的活就是让200多台业务机的zabbix agent全部认到新地址。老环境是不同时期、不同同事装的,zabbix_agentd.conf里的Server、ServerActive、Hostname五花八门,不少机器还留着好几年前的原始配置,连最基本的参数都没规范过。200多台机器,如果一台台登录手工改,每台至少五分钟,一个人干就是十六个小时往上,人一疲劳还容易改错、漏改。我花了一个多小时写了这个批量替换zabbix agent配置文件的Shell脚本,最后全量下发只用了不到十分钟。这篇文章把脚本的设计思路、完整代码、执行节奏和上线后踩过的坑都整理出来,给同样要面对几十上百台agent配置变更的运维朋友做个参考。
1. 批量替换配置的典型场景:不是闲得慌才写脚本
先别急着看代码,搞清楚"什么时候需要批量替换agent配置文件"比脚本本身更重要。因为脚本写起来不难,难的是你想清楚到底要替什么、替完之后会不会把线上监控搞挂。
1.1 Zabbix Server迁移:两个参数决定死活
最典型的场景就是Zabbix Server迁移。老Server扛不住监控规模,或者机房整体搬迁,新Server的IP和旧的根本不是一回事。Agent侧需要改动的核心参数就两个:Server(被动监控模式下,允许哪个Server来轮询)和ServerActive(主动模式下,Agent向哪个Server上报数据)。
这两个参数不指过去,新Server上永远看不到这些主机。实战中要特别注意:老环境的配置文件里,这两个参数的值很可能是不同的地址。比如Server填的是老Server内网IP,ServerActive填的是另一个跳板地址。替换前先把新旧地址对应关系列清楚,别想当然地以为两个都填新IP就完事。有些混合模式下,Server保留了但ServerActive变了,这种半替换状态最容易出问题,因为从Server前端看主机是"在线"的,但历史数据断断续续,排查起来很费劲。
1.2 被动模式转主动模式:配置逻辑完全不同
第二种高频场景是监控模式的整体切换。老架构习惯用被动模式,每台agent监听10050端口等Server来拉数据,机器少还行,机器一多Server轮询压力很大。新规划往往会切到主动模式,让agent周期性连接Server的10051端口主动推送数据。
这两种模式的配置差异非常大,绝不是改一个参数的事。我的建议是把下面这个对照表打印出来对着改:
| 配置参数 | 被动模式 | 主动模式 |
|---|---|---|
| Server | 必须填Server IP,决定谁可以拉取 | 可填可不填,主要控制被动数据来源 |
| ServerActive | 一般不用 | 必须填Server IP和端口,默认10051 |
| Hostname | 影响不大 | 必须与前端创建的主机名完全一致 |
| ListenPort | 10050,需对外开放 | 10050可以不开,按需保留 |
主动模式下最坑的是Hostname。Agent上报数据时是用Hostname来标识自己的,这个值必须和Zabbix Server前端添加主机时填的名字一模一样。批量替换配置时,如果新模板里写死了一个Hostname,200台agent全都会以同一个名字注册,结果就是前端只看到一台主机在跳数据。这个坑我后面会专门讲,这里先记住:新模板里要么用HostnameItem=system.hostname自动取本机名,要么在脚本里按主机清单注入各自的Hostname。
1.3 配置统一标准化:顺手清理历史债务
第三种情况最不起眼但最刚需。公司历史机器上的agent配置是不同时期、不同同事装的,有的加了Include段落,有的自定义了UserParameter,有的改过日志级别,还有的压根就是装完没动过的原始配置。你要做的是把所有机器统一成一份标准配置,顺带把历史遗留问题清掉。
比如我这次就要求所有机器统一开启主动模式、统一配置Include自定义参数目录、统一日志轮转、统一Hostname命名规范。这种标准化动作靠"登录每台机器手工编辑"根本不现实,必须直接推送一份完整的新配置文件,把旧的原始配置整个覆盖掉。这也是"批量替换"比"批量修改"更靠谱的地方:整体覆盖的结果是可预期的,不会出现这台机器保留了某一行、那台机器没保留的中间态。
2. 动手前先想清楚:脚本的三个设计原则
写脚本之前,我特意把现有机器上的zabbix_agentd.conf拉回来看了几份。你会发现这些文件有的很干净只有默认配置,有的被前人加了一大堆自定义参数,还有的带着大段注释。针对这种情况,脚本设计有几点原则必须先定下来。
2.1 替换策略:整体覆盖,而不是逐行比对
我见过有人写脚本去sed替换某个参数,比如把Server=192.168.1.10替换成Server=192.168.2.10。对于配置相对统一的机器这种写法没问题,但一旦某台机器漏配了这行、或者注释格式不一样,sed就静默失败了,事后你根本不知道哪台没改。
所以我的选择是:直接整体覆盖。新配置是一份经过验证的标准模板,每台机器不管原来是什么样,替换后状态完全一致。原来的文件整体备份好,真要回滚也是整体回滚,逻辑清晰。整体覆盖的代价是你必须把新模板写得足够完整,不能漏掉任何一台机器原来依赖的参数。
2.2 安全三原则:可回滚、可灰度、可追溯
批量替换配置属于高影响面操作,一台机器配错了顶多丢一台监控,200台机器一起出错就是整个监控平台瘫痪。所以脚本设计的第一原则不是"快",而是"安全"。
可回滚:替换前把每台机器的原始配置文件完整备份,备份名带时间戳,出问题一条命令就能恢复。可灰度:脚本必须支持只执行清单里的某几台,先在两三台试点机器上跑通,再放开全量。可追溯:每一步执行结果都要落日志,哪台成功、哪台失败、失败在哪个环节,出问题能直接翻日志查。
这三个原则看起来是废话,但真到写脚本的时候很容易为了省事丢掉。我见过太多"一把梭"的脚本,备份没做、日志没留、一跑就是全量,出了问题连从哪台开始挂的都不知道。
2.3 工具选型:为什么先用Shell而不是Ansible
有人问,批量分发配置这种活,Ansible不是更专业吗?确实,如果生产环境已经有一套成熟的自动化运维平台,用Ansible的copy模块加service模块会更顺手。但实际场景是,很多公司并没有部署统一的自动化平台,或者这次只是一个一次性的临时操作,为它去引入一套工具链、配好inventory、写好playbook,成本反而比直接写脚本高。
Shell加SSH加SCP是最小可行方案:控制端只要装了bash和openssh-clients就能跑,目标机只要有sshd和zabbix agent就能收。零额外依赖,脚本本身也能留在服务器上当个小工具反复用。如果你后续有频繁的批量变更需求,再考虑升级到Ansible不迟。工具够用就好,别为了用工具而用工具。
2.4 动手前要确认的前置条件
脚本依赖控制端到目标机的SSH免密登录。如果你现在还是密码登录,先去把公钥下发一遍,否则脚本会在每台机器上停下来等你输密码,批量的意义就没了。
主机清单文件格式我用的最简单的一种:每行一个IP或者主机名,#开头的行作为注释跳过,空白行也跳过。不建议在清单里带密码或者端口,那会让脚本变复杂且不安全。端口统一走22,如果你有特殊端口需求,可以把清单改成host:port格式再在脚本里split处理,但大多数内网环境用不上这个复杂度。
3. 脚本完整实现与关键环节拆解
直接上代码。下面的脚本我在实际环境中跑过,CentOS 7和Ubuntu 20.04都验证过,用的是systemd管理zabbix-agent服务。如果你的系统是SysVinit,把systemctl restart zabbix-agent改成service zabbix-agent restart就行。
#!/usr/bin/env bash # ============================================================= # 批量替换 Zabbix Agent 配置文件脚本 # 适用: CentOS 7+ / Ubuntu 18.04+ 等使用 systemd 的系统 # 前置: # 1. 控制端与目标机已配置 root SSH 免密 # 2. 新配置模板放在脚本同目录: zabbix_agentd.conf # 3. 主机清单文件: 每行一个IP/主机名,#开头为注释 # 用法: # ./replace_zabbix_agent_conf.sh hostlist.txt # ============================================================= set -uo pipefail HOST_LIST="${1:?用法: $0 hostlist.txt}" NEW_CONF="./zabbix_agentd.conf" REMOTE_CONF="/etc/zabbix/zabbix_agentd.conf" SSH_USER="root" SSH_PORT="22" TS="$(date +%Y%m%d_%H%M%S)" LOG_FILE="./agent_replace_${TS}.log" OK_COUNT=0 FAIL_COUNT=0 log() { echo "[$(date '+%F %T')] $*" | tee -a "${LOG_FILE}" } replace_one() { local host="$1" local active_state="" log "===== 开始处理: ${host} =====" # 连通性检查,连不上的直接跳过,不影响其他机器 if ! ssh -p "${SSH_PORT}" -o ConnectTimeout=5 -o StrictHostKeyChecking=no \ "${SSH_USER}@${host}" "echo ok" >/dev/null 2>&1; then log "[跳过] ${host} SSH无法连接" FAIL_COUNT=$((FAIL_COUNT + 1)) return 1 fi # 备份原配置,备份名带时间戳,可随时回滚 if ! ssh -p "${SSH_PORT}" "${SSH_USER}@${host}" \ "cp -a ${REMOTE_CONF} ${REMOTE_CONF}.bak_${TS}"; then log "[失败] ${host} 备份原配置失败" FAIL_COUNT=$((FAIL_COUNT + 1)) return 1 fi # 上传新配置 if ! scp -P "${SSH_PORT}" "${NEW_CONF}" \ "${SSH_USER}@${host}:${REMOTE_CONF}"; then log "[失败] ${host} 新配置上传失败" FAIL_COUNT=$((FAIL_COUNT + 1)) return 1 fi # 用agent自带参数测试配置能否被正常加载 # 这一步通过才重启,避免把线上服务搞挂 if ! ssh -p "${SSH_PORT}" "${SSH_USER}@${host}" \ "zabbix_agentd -c ${REMOTE_CONF} -t agent.ping" >/dev/null 2>&1; then log "[失败] ${host} 配置加载测试未通过" FAIL_COUNT=$((FAIL_COUNT + 1)) return 1 fi # 重启agent if ! ssh -p "${SSH_PORT}" "${SSH_USER}@${host}" \ "systemctl restart zabbix-agent"; then log "[失败] ${host} 服务重启失败" FAIL_COUNT=$((FAIL_COUNT + 1)) return 1 fi # 确认服务是active active_state=$(ssh -p "${SSH_PORT}" "${SSH_USER}@${host}" \ "systemctl is-active zabbix-agent" 2>/dev/null) if [ "${active_state}" = "active" ]; then log "[成功] ${host} 替换完成,agent状态active" OK_COUNT=$((OK_COUNT + 1)) else log "[异常] ${host} agent状态: ${active_state},需要人工排查" FAIL_COUNT=$((FAIL_COUNT + 1)) fi } # ---------- 主流程 ---------- if [ ! -f "${NEW_CONF}" ]; then log "本地缺少新配置模板: ${NEW_CONF}" exit 1 fi if [ ! -f "${HOST_LIST}" ]; then log "主机清单不存在: ${HOST_LIST}" exit 1 fi log "开始批量替换Zabbix Agent配置,清单: ${HOST_LIST}" while read -r host; do [ -z "${host}" ] && continue [[ "${host}" == \#* ]] && continue replace_one "${host}" done < "${HOST_LIST}" log "全部执行完毕: 成功 ${OK_COUNT} 台, 失败 ${FAIL_COUNT} 台"3.1 主流程设计:每一个环节失败都不硬闯
这个脚本的主流程是串行的,每一台机器内部有6个环节:连通性检查、备份、上传、配置加载测试、重启、状态确认。任何一个环节失败,这台机器直接标记失败然后跳到下一台,绝不硬闯。
这个设计你可能觉得啰嗦,但它是整个脚本最值钱的部分。我第一次写类似的批量工具时,为了省事把备份和测试都省了,直接上传加重启,结果有一台机器的配置模板路径写错了,scp把文件传到了错误位置,agent起不来,而那台机器还是线上核心数据库。从那以后,我的批量脚本里每一步都带判断,宁可多写几行,也不能让一台机器带着错误状态留在生产环境里。
3.2 备份环节为什么用cp -a而不是mv
备份命令是cp -a ${REMOTE_CONF} ${REMOTE_CONF}.bak_${TS},用-a参数保留文件的属主、属组、权限和时间戳属性。这样备份出来的文件和你之后的回滚操作是完全等价的,直接cp回去就能用。
有同事问过我为什么不用mv把原文件改名,其实也行,但逻辑上有区别:mv之后原路径就空了,如果scp上传失败,这台机器就处于"没有配置文件"的裸奔状态;而cp保留原文件,即使上传失败,原配置还在,agent还在按旧配置运行,故障面完全可控。备份文件名带时间戳也是必须的,我的TS变量在整个脚本执行期间保持不变,一台机器重复跑多次也不会互相覆盖备份。
3.3 用zabbix_agentd -t做配置加载验证
这个环节是我强烈建议你加上的。替换完配置之后不要急着restart,先执行一遍zabbix_agentd -c ${REMOTE_CONF} -t agent.ping。这条命令会启动一个agent进程去加载指定配置文件,然后测试内置键agent.ping能不能正常返回。如果配置里有语法错误、参数拼写错误、Include指向了不存在的目录,这一步会直接报错退出。
这一步的价值在于:你可以在不打断线上agent进程的情况下,先验证新配置"能不能被加载"。如果跳过它直接restart,一旦新配置有问题,systemd会把服务拉起来又立刻杀掉,反复几次之后干脆报failed,线上的监控数据就断档了。用-t先试一遍,配置有问题就当场拦截住,根本不会走到重启那一步。这个技巧是我踩过一次坑之后才加上去的,代价很小,收益很大。
3.4 日志与统计:批量的底气来自可追溯
脚本里所有关键动作都通过log()函数同时输出到终端和日志文件,日志文件名带时间戳,比如agent_replace_20250618_143000.log。每次执行完之后,我记得先不急着清屏,而是把日志里的[失败]和[异常]行筛出来,单独处理这几台的问题机器。
脚本末尾会打印成功和失败的累计数量。这个汇总信息看起来简单,但在全量跑完以后特别有用:你不需要再对着几百台机器去数,一眼就知道还有几台没搞定。加上每台机器内部的6个环节都有明确的成功或失败标记,哪台卡在哪一步,翻日志就能定位。
4. 从1台到200台的落地节奏控制
脚本写出来只是第一步,怎么执行才能既快又不翻车,才是真正考验经验的地方。我的习惯是"先单机、再灰度、后全量",三步走,每一步都有明确的验证口径。
4.1 单机试点:先拿两三台机器跑通
全量下发之前,先建一个只有两三台测试机的清单,把脚本跑一遍。这时候重点看的不是脚本本身,而是确认新配置模板在这几台机器上真的能正常工作。具体检查三件事:第一,脚本日志里这几台机器是否全部标记为成功;第二,去Zabbix Server前端页面看这几台主机是否正常上线,数据是否在更新;第三,随机挑一台机器执行zabbix_agentd -p看agent端输出的检查项列表是否正常。
单机试点这一步千万不能省。你写脚本时的假设(比如配置文件路径、服务名、agent二进制位置)和目标机器的实际情况可能有出入,试点阶段就是要把这些出入全部暴露出来。我的经验是,第一次试点跑完一定会发现至少一个和预期不符的地方,这很正常,调整完再试。
4.2 分批灰度与失败自动跳过
试点通过后,把清单扩大到10到20台再跑一轮。这一轮重点观察脚本在"批量"状态下的表现:多台机器依次执行时会不会有ssh连接超时、scp传输会不会卡住、日志输出是否清晰。确认无误之后,再从20台到50台、100台,最后全量。
灰度过程中,脚本"失败自动跳过"的机制特别重要。假设200台机器里有3台ssh连不上,脚本不会卡在那里,而是把这3台标记为失败继续往下走。跑完之后我只需要针对这3台单独排查,其他197台已经全部切换完毕。这种"小故障不影响大部队"的容错设计,是批量化操作里最该有的品质。
4.3 机器特别多时的并发思路
脚本默认是逐台串行执行,200台机器每台耗时大约3到5秒(主要是ssh和scp的开销),全量跑完大概15分钟左右,其实可以接受。但如果你要处理的是上千台机器,串行就会有点慢。这时可以用xargs做简单并发,比如:
cat hostlist.txt | grep -v '^#' | grep -v '^$' \ | xargs -P 10 -I {} ./replace_zabbix_agent_conf.sh --single {}注意xargs -P的并发数不要开太大。每台机器会同时建立多个ssh连接,并发数超过20时,目标网络设备或Zabbix Server的接入层可能会扛不住。我实测下来内网环境并发10左右比较稳。另外,并发模式下日志会交错输出,建议每条日志都带上主机名(脚本里已经做了),方便事后按主机名grep。如果你不确定并发会不会引发问题,宁可串行多跑一会儿,也不要图快把执行过程搞乱。
5. 替换后agent不上线的排错清单
再完美的脚本也会遇到现场环境的意外。这一节把我在替换过程中遇到过的、以及身边同事踩过的一些典型问题进行汇总,按照"现象、原因、排查"三个维度整理。你可以把这个清单当成排错手册,遇到问题按顺序查。
5.1 服务已经是active,前端却看不到主机
这是最常见的"看起来成功,实际没成功"的情况。脚本标记成功、systemctl is-active也返回active,但Zabbix Server前端主机列表里还是红色ZBX,或者干脆看不到这台主机。我遇到的主要原因有三个:
第一,Server参数填的不是新Server的IP,agent虽然起来了,但在等待一个永远不来的连接。第二,防火墙没放行10050端口,尤其是被动模式下,Server无法连接agent。第三,主动模式下Hostname和前端主机名不一致,agent的数据被Server判定为"未知主机"直接丢弃。
排查顺序建议这样:先在Server上用zabbix_get -s <agent_ip> -k agent.ping测试,看能不能取到值;取不到就检查网络和防火墙;能取到值但前端不显示,就重点查Hostname和主机名是否一致。这个顺序能帮你快速区分是网络层问题还是数据层问题。
5.2 换行符、文件权限和SELinux的坑
这三个坑都属于"文件本身没写错,但系统不认"的隐蔽问题。
换行符是坑中之王。如果你在Windows上用记事本或某些编辑器改过配置文件,再传到Linux上,每行末尾会多一个\r。zabbix agent解析配置时会把这个不可见字符当成参数的一部分,结果就是各种莫名其妙的报错,比如Server=192.168.1.10\r解析出来的IP根本连不上。排查方法很简单,在目标机上执行file /etc/zabbix/zabbix_agentd.conf,输出里如果提到CRLF就是中招了,用sed -i 's/\r$//'清一遍再重新加载。
文件权限和属主的问题也值得留个心眼。scp上传的配置文件默认属主是root,如果agent进程以zabbix用户运行,而配置文件里Include了某些属主是别的用户的目录,agent可能读不到那些子配置。标准情况下/etc/zabbix/zabbix_agentd.conf属主root、权限644没有问题,但如果你Include的目录或文件权限不对,agent会静默跳过。把Include目录下的文件属主统一成zabbix用户最省事。
SELinux的问题在启用强制模式的环境里会出现:从/root或/tmp目录scp过去的文件,SELinux上下文可能不对,agent进程读文件时会被拒绝。处理方法是执行一次restorecon -Rv /etc/zabbix或者直接chcon system_u:object_r:etc_t:s0 /etc/zabbix/zabbix_agentd.conf。验证SELinux有没有拦截,看/var/log/audit/audit.log里有没有denied记录就行。
| 现象 | 可能原因 | 快速验证 |
|---|---|---|
| 配置报错且提示IP对不上 | 换行符CRLF | file命令看格式 |
| agent日志提示无法读取配置 | 权限或SELinux | ls -Z 查看上下文 |
| Include的监控项全部没有数据 | 子配置权限不对 | ls -l 检查644以上权限 |
5.3 用agent日志和zabbix_get快速定位
排错必备三件套:agent日志、zabbix_get、systemctl状态。agent日志默认在/var/log/zabbix/zabbix_agentd.log,替换完配置后如果服务起不来或数据不上报,第一件事就是tail -n 50看这个日志。agent的日志写得很直白,配置文件哪一行解析错误、连不上Server、Hostname匹配失败,都会有明确记录。
zabbix_get是Zabbix Server端自带的排查工具,用法是zabbix_get -s <agent_ip> -p 10050 -k agent.ping。它能模拟Server去主动拉取agent数据,返回1说明agent整体通;返回空或者超时,说明网络或Server参数有问题。这个工具能帮你把"问题在agent侧还是server侧"快速区分开。通过这两步,大部分问题都能在十分钟内定位,不用对着前端页面瞎猜。
6. 替换做完之后,我留下了三个习惯
批量替换配置这个动作本身是一次性的,但它带来的习惯是可以沉淀下来的。
6.1 配置模板和脚本一起纳入版本管理
以前我总觉得配置文件这种东西没必要进Git,直到有一次发现一台机器上的配置和其他的差了十几个参数,怎么进来的都说不清。现在我把zabbix_agentd.conf的标准模板、批量替换脚本、主机清单样例全部放进同一个Git仓库,每次变更都走一次提交记录。下次再有人问"这台机器的agent为什么和别人不一样",翻一下提交历史就能解释清楚。配置文件模板化的收益不在当下,而在半年后回头维护的时候。
6.2 这个脚本扩展一下,就是通用批量分发器
把脚本里的NEW_CONF="zabbix_agentd.conf"和REMOTE_CONF="/etc/zabbix/zabbix_agentd.conf"都改成参数传入,它就从一个专用脚本变成了一个通用的批量文件分发器。不只是zabbix配置,nginx.conf、logrotate配置、hosts文件,凡是需要"N台机器统一替换一个文件"的场景,把参数换掉就能用。我已经把改造后的版本留在服务器上,后来替换nginx配置也用的它。
6.3 顺手把agent在线巡检做成一个固定动作
替换完成只是开始,agent的日常在线巡检才是长期要做的。我的做法是写一个更简单的脚本,每天定时检查每台机器的systemctl is-active zabbix-agent,如果发现非active状态就告警,连同一个简单的端口探活一起做。这样即使之后配置再有变动、某台机器agent悄悄挂了,也能第一时间发现。
回头再看这次批量替换,真正花时间的不是写脚本本身,而是动手前想清楚替换策略、动手后验证执行结果。这个思路放在任何批量运维操作上都通用:先备份、再灰度、留日志、能回滚。有了这套脚本和习惯,下次再碰上类似的批量变更,你就不会再想一台台手动去改了。