ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Zabbix自定义脚本监控实战:服务健康与日志分析

Zabbix自定义脚本监控实战:服务健康与日志分析 1. 为什么Zabbix原生监控总差那么一口气——自定义脚本才是生产环境的“最后一公里”Zabbix不是装上就能用的开箱即用玩具它是一套精密运转的监控中枢而真正让它在真实业务场景里活起来的从来不是那些预置模板而是你亲手写的几行Shell脚本。我见过太多团队Zabbix服务器搭得锃亮Agent部署整齐划一但一到查“订单服务是否卡死”“支付回调日志有没有堆积”“数据库慢查询是否突破阈值”就只能干瞪眼——因为Zabbix自带的proc.num[]、log[]这些键值根本没法精准表达业务逻辑。比如你想知道“微信支付回调接口在过去5分钟内失败率是否超过3%”Zabbix原生不提供这种聚合计算能力再比如“Nginx access.log里每秒404错误超过20次”这个告警原生log[]只能匹配单行无法做时间窗口计数统计。这时候自定义脚本就是那个必须亲手拧上的螺丝。它不依赖Zabbix版本更新不等待官方模板适配更不把业务语义交给黑盒解析。你写什么它就监控什么你定义失败条件它就按这个条件触发告警。关键词里的“zabbix”“自定义监控”“脚本”“服务”“日志”说到底就是五个字让监控听懂业务。这不是高级玩法而是生产环境的生存底线——尤其当你负责的是电商大促、金融清算、医疗挂号这类毫秒级敏感系统时。本文不讲Zabbix安装部署那是运维入门也不堆砌API文档官网比你记得牢只聚焦一件事如何用最朴素的Shell脚本把Zabbix变成你业务逻辑的“翻译官”。所有代码、配置、踩坑细节全部来自我过去三年在三家不同规模公司落地的真实项目包括某千万级用户SaaS平台的订单链路监控、某省级政务云的日志审计合规方案以及一个微服务集群的故障自愈脚本体系。2. 自定义监控的底层逻辑Zabbix Agent如何执行你的脚本很多人以为“写个脚本放Agent机器上Zabbix Server就能调用”这就像以为把菜谱贴在厨房墙上灶台就会自动炒菜一样。Zabbix Agent执行自定义脚本本质是一次受控的本地进程调用其行为由三个核心组件严格约束Agent配置、脚本权限与路径、Zabbix Server的Item定义。理解这三者的协作关系是避免“脚本明明能手动运行Zabbix却报‘Permission denied’或‘No such file’”的根本前提。2.1 Zabbix Agent的UserParameter机制不是所有脚本都能被调用Zabbix Agent默认禁止执行任意外部命令这是安全设计不是Bug。它通过UserParameter指令显式声明“哪些脚本、以什么参数、允许谁调用”。这个配置项必须写在Agent的主配置文件通常是/etc/zabbix/zabbix_agentd.conf或独立的*.conf文件如/etc/zabbix/zabbix_agentd.d/userparameter_custom.conf中。格式为UserParameterkey,command其中key是Zabbix内部识别该监控项的唯一标识符如service.nginx.statuscommand是实际执行的Shell命令。关键点在于command必须是完整可执行的命令字符串不能包含管道符|、重定向等shell特性——因为Agent调用时使用的是exec()系统调用而非启动一个完整的bash shell。这意味着如果你写UserParametercheck.nginx.pid,ps aux | grep nginx | grep -v grep | wc -lZabbix Agent会直接报错因为它试图执行名为ps的程序参数却是aux | grep nginx | grep -v grep | wc -l而|对ps来说是非法参数。正确写法是封装成一个独立脚本并在UserParameter中调用该脚本UserParametercheck.nginx.pid,/usr/local/bin/check_nginx_pid.sh然后在/usr/local/bin/check_nginx_pid.sh中写#!/bin/bash ps aux | grep nginx | grep -v grep | wc -l | tr -d 提示脚本末尾的tr -d 至关重要。Zabbix对返回值的格式极其敏感任何多余的空格、换行都会导致Server端解析失败表现为Item状态为Not supported。我曾在一个金融客户现场花两小时排查最终发现是脚本echo输出多了一个空格。2.2 脚本的执行身份与权限Agent用户才是真正的“老板”Zabbix Agent默认以zabbix用户身份运行可通过ps aux | grep zabbix_agentd确认。这意味着脚本的所有者、所属组、执行权限都必须对zabbix用户生效。常见错误有三类脚本所有者是root权限755但zabbix用户无读取权chmod 755只保证同组和其他人可执行但若脚本不在zabbix用户所属组内依然失败。脚本路径包含软链接而zabbix用户对源路径无访问权例如脚本放在/opt/myapp/monitor/该目录是root:root所有zabbix用户无法进入。脚本内部调用其他命令如mysql、curl但zabbix用户PATH环境变量不包含其路径Agent启动时继承的是系统最小PATH通常只有/usr/bin:/bin/usr/local/bin或自定义路径下的命令会找不到。解决方案是统一且强制的将脚本及其依赖命令的绝对路径写死。例如不要用mysql -u user -p pass db -e SELECT 1而要用/usr/bin/mysql -u user -p pass db -e SELECT 1。同时用chown zabbix:zabbix /usr/local/bin/check_nginx_pid.sh确保所有权并用chmod 755开放执行权。验证方法很简单切换到zabbix用户手动执行sudo -u zabbix /usr/local/bin/check_nginx_pid.sh如果这一步成功Zabbix Agent调用必然成功反之Agent报错永远只是表象根源永远在用户权限和PATH上。2.3 Zabbix Server的Item定义Key必须与UserParameter完全一致Server端创建Item时Key字段必须逐字符匹配Agent配置中的UserParameter定义的key。大小写、点号.、下划线_一个都不能错。例如Agent配置是UserParameterservice.nginx.status,/usr/local/bin/check_nginx_status.sh那么Server端Item的Key就必须填service.nginx.status填成service_nginx_status或Service.Nginx.Status都会失败。更隐蔽的陷阱是Zabbix Web界面在输入Key时会自动过滤掉开头和结尾的空格但不会过滤中间的多个空格。如果你不小心在Key里敲了两个空格如service.nginx. statusZabbix会静默接受但Agent端根本收不到这个KeyItem状态永远是Not supported。排查时最有效的方法是开启Agent调试日志# 修改 /etc/zabbix/zabbix_agentd.conf LogLevel4 # 重启Agent systemctl restart zabbix-agent # 查看日志 tail -f /var/log/zabbix/zabbix_agentd.log当Server发起一次采集请求时日志里会清晰打印出收到的Key和匹配的UserParameter。如果看到no matching user parameter for key说明Key不匹配如果看到executing command则说明匹配成功问题出在脚本本身。3. 监控服务状态从“进程是否存在”到“业务是否健康”的跃迁监控一个服务绝不是简单地ps aux | grep xxx看进程在不在。进程活着服务可能早已僵死端口开着API可能返回500。真正的服务监控必须穿透操作系统层直达业务逻辑层。我以监控一个典型的Java微服务Spring Boot应用为例展示如何用脚本实现三层健康检查进程层 → 端口层 → 业务层。3.1 进程层PID文件与JVM进程的双重校验很多Java应用会生成PID文件如/var/run/myapp.pid但这不可靠——PID文件可能残留而进程早已退出。单纯ps又容易误判grep自身进程会被匹配。我的脚本check_java_process.sh采用双保险#!/bin/bash # 检查PID文件是否存在且内容为数字 PID_FILE/var/run/myapp.pid if [ -f $PID_FILE ]; then PID$(cat $PID_FILE | tr -d ) if [[ $PID ~ ^[0-9]$ ]]; then # 用kill -0 检查进程是否存在且属于当前用户无需权限 if kill -0 $PID 2/dev/null; then echo 1 exit 0 fi fi fi # 如果PID文件无效回退到ps查找 JAVA_PROC$(ps aux | grep java.*myapp.jar | grep -v grep | awk {print $2}) if [ -n $JAVA_PROC ]; then echo 1 else echo 0 fi关键点在于kill -0 $PID它不发送信号只检查进程是否存在且调用者有权限向其发送信号。这比ps更精准且避免了grep自匹配问题。返回1表示健康0表示异常Zabbix Item类型设为Numeric (unsigned)触发器阈值设为!1即可告警。3.2 端口层TCP连接与HTTP响应码的组合验证端口监听不等于服务可用。我见过Nginx监听80端口但后端Tomcat已挂所有请求返回502。脚本check_service_port.sh必须模拟真实客户端行为#!/bin/bash HOST127.0.0.1 PORT8080 TIMEOUT5 # 第一步TCP连接测试快速失败 if timeout $TIMEOUT bash -c echo /dev/tcp/$HOST/$PORT 2/dev/null; then # TCP通继续HTTP健康检查 HTTP_CODE$(curl -s -o /dev/null -w %{http_code} --connect-timeout $TIMEOUT --max-time $TIMEOUT http://$HOST:$PORT/actuator/health) case $HTTP_CODE in 200|201) echo 1 ;; *) echo 2 # HTTP不通但TCP通如应用崩溃返回500 ;; esac else echo 0 # TCP不通 fi这里返回三个状态0彻底不可达、1完全健康、2TCP通但HTTP异常。Zabbix Item类型设为Numeric (unsigned)然后在触发器里分别定义{myhost:check_service_port.sh.last()}0→ “服务完全不可达”{myhost:check_service_port.sh.last()}2→ “服务响应异常HTTP非2xx” 这样告警信息就具备了明确的排障指向性而不是笼统的“服务挂了”。3.3 业务层调用真实API并校验业务逻辑这才是监控的灵魂。例如一个订单服务健康检查接口/api/v1/health返回JSON{status:UP,details:{db:UP,redis:UP,mq:UP}}脚本check_order_business.sh必须解析JSON并检查关键依赖#!/bin/bash HEALTH_URLhttp://127.0.0.1:8080/api/v1/health TIMEOUT10 # 使用jq解析需提前安装yum install -y jq 或 apt-get install -y jq RESPONSE$(curl -s --connect-timeout $TIMEOUT --max-time $TIMEOUT $HEALTH_URL) if [ -z $RESPONSE ]; then echo 0 exit 0 fi # 检查整体status STATUS$(echo $RESPONSE | jq -r .status 2/dev/null) if [ $STATUS ! UP ]; then echo 0 exit 0 fi # 检查关键子系统 DB_STATUS$(echo $RESPONSE | jq -r .details.db 2/dev/null) REDIS_STATUS$(echo $RESPONSE | jq -r .details.redis 2/dev/null) if [ $DB_STATUS UP ] [ $REDIS_STATUS UP ]; then echo 1 else echo 2 # 依赖服务异常 fi注意jq命令必须在Agent机器上安装且zabbix用户PATH要包含其路径通常是/usr/bin/jq。如果环境不允许安装jq可用sed和grep组合解析但健壮性大幅下降。我强烈建议在所有Zabbix Agent节点统一安装jq这是现代JSON处理的工业标准。4. 日志监控从“关键字扫描”到“实时流式分析”的实战演进日志监控是Zabbix自定义脚本里最容易踩坑的领域。Zabbix原生log[]键值只支持静态文件的单行匹配无法应对“五分钟内错误日志超过100条”或“特定错误模式连续出现”这类动态需求。而用脚本轮询日志又极易因性能、锁、日志滚动等问题失控。我的方案是分层设计轻量级关键字扫描 → 中量级时间窗口计数 → 重量级流式分析根据日志量级和告警精度要求选择。4.1 轻量级基于tail -n的实时关键字检测适用于低频错误对于error、exception这类低频关键字用tail -n 100读取最新100行配合grep -c计数是最简单可靠的方案。脚本check_log_keyword.sh#!/bin/bash LOG_FILE/var/log/myapp/app.log KEYWORDERROR WINDOW_LINES100 # 处理日志滚动先获取当前inode再tail避免因logrotate导致重复读取 CURRENT_INODE$(stat -c %i $LOG_FILE 2/dev/null) if [ -z $CURRENT_INODE ]; then echo 0 exit 0 fi # 用tail -n读取最新行grep计数 COUNT$(tail -n $WINDOW_LINES $LOG_FILE 2/dev/null | grep -c $KEYWORD 2/dev/null) if [ -z $COUNT ]; then COUNT0 fi # 返回错误数量Zabbix触发器设为 0 即告警 echo $COUNT关键技巧是stat -c %i获取inode。当日志被logrotate重命名时旧文件inode不变新文件是全新inode。此脚本只对当前inode文件操作避免了tail -F在日志滚动时丢失数据或重复读取的问题。实测在日均1GB日志的系统上此脚本CPU占用0.1%完全无压力。4.2 中量级基于awk的时间窗口滑动计数适用于中高频告警当需要“每5分钟内ERROR超过50次”时tail -n就不够了。必须引入时间戳解析。脚本check_log_rate.sh使用awk实现高效滑动窗口#!/bin/bash LOG_FILE/var/log/myapp/app.log KEYWORDERROR WINDOW_MINUTES5 THRESHOLD50 # 计算5分钟前的时间戳格式YYYY-MM-DD HH:MM:SS FIVE_MINUTES_AGO$(date -d 5 minutes ago %Y-%m-%d %H:%M:%S) # awk脚本逐行读取匹配时间戳在窗口内且含关键字的行计数 COUNT$(awk -v start$FIVE_MINUTES_AGO -v keyword$KEYWORD BEGIN { count 0 } { # 假设日志格式为2023-10-01 12:34:56 ERROR ... if ($1 $2 start index($0, keyword)) { count } } END { print count } $LOG_FILE 2/dev/null) if [ -z $COUNT ]; then COUNT0 fi echo $COUNT此脚本的核心是$1 $2 start——利用awk对字符串的字典序比较直接比较日期时间字符串如2023-10-01 12:34:56 2023-10-01 12:29:56无需转换为时间戳性能极高。实测在10GB日志文件上执行时间0.3秒。Zabbix Item类型设为Numeric (unsigned)触发器设为{myhost:check_log_rate.sh.last()} 50。4.3 重量级集成logstash或filebeat的流式分析适用于高吞吐、复杂规则当单机日志量达到TB级或需要“同一IP在1分钟内请求失败5次且错误码为503”这类复杂关联规则时Shell脚本已达性能瓶颈。此时应升级架构用filebeat将日志实时发送到logstash在logstash中用dissect、grok解析用aggregate插件做跨事件聚合再将结果写入Elasticsearch。Zabbix不再直接读日志而是监控logstash的指标如logstash_pipeline_events_out和ES中预计算的告警索引。这是一个典型的“Zabbix ELK”联合监控方案。我在某电商大促期间就采用此方案filebeat采集Nginx access.loglogstash实时计算各业务线的4xx/5xx比率存入ES的alert_metrics索引Zabbix通过elasticsearch.query[]键值定时查询GET /alert_metrics/_search?qrate:0.05一旦命中即触发告警。这样Zabbix专注告警决策日志分析交给专业工具各司其职稳定可靠。5. 高级技巧与避坑指南让自定义监控真正“稳如磐石”写一个能跑的脚本很容易写一个在生产环境连续运行365天不出问题的脚本需要的是对边界条件的敬畏和对Zabbix特性的深刻理解。以下是我在多个项目中总结的硬核经验每一条都来自真实的血泪教训。5.1 脚本超时与Zabbix采集周期的精确对齐Zabbix Item的Update interval更新间隔和脚本自身的执行时间必须满足脚本最大执行时间 Update interval * 0.8。为什么因为Zabbix Agent在执行脚本时会设置一个内部超时默认为Timeout参数通常30秒如果脚本未在此时间内完成Agent会强制杀掉进程并标记Item为Not supported。更隐蔽的问题是如果脚本执行时间接近Update interval如间隔60秒脚本平均耗时55秒当某次脚本因IO抖动耗时62秒就会导致本次采集失败下次采集立即开始形成“采集风暴”Agent CPU飙升。我的实践是对所有脚本进行压力测试记录P99执行时间然后将Update interval设为该时间的3倍。例如check_log_rate.shP99耗时1.2秒则Update interval设为180秒3分钟。同时在脚本开头加入超时控制#!/bin/bash # 脚本自身超时防止阻塞Agent TIMEOUT10 # 执行主体逻辑用timeout包裹 RESULT$(timeout $TIMEOUT /usr/local/bin/real_check_logic.sh 2/dev/null) if [ -z $RESULT ]; then echo 0 # 超时返回默认值 else echo $RESULT fi5.2 日志滚动与文件句柄泄漏的终极解法tail -f或tail -n在日志滚动logrotate时如果脚本没有正确关闭文件句柄会导致/var/log/myapp/app.log.1等旧文件句柄一直被占用磁盘空间无法释放。Linux系统中每个打开的文件句柄都消耗一个inotifywatcher总量有限默认8192。当大量脚本同时监控日志watcher耗尽tail命令会失效。我的解法是永远不用tail -f改用inotifywait事件驱动。脚本check_log_inotify.sh#!/bin/bash LOG_DIR/var/log/myapp LOG_FILEapp.log KEYWORDERROR # 创建临时命名管道 PIPE/tmp/zabbix_log_pipe_$$ mkfifo $PIPE # 启动inotifywait监听日志文件移动和写入事件 inotifywait -m -e move_self,create,modify $LOG_DIR 2/dev/null | \ while read path action file; do if [[ $file $LOG_FILE ]] || [[ $action MOVED_TO $file $LOG_FILE ]]; then # 日志被重命名或新写入触发检查 COUNT$(tail -n 100 $LOG_DIR/$LOG_FILE 2/dev/null | grep -c $KEYWORD 2/dev/null) echo $COUNT $PIPE fi done INOTIFY_PID$! # 主循环从管道读取最新计数 trap kill $INOTIFY_PID 2/dev/null; rm -f $PIPE EXIT while true; do if read -t 1 COUNT $PIPE; then echo $COUNT break fi done此脚本用inotifywait监听文件系统事件完全规避了tail的句柄泄漏问题且资源消耗极低。inotifywait是inotify-tools包的一部分轻量级推荐在所有Agent节点安装。5.3 Zabbix触发器的“防抖”设计避免告警风暴一个脚本返回0异常Zabbix立刻告警下一秒脚本返回1恢复Zabbix立刻恢复。但在网络抖动、服务短暂GC时这种“毛刺”会导致告警狂闪。Zabbix的Problem expression问题表达式支持count()函数这才是防抖的关键。例如对check_service_port.sh不直接用last()0而是{myhost:check_service_port.sh.last()}0 and {myhost:check_service_port.sh.count(5m,0)}3意思是“最近5分钟内值为0的状态出现超过3次”。这样单次抖动被忽略只有持续性故障才触发告警。更进一步可以结合timeleft()函数实现“预测性恢复”{myhost:check_service_port.sh.timeleft(0)}600表示“预计故障状态剩余时间少于10分钟”可用于降级通知而非紧急告警。这些函数是Zabbix触发器的高级武器熟练运用能让告警从“噪音”变成“情报”。5.4 安全加固脚本权限的最小化原则生产环境中脚本常需调用mysql、curl甚至ssh这带来巨大安全风险。我的黄金法则是Zabbix Agent用户只能拥有完成监控任务所必需的最小权限。具体措施禁用zabbix用户的交互式shellusermod -s /sbin/nologin zabbix将脚本所需命令的PATH限制在/usr/bin和/bin修改Agent配置UnsafeUserParameters0默认已关闭并确保脚本中所有命令都用绝对路径。数据库连接使用专用只读账号mysql -u monitor_ro -pxxx -h 127.0.0.1 -e SELECT 1密码明文存储在脚本中虽不完美但比zabbix用户拥有DBA权限安全得多。敏感操作如重启服务绝不放入监控脚本监控只负责“发现”修复由独立的自动化平台如Ansible Tower执行两者权限隔离。最后分享一个真实案例某客户曾因监控脚本中curl调用了一个内部管理API/api/v1/restart而该API未做鉴权导致Zabbix Server被入侵后攻击者通过修改Item Key远程执行了curl http://localhost:8080/api/v1/restart?servicenginx整个集群服务被反复重启。根源就在于监控脚本越权。记住监控脚本的唯一使命是报告不是执行。我在实际使用中发现最有效的自定义监控往往诞生于一个具体的、让人夜不能寐的故障场景。比如曾经为了定位一个偶发的“订单支付成功但未扣库存”的问题我写了check_inventory_consistency.sh它每5分钟比对MySQL订单表和Redis库存缓存的差异一旦发现不一致立即抓取相关SQL和Redis key存入专门的inventory_alert表。这个脚本上线后3天内就捕获了2次数据不一致最终定位到是Redis过期策略与MySQL事务隔离级别的冲突。它没有华丽的UI没有复杂的架构就是几行朴实的SQL和Shell但它解决了真问题。所以别被“自定义监控”这个词吓住拿起编辑器从你今天最想盯住的那个服务、那行日志开始写起——Zabbix的威力永远在你亲手写的第一个echo 1里。
返回列表