
先说结论Zabbix 自带的 Email 介质类型不是不能用但用到真实生产环境里你迟早会碰上需要把它替换掉的一天。我最近在给一套某公司的监控平台重新梳理告警链路核心需求就一句话把触发器产生的报警信息稳定地发到值班邮箱问题恢复后再补一封恢复邮件延迟低、能追踪、还能按人员分流。折腾一圈之后我放弃了内置 SMTP 媒介改成用 Python 脚本收尾反而把整个链路看得明明白白。这篇文章就把整套方案完整记录下来从 Zabbix 如何调用脚本、Python 发信脚本怎么写到 Media Type、用户媒介、Action 怎么串联再到上线后踩过的坑一次性说清楚。1. 内置SMTP媒介能用但为什么我最后还是换成了Python脚本1.1 内置Email类型确实开箱即用Zabbix 内置的 Email 介质类型界面里把 SMTP 服务器、端口、用户名、密码、发件人地址填上就能立即发信。通道少的测试环境里这种配置方式真的省事。当时我刚接到需求时第一反应也是走这条路新建一个 Email 介质类型填好邮箱服务商给的 SMTP 地址和授权码再绑定用户动作里选择这个媒介发送十几分钟就能把邮件告警跑起来。Zabbix 本身还会自动在告警记录里标记“已发送”操作路径短任何刚接触 Zabbix 的运维都能快速上手。所以说内置 Email 介质类型不是没有价值它覆盖了大量对格式、送达率、分发策略没有特殊要求的场景。1.2 生产环境里几道绕不过去的坎可真实环境里的问题往往不是配置不出来而是配置出来的东西不稳定还没法快速定位。第一道坎是邮箱服务商的认证策略越来越严。很多企业邮箱和公共邮箱服务商不再接受简单的“账号加登录密码”方式而是要求使用客户端授权码甚至要单独设置 IP 白名单。Zabbix 内置 Email 类型给到你的就是用户名、密码、发件人地址这几个固定字段一旦遇到服务商返回“认证失败”或“TLS 版本不支持”你基本只能靠猜。第二道坎是排障链路不透明。内置类型的发送逻辑封装在 Zabbix 内部邮件队列阻塞、SMTP 握手超时、对方服务器拒绝接信都只会体现在告警记录里的一条失败状态上。你想看具体是哪一步断了没有日志可查。对于生产告警这种“黑盒”是不可接受的。第三道坎是灵活性不够。同一个动作里有人希望把多台主机的问题汇总成一封摘要邮件有人希望按严重级别走不同的通知策略有人想在企业微信、钉钉、邮件之间做切换。内置 Email 类型只能笨拙地发一封标准邮件任何内容加工都得另起炉灶。所以我的思路很明确把发信动作集中到一个 Python 脚本里把 SMTP 连接、参数校验、日志、重试、内容格式化全放进脚本Zabbix 只负责把事件转换成参数并调用脚本。控制权完全回到自己手里。1.3 自建脚本后换来的是什么换成 Python 脚本之后最明显的变化是问题可观测了。脚本每次运行都会写日志收件人、主题、发送结果一目了然。SMTP 握手失败会输出具体异常栈TLS 协商报错能看到详细原因不再像内置媒介那样只有一个孤零零的失败状态。另一个变化是规则可编排了。脚本里可以加收件人白名单可以按收件人前缀走不同 SMTP 通道也可以把正文先格式化再发送。Zabbix 侧的逻辑保持简单复杂业务都在脚本层解决职责非常清晰。2. 告警链路和参数传递先搞清Zabbix怎么调用脚本很多人在这一步卡住不是因为不会写 Python而是不理解 Zabbix 到底用什么姿势调用脚本。先把这个机制讲透后面配置起来就顺了。2.1 脚本型媒介在Zabbix内部的执行过程Zabbix 的告警发送是由服务端的 Alert Manager 组件负责的。当一条动作命中某个触发器事件时Zabbix 会把事件封装成一条发送任务然后根据介质类型找到对应脚本。所有脚本型媒介都会在zabbix_server.conf配置的AlertScriptsPath目录下执行也就是说你在界面里填的“脚本名称”其实只是一个文件名完整路径由服务端拼接而成。执行时Zabbix 其实就是在命令行里运行了这样一条命令/usr/bin/python3 /usr/lib/zabbix/alertscripts/send_mail.py opsexample.com 告警标题 告警正文这个细节很重要。你要先确认AlertScriptsPath到底配置在哪个目录。可以这样查grep -E ^AlertScriptsPath /etc/zabbix/zabbix_server.conf如果搜不到相关配置不同发行版的默认目录可能不同常见的是/usr/lib/zabbix/alertscripts或/etc/zabbix/alertscripts。想省事的话也可以直接在配置文件里显式加一行比如AlertScriptsPath/usr/lib/zabbix/alertscripts改完配置记得重启zabbix_server服务否则不会生效。2.2 三个核心宏参数和一张映射表脚本型介质类型在界面里有一项“脚本参数”这一项决定了 Zabbix 按什么顺序、把哪些内容作为命令行参数传给脚本。常用配置就是下面三个宏顺序固定但可以由你自定义{ALERT.SENDTO} {ALERT.SUBJECT} {ALERT.MESSAGE}对应关系如下表参数位置Zabbix宏含义脚本中变量参数1{ALERT.SENDTO}用户媒介里配置的收件邮箱sys.argv[1]参数2{ALERT.SUBJECT}动作里设置的默认主题sys.argv[2]参数3{ALERT.MESSAGE}动作里设置的默认正文sys.argv[3]说白了脚本收到的就是三个普通字符串不用关心 Zabbix 内部事件模型只需要把这些字符串组织成一封 MIME 邮件发送出去。这种设计非常朴素但对脚本编写者来说反而是好事逻辑简单便于测试。你完全可以在命令行里手工模拟同样的调用把脚本调通之后再回到 Web 界面里配置。2.3 执行身份、环境变量与调试入口在 Zabbix 里守护进程zabbix_server一般以zabbix用户身份运行所以脚本也必须让这个用户可读、可执行。很多新手直接复制脚本文件到 alertscripts 目录后忘了加权限结果界面里触发动作时脚本就静默失败。目录和文件权限至少要是chown zabbix:zabbix /usr/lib/zabbix/alertscripts/send_mail.py chmod 755 /usr/lib/zabbix/alertscripts/send_mail.py还有一点经常被忽略zabbix_server是从受限环境去执行外部脚本的PATH环境变量非常短不会自动加载你平时 shell 里那些路径。所以脚本中调用外部命令时尽量使用绝对路径避免依赖环境变量。调试时也建议完全模拟 Zabbix 的执行身份sudo -u zabbix /usr/bin/python3 /usr/lib/zabbix/alertscripts/send_mail.py opsexample.com 测试标题 测试正文如果这条命令能成功再回 Web 界面测试至少能排除一大部分脚本本身的问题。3. 一封像样的告警邮件Python发送模块的设计与实现脚本本身不难难点在细节处理。SMTP 连接、认证、超时、邮件格式、编码、日志每一项都直接影响告警的可靠度。3.1 为什么用smtplib而不是封装好的邮件HTTP接口你可能会问现在很多邮箱服务商都有 HTTP 接口直接用requests调接口不是更省事吗我当时也对比过。对于邮件告警这种高频、低延迟、需要稳定认证的场景SMTP 协议反而是最成熟可靠的方案。smtplib是 Python 标准库不需要额外安装第三方包服务器上只要有 Python 就能用。企业邮箱和各类邮件服务商对 SMTP 的兼容性普遍很好SASL 认证、SSL/TLS 都支持得比较完整。另外SMTP 排障路径清晰你只要盯住握手、认证、发信、退信这四个阶段就够了。3.2 能直接落地的一段发信脚本先给出完整可用的脚本再逐步解释关键点。这套脚本我放在/usr/lib/zabbix/alertscripts/send_mail.py内容如下#!/usr/bin/env python3 # -*- coding: utf-8 -*- Zabbix 邮件告警发送脚本 用法: send_mail.py 收件人 主题 正文 import os import sys import logging import smtplib from email.mime.text import MIMEText from email.header import Header from email.utils import formataddr # SMTP 基础配置改成你自己的邮箱服务商参数 SMTP_HOST smtp.example.com SMTP_PORT 465 SMTP_USER zabbixexample.com SMTP_PASS your-auth-code SMTP_FROM zabbixexample.com SMTP_FROM_NAME Zabbix Alert # 允许通过环境变量覆盖方便调试和不同环境部署 SMTP_HOST os.getenv(ALERT_SMTP_HOST, SMTP_HOST) SMTP_PORT int(os.getenv(ALERT_SMTP_PORT, SMTP_PORT)) SMTP_USER os.getenv(ALERT_SMTP_USER, SMTP_USER) SMTP_PASS os.getenv(ALERT_SMTP_PASS, SMTP_PASS) SMTP_FROM os.getenv(ALERT_SMTP_FROM, SMTP_FROM) LOG_FILE /var/log/zabbix/alert_mail.log def setup_logging(): log_dir os.path.dirname(LOG_FILE) if log_dir and not os.path.isdir(log_dir): os.makedirs(log_dir, exist_okTrue) logging.basicConfig( filenameLOG_FILE, levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, ) def send_mail(to_addr, subject, body): msg MIMEText(body, plain, utf-8) msg[Subject] Header(subject, utf-8) msg[From] formataddr((str(Header(SMTP_FROM_NAME, utf-8)), SMTP_FROM)) msg[To] to_addr if SMTP_PORT 465: server smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout30) else: server smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout30) server.starttls() try: server.login(SMTP_USER, SMTP_PASS) server.sendmail(SMTP_FROM, [to_addr], msg.as_string()) finally: server.quit() def main(): setup_logging() if len(sys.argv) ! 4: logging.error(参数数量不对收到 %d 个参数: %s, len(sys.argv) - 1, sys.argv[1:]) sys.exit(1) to_addr, subject, body sys.argv[1], sys.argv[2], sys.argv[3] if ( not in to_addr or to_addr.count() ! 1 or len(to_addr) 128 or any(ch in to_addr for ch in [ , ,, \n, \r]) ): logging.error(非法收件人地址: %s, to_addr) sys.exit(1) if not subject.strip(): subject (无主题) if len(body) 200000: logging.warning(正文长度 %d可能超过单封邮件大小限制, len(body)) try: send_mail(to_addr, subject, body) logging.info(告警邮件已发送 - %s | 主题: %s, to_addr, subject[:100]) except Exception as exc: logging.exception(告警邮件发送失败 - %s | 错误: %s, to_addr, exc) sys.exit(70) if __name__ __main__: main()你只需要把脚本里的SMTP_HOST、SMTP_PORT、SMTP_USER、SMTP_PASS、SMTP_FROM换成你自己邮箱服务商的参数然后保存、赋权就能测试了。3.3 中文主题、正文编码与邮件头规范邮件告警里最容易出乱码的就是中文主题和中文正文。我把这部分的处理单独拎出来讲。正文部分使用MIMEText(body, plain, utf-8)明确指定纯文本和 UTF-8 编码。主题部分则通过Header(subject, utf-8)进行编码这样主题里的中文在邮件头中会被正确编码成可传输的格式各个邮件客户端都能正常解码。发件人名称同理Header(SMTP_FROM_NAME, utf-8)确保“Zabbix Alert”这类中文或特殊字符不会破坏整个邮件头结构。有一个很多人忽略的规范问题SMTP_FROM这个发件地址最好和SMTP_USER的认证账号保持同一域名。很多邮件服务商在 SMTP 认证之后会校验信封里的发件地址是否属于当前账号域不一致会直接拒发。所以我建议让SMTP_FROM SMTP_USER或者至少保证域名一致。3.4 参数校验、日志记录和异常退出脚本里的参数校验不是摆设。Zabbix 传入的{ALERT.SENDTO}来自用户媒介配置正常情况确实是邮箱地址但为了安全还是要防止邮件头注入。脚本里对收件人做了检查必须只含一个不能有空格、逗号、换行符。因为如果to_addr里被塞进换行符理论上可以在 MIME 头部注入额外字段这会成为一种利用途径。日志设计也很关键。我将日志写到/var/log/zabbix/alert_mail.log每次发送都记录收件人、主题摘要和最终异常信息。脚本最终是否返回非零退出码会影响 Zabbix 对这个动作标记为成功还是失败。正常情况返回 0参数错误返回 1发送异常返回 70这个数字是sysexits.h中“软件内部错误”的约定值便于统一识别。提示日志目录必须让zabbix用户有写权限。否则脚本运行到logging.basicConfig就会直接抛异常。建议提前执行mkdir -p /var/log/zabbix chown zabbix:zabbix /var/log/zabbix。4. Zabbix前端串联Media Type、用户媒介、Action做到位脚本准备好之后剩下的就是在 Zabbix 前端把整条链路串起来。这一步看着简单但每一步都有容易踩的细节。4.1 在管理界面创建脚本型Media Type进入“管理 → 告警媒介 → 媒介类型”点击“新建媒介类型”。名称我建议起一个相对具体的名字比如python-mail-alert不要叫mail这种太泛的名字避免以后和企业邮件方案混淆。类型选择“脚本”。脚本名称填send_mail.py注意这里只填文件名不填路径。脚本参数区域依次加三行{ALERT.SENDTO} {ALERT.SUBJECT} {ALERT.MESSAGE}创建后一定确认“已启用”勾选状态是开启的。这一步漏掉的话动作会直接跳过这个媒介而且不会给你任何特别明显的报错。Zabbix 不同版本对“媒介”和“介质类型”的中文翻译略有差别但路径都在管理模块下对应关系是明确的。4.2 用户媒介的时间窗、严重性过滤和归属介质类型建好后还要把它分配给具体接收人。在“用户 → 用户列表”里选择一个用户进入“告警媒介”页签添加媒介。选择刚才创建的python-mail-alert在“发送到”里填真实收件邮箱。那个“时间窗口”如果不特别设置默认是全天候也就是一周七天、每天 00:00 到 24:00。如果你是做夜间值班轮换可以在这里按群体配置不同的时间段。严重性过滤也建议在这里合理设计。比如普通值班人员只接收“高”和“灾难”两个级别工程师级别接收“中”以上这样能减少半夜被无关告警轰炸的疲劳感。用户媒介没有配置好的话就算后面动作写再完美也会出现“动作执行成功但没人收到邮件”的情况。4.3 触发动作的消息模板与恢复通知介质和用户都准备好了接着是动作。进入“配置 → 动作 → 触发动作”新建动作。动作里有两块核心配置一块是操作另一块是恢复操作。操作里选择“发送消息”目标选择你配置过媒介的用户组或者直接选具体用户然后在“仅发送到”里选择python-mail-alert。消息模板可以这样设计主题[告警] {TRIGGER.STATUS}: {TRIGGER.NAME}正文告警主机{HOST.NAME} 告警IP{HOST.IP} 触发器{TRIGGER.NAME} 级别{TRIGGER.SEVERITY} 状态{TRIGGER.STATUS} 当前值{ITEM.NAME1} {ITEM.VALUE1} 触发时间{EVENT.DATE} {EVENT.TIME}恢复操作同样选择发送消息主题用[恢复] {TRIGGER.STATUS}: {TRIGGER.NAME}正文里可以补上恢复时间{EVENT.RECOVERY.DATE} {EVENT.RECOVERY.TIME}。这里有个值得推广的做法问题操作和恢复操作不要共用同一套模板。恢复邮件里的关键信息应该是“什么时候恢复的、当前值恢复到多少”这样值班人员才能快速关闭工单。条件设置建议从宽到严逐步收敛。初上线时可以只选“主机组属于 XX”不加严重级别条件跑几天之后再根据告警量细化。Step duration 在测试阶段可以调成 60 秒方便快速看到重复通知生产建议保持默认的 1 小时或者按值班安静时段的实际诉求调整。4.4 从命令行到Web端的完整验证路径配置完成后不要直接等真实故障来验证。我习惯按下面顺序做完整验证先确认配置已重载执行zabbix_server -R config_cache_reload。在命令行手工模拟脚本调用确认脚本本身能发信。回到 Web 界面临时建一个监控项和触发器比如system.uptime[].last()小于 0 这种必失败的表达式触发一条问题事件。到“报表 → 动作日志”里看动作是否执行再到脚本日志和收件邮箱确认最终结果。确认恢复动作正常后把临时触发器和监控项清理掉。如果动作日志里有记录、脚本日志也显示发送成功但邮箱收不到基本可以排除 Zabbix 侧的问题接下来就是第 5 节的送达类问题了。5. 上线后最常见的四个坑及排查思路这套方案我前后用了很长时间踩过的坑基本集中在四类问题上写出来帮大家少走弯路。5.1 脚本不执行路径、权限、换行符三件套如果配置了动作却迟迟收不到邮件首先怀疑脚本本身没有执行或者执行时报错。排查顺序建议是先查zabbix_server.conf里的AlertScriptsPath确认脚本文件确实在对应目录。再看脚本权限用ls -l看属主和执行位。属主是root但权限 600 的话zabbix用户根本没有读取权限。最后用file send_mail.py检查文件类型。如果在 Windows 上编辑后直接上传文件可能带了 CRLF 换行Linux 执行时会出现: No such file or directory这类诡异错误。排查时的关键动作是回到第 2 节那句手工调用命令由zabbix用户直接执行脚本。如果手工执行能发信Web 端却不行那大概率还是 Zabbix 查不到脚本或者参数配置错了。5.2 中文乱码不是邮箱问题是编码处理不到位有时候手工执行测试内容正常但 Zabbix 发出来的邮件中文乱码常见原因有三个脚本没有用Header(subject, utf-8)编码主题MIMEText 没有指定charset或者终端环境向脚本传入了错误的字节序列。前两个问题代码里已经解决。第三个问题要注意的是Zabbix 动作模板里如果是从别处复制过来的文本有可能带着 BOM 头或者特殊换行符传参后邮件内容看起来一堆横线和问号。解决办法是把动作里的“默认消息”重新手工敲一遍尤其注意引号和换行。还不行的话就在脚本里对正文做一次body.encode(utf-8, errorsreplace).decode(utf-8)把非法字节清理干净再发送。5.3 发件被拦截或延迟到达的根因查找脚本执行成功日志也显示“发送成功”但用户一直没收到问题往往出在邮件信任链上。最典型的原因有三个一是发件地址与认证账号不一致。很多邮件服务商要求认证身份和MAIL FROM地址必须在同一域这也是我前面强调SMTP_FROM要和SMTP_USER一致的原因。二是域名没有 SPF/DKIM 授权。你通过某个 SMTP 服务器发信等于让那台服务器代替你的域名发信。为了证明“这封邮件确实是该域名主动发的”域名管理员需要在 DNS 里配置 SPF 记录允许这个发信服务器的 IP 段。没有 SPF 的邮件很容易被对方判断为仿冒邮件丢进垃圾箱。三是发信频率触发了限流。大批量告警时会遇到 SMTP 服务商返回连接超时或450之类的临时错误这是临时限流。脚本里遇到异常退出会返回非零码Zabbix 会把这个动作标记失败。但邮件本身可能已经进了服务商队列后面又补投了结果就是用户收到的告警时间有偏差。遇到这种情况建议在脚本里加一段简单的重试逻辑发送失败后等待 5 秒再重试一次最多三次降低偶发失败概率。5.4 告警风暴造成并发涌堵时的应对最后说一个所有监控系统都会遇到的场景某台核心数据库或网络设备一抖动底下成百上千台主机的触发器同时报警。这时候 Zabbix 会在极短时间内部署大量脚本进程每个脚本都去建一次 SMTP 连接。结果不是你邮箱爆了就是对方的 SMTP 网关先把你限流甚至拉黑。应对思路从根源上讲要在动作层做好两条合理使用严重级别条件避免把“信息级”事件也全套发一遍在“操作”步骤里设置升级循环同一问题的重复通知不要无限刷。其次在动作里加条件比如“触发事件最多 N 次”或者在触发器表达式里用nodata()、last()做一次抑制让故障恢复前的中间状态不再反复产生新事件。脚本层也可以做一点保护。最简单的方法是加一个临时文件锁让同一时刻只允许一个发信进程运行但这种方式在高并发场景会丢告警我不建议默认使用。更稳妥的做法是设置 SMTP 连接的超时时间避免脚本因为网络阻塞而长时间占住进程资源。脚本里的timeout30就起到这个作用宁可放弃某一条消息也不要让大量脚本堆积在 Zabbix 端拖垮整个服务。邮件告警的稳定性不在脚本多复杂而在链路每一环都可控。把 Zabbix 的调用机制、脚本的编码细节、SMTP 的送达链路都摸清楚之后这套 Python 发信方案就是一套可以反复复用、出了问题能按日志快速定位的成熟方案。之后再遇到更复杂的告警场景比如摘要合并、按值班人分流、多渠道联动你只需要在这个脚本基础上继续延伸即可不需要再动 Zabbix 侧的框架。