☰
AlertManager告警通知实战:邮件与企业微信配置全解析
2026/10/5 15:50:07 网站建设 项目流程

前阵子凌晨两点多,我放在床头的手机开始不停震动。打开一看,是Prometheus告警:数据库连接数飙升,服务已经出现大量超时。好在当时值班同事通过微信告警群看到了消息,赶在用户集中反馈之前把问题处理掉了。这个场景,就是AlertManager存在的意义——它把Prometheus发现的异常,变成有人看到的通知。AlertManager承担了从告警产生到邮件、微信告警触达之间的所有工作,包括分组、去重、抑制和路由。这篇文章就围绕AlertManager的邮件告警和微信告警配置展开,把整个链路从原理到实操完整梳理一遍,适合正在搭建或优化Prometheus告警体系的运维、SRE和后端同学参考。

1. 告警通知失联的痛点与AlertManager的两个核心价值

1.1 为什么Prometheus离不开AlertManager

可能有的同学刚接触Prometheus时会有疑问:prometheus.yml里有rule_files,规则里写了expr和labels,触发之后会标记一个alert,这不就够了吗?实际上Prometheus只负责评估规则并将告警状态暴露在API里,它没有内置的发送能力。没有AlertManager,告警就只是Prometheus界面上的一条状态,人不在电脑前根本看不到。

AlertManager把“告警状态”变成“通知动作”。它从Prometheus收到firing的告警后,会做四件事:接收、去重、分组、路由。多台Prometheus上报同一条告警时只发一次;同一时间大量告警时可以按项目或实例打包成一条;不同严重级别可以走不同的通知渠道。这些能力是为生产环境下的告警疲劳准备的。

我见过不少团队直接拿脚本轮询Prometheus API去发邮件,一开始能用,到后面规则一多,要么重复发送,要么不知道怎么按业务线分流。AlertManager把这些机制做得很成熟,与其自己造轮子,不如把通知这块交给它。

1.2 邮件和微信在告警链路中的不同定位

先说结论:正式环境里,我建议邮件和微信同时配,不要二选一。邮件起到的是留痕和归档作用,适合事后排查时翻时间线、做周报;微信起到的是即时触达作用,适合值班人员第一时间看到并响应。两者面对的阅读场景完全不同——邮件是“稍后处理”,微信是“现在处理”。

我这里说的微信告警,指的是企业微信,不是个人微信。为什么?个人微信没有面向第三方的消息推送接口,也不可能让你往某个群自动发消息,硬折腾个人微信接口属于高风险的违规操作,账号容易被限制。企业微信提供标准的API和Webhook,可以把告警推送到应用、群机器人、部门成员,这才是生产环境可用的方案。

另外有个容易被忽略的点:邮件和微信的通知能力在不同故障场景下表现不一样。比如邮件服务器本身挂了,邮件告警自然失效,此时微信如果还在,那就是唯一通道。反过来,如果内网出口网络出问题,企业微信API连不上,邮件可能还能通过备用线路出去。所以两条通道互为冗余,价值远远大于“多一个通知方式”。

2. 部署配置前置:告警规则、路由与通知渠道的关系

2.1 需要准备的环境和版本

这套东西跑起来其实很轻。我当前环境用的是Prometheus 2.45 + AlertManager 0.26,CentOS 7和Ubuntu 22.04都跑过。AlertManager就是个二进制文件,从官网下载.tar.gz解压,或者用容器跑都行,没有特殊依赖。

需要准备的东西如下:

  • Prometheus服务端,一台即可,负责采集和规则评估。
  • AlertManager服务,默认端口9093,负责通知。
  • 企业微信后台的管理员权限,用于创建自建应用、获取CorpID和AgentId。
  • 一个可用的SMTP发送账号,比如企业邮箱、163邮箱等,用于邮件告警。

组件之间的连通关系是:Prometheus把告警POST到AlertManager的API,AlertManager根据route配置决定走哪个receiver,receiver再调用SMTP或者Webhook。微信这一侧,AlertManager不能直接调企业微信API,需要在中间加一个转发服务。这个转发服务在后文会给出完整代码。

2.2 Prometheus告警规则怎么写得准

告警规则的质量,直接决定AlertManager收到的告警有没有价值。如果Prometheus发过来一堆无用告警,AlertManager再智能也没办法变废为宝。

规则文件一般长这样:

groups: - name: host-alert rules: - alert: HostDown expr: up == 0 for: 2m labels: severity: critical annotations: summary: "{{ $labels.instance }} 不可达" description: "实例 {{ $labels.instance }} 连续2分钟无法采集,请检查主机网络或服务状态。" - alert: HighCPUUsage expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85 for: 10m labels: severity: warning annotations: summary: "{{ $labels.instance }} CPU使用率过高" description: "实例 {{ $labels.instance }} 的CPU使用率超过85%,持续10分钟。"

几个容易踩的细节:expr里一定要设计好for,避免瞬时抖动误报。磁盘类、进程类告警建议加“持续N分钟”条件。labels里的severity一定要带上,后面分组、抑制、路由都靠它。annotations里的模板不要写太复杂,万一某个label不存在,渲染出来的内容会很难看。

还有个经验:告警规则的数量控制在“人能处理”的范围内。我见过一个Prometheus配了200多条规则,结果每天告警几百条,团队直接屏蔽通知。宁可规则少而精,也不要多而噪。

2.3 AlertManager的主配置结构说明

AlertManager只有一个配置文件alertmanager.yml,结构分四块:global、route、receivers、inhibit_rules。先看一个最小可用的例子:

global: resolve_timeout: 5m smtp_smarthost: 'smtp.example.com:465' smtp_from: 'alert@example.com' smtp_auth_username: 'alert@example.com' smtp_auth_password: 'your-password' route: group_by: ['alertname'] group_wait: 30s group_interval: 5m repeat_interval: 4h receiver: 'email-wechat' receivers: - name: 'email-wechat' email_configs: - to: 'ops-team@example.com' webhook_configs: - url: 'http://127.0.0.1:8080/wechat' send_resolved: true inhibit_rules: - source_matchers: [severity = "critical"] target_matchers: [severity = "warning"] equal: ['alertname']

新版AlertManager里,匹配标签用的是matchers,旧版的match和match_re虽然还兼容,但配置新环境建议直接用matchers。url里的地址就是后面转发服务监听地址。send_resolved一定要开,否则故障恢复了你微信里永远等不来“已恢复”这条消息,值班人员会一直处于紧绷状态。

3. 邮件告警落地:SMTP配置与收信实测

3.1 SMTP配置与参数解释

邮件告警的核心参数都在global和receivers里。smtp_smarthost填写SMTP服务器地址和端口。这里有个大坑:很多云厂商和机房默认封禁25端口,导致邮件发不出去。建议优先使用465(SSL)或587(STARTTLS),这两个端口一般不会被封。

举几个常见邮箱的SMTP配置参考:

邮箱类型SMTP服务器端口加密方式
网易163smtp.163.com465SSL
腾讯企业邮smtp.exmail.qq.com465SSL
QQ邮箱smtp.qq.com465SSL
阿里企业邮smtp.qiye.aliyun.com465SSL

使用465端口时,需要在global里设置smtp_require_tls,新版本默认会自动协商SSL,建议显式配置更稳妥。auth_username和smtp_from最好保持一致,很多服务器会校验发件人地址是否和登录账号匹配,不一致直接被拒。

如果公司有自建邮件服务器,比如Postfix或Exchange,可以先在本地命令行用telnet或者swaks测试一下协议是否通畅,再填入AlertManager。直接配到AlertManager再排查,日志信息不够直观,反而慢。

3.2 告警邮件的模板与标题优化

默认情况下AlertManager发的邮件是一个表格样式,包含告警名、标签、注解、开始时间等,能看但不够清晰。我强烈建议自定义邮件模板,尤其要优化邮件标题。因为很多人收件箱里告警邮件一多,靠标题识别紧急程度非常重要。

在alertmanager.yml里指定模板路径:

templates: - '/etc/alertmanager/templates/*.tmpl'

邮件标题可以通过email_configs的headers来定制:

receivers: - name: 'email' email_configs: - to: 'ops-team@example.com' send_resolved: true headers: subject: '{{ template "email.subject" . }}'

对应的模板文件:

{{ define "email.subject" }} [{{ .Status | toUpper }}] {{ .CommonLabels.alertname }} - {{ range .Alerts }}{{ .Labels.instance }}{{ end }} {{ end }}

这会让邮件标题变成[FIRING] HostDown - 192.168.1.10这样的格式。手机锁屏通知栏里一眼就能看到是哪个实例出了什么问题。正文模板同理,把summary、description、开始时间、当前值都列出来,节省排查时来回翻页面时间。

3.3 收不到邮件和进垃圾箱的问题排查

邮件告警最常见的问题就是“Prometheus已经触发告警了,但邮箱里什么都没有”。排查链路我建议按这个顺序走:

  1. 先看AlertManager日志,启动时加--log.level=debug可以看到smtp请求的详细返回。如果日志显示connection refused,基本是端口被封或服务器不可达。
  2. 检查smtp_auth_password是否填对,很多邮箱的“密码”是单独的客户端授权码,不是登录密码。163、QQ邮箱都需要先去网页端开启SMTP服务并生成授权码。
  3. 确认From、AuthUsername是否一致,不一致会在EHLO后直接报535错误。
  4. 如果日志显示发送成功但收件箱没有,去垃圾箱翻一翻。大量监控邮件很容易被反垃圾规则拦截,可以在邮箱里设置白名单,把发件地址加入白名单。

我还遇到过一种情况:邮件发到团队共享邮箱,结果邮箱容量满了,新邮件一直退信。这种问题日志里能看见退信原因是mailbox full,但很多人不会去看退信。建议告警邮件发到一个单独的邮箱或者带归档策略的邮箱,避免和日常邮件混在一起被容量限制。

4. 微信告警落地:企业微信自建应用与转发服务

4.1 为什么不用个人微信

关于个人微信我多说一句:不要考虑用个人微信做告警推送。个人微信没有官方API,所有通过模拟网页协议、Hook等方式发的消息都违反软件使用规范,有封号风险。如果告警系统因此不稳定,比告警不发还要麻烦。生产环境的告警推送必须走正规渠道,企业微信是目前最合适的载体,免费、有API、支持群机器人。

企业微信提供两种接入方式:一种是群机器人Webhook,适合直接推到一个群;另一种是自建应用,可以按成员、部门定向推送,还支持文本卡片。群机器人配置最简单,但消息格式不如自建应用灵活,而且只能推送到固定的群里。我下面以自建应用为主,因为告警通常希望发给指定值班人或者运维团队,而不是所有群成员。

4.2 企业微信后台配置步骤

第一步:需要一个企业微信账号,注册就能用。进入管理后台,在“应用管理”里找到“自建”,点击“创建应用”。名称随便填,比如“监控告警”,应用logo选一个明显点的,可见范围里勾选需要接收告警的部门和成员。

创建完成之后,会拿到一个AgentId和一个Secret,这两个参数后面要用。同时在企业信息里能找到CorpID。这三个值不要写在代码仓库里,建议通过环境变量传给转发服务。

发送消息之前,企业微信要求把服务器出口IP加入应用的“企业可信IP”列表。如果服务部署在云主机上,需要去后台添加公网出口IP,否则API调用会报not allow to access from your ip错误。这个细节很容易漏,我最早调通企业微信API就卡在这一步。

4.3 用Python写一个Webhook适配服务

AlertManager发送Webhook时,POST的是一段固定结构的JSON,包含status、alerts、groupLabels等信息。企业微信API期望的是另一个格式,比如text消息要有content字段。所以中间必须有一个适配层。

我写了一个非常轻量的Python服务,基于Flask:

import os import json import requests from flask import Flask, request app = Flask(__name__) WECOM_CORPID = os.getenv("WECOM_CORPID") WECOM_AGENTID = os.getenv("WECOM_AGENTID") WECOM_SECRET = os.getenv("WECOM_SECRET") def get_access_token(): url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" params = {"corpid": WECOM_CORPID, "corpsecret": WECOM_SECRET} resp = requests.get(url, params=params, timeout=5).json() if resp.get("errcode") != 0: raise RuntimeError(resp.get("errmsg")) return resp["access_token"] def build_message(alert_data): status = alert_data.get("status") common = alert_data.get("commonLabels", {}) alerts = alert_data.get("alerts", []) lines = [] lines.append("=== 告警恢复 ===" if status == "resolved" else "=== 告警触发 ===") lines.append("策略: {}".format(common.get("alertname", "unknown"))) lines.append("级别: {}".format(common.get("severity", "unknown"))) for alert in alerts: labels = alert.get("labels", {}) annotations = alert.get("annotations", {}) lines.append("实例: {}".format(labels.get("instance", ""))) lines.append("详情: {}".format(annotations.get("description", annotations.get("summary", "")))) lines.append("时间: {}".format(alerts[0].get("startsAt", ""))) return "\n".join(lines) @app.route("/wechat", methods=["POST"]) def wechat(): data = request.get_json() token = get_access_token() text = build_message(data) msg = { "touser": "@all", "msgtype": "text", "agentid": int(WECOM_AGENTID), "text": {"content": text}, "safe": 0, } url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + token resp = requests.post(url, json=msg, timeout=5).json() return json.dumps(resp), 200, {"Content-Type": "application/json"} if __name__ == "__main__": app.run(host="0.0.0.0", port=8080)

代码逻辑不复杂:接收AlertManager的JSON,提取关键字段拼成一段文字,然后调用企业微信API发到配置的可见成员。这里的touser用的是@all,实际使用时可以改成具体成员的UserID或者部门ID,避免无关人员被告警轰炸。

4.4 用systemd托管转发服务并验证

转发服务是用Flask写的,部署时不建议直接前台跑,我用systemd托了一下:

[Unit] Description=AlertManager WeCom Forwarder After=network-online.target [Service] Environment=WECOM_CORPID=xxx Environment=WECOM_AGENTID=1000002 Environment=WECOM_SECRET=xxx ExecStart=/usr/bin/python3 /opt/wechat-forwarder/app.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target

然后把文件放到/etc/systemd/system/wechat-forwarder.service,执行systemctl daemon-reload、systemctl enable --now wechat-forwarder。之后可以通过systemctl status wechat-forwarder查看运行状态,日志用journalctl -u wechat-forwarder -f追踪。

全部就绪后,随便触发一条测试告警,微信里很快就能收到消息。我第一次跑通时特意模拟了恢复通知,确认resolved状态也能推到微信里。这两个状态的消息最好都在,值班人员才能对故障生命周期有完整感知。

5. 告警治理:分组、静默、抑制的正确用法

5.1 路由分组让告警不再轰炸

告警通知最怕的不是没告警,而是告警风暴。一台机器挂了,node_exporter的up告警、磁盘挂载探测告警、进程探活告警会同时触发,如果每个告警都单独推送,手机直接被打爆。

route的group_by字段就是用来解决这个问题的。把相同维度归成一组,比如group_by: ['alertname']表示同一类告警合并成一条;group_by: ['alertname', 'instance']表示同一个实例的同一类告警合并。group_wait是组内第一条告警的等待时间,在等待期内到达的同组告警会被合并之后再发;group_interval是组内新告警加入后重新发送的间隔;repeat_interval是同一组告警重复发送的间隔。

我的建议是:group_wait设30s,group_interval设5m,repeat_interval根据级别区分。warning级别可以是6h,critical级别可以是1h到2h。这样既不会漏告警,也不会因为同一问题反复提醒而让人麻木。

5.2 静默配置的使用时机

静默(Silence)解决的场景是:明明知道接下来要维护,系统会报一堆预期内的告警,但又不想关闭监控规则。比如更换数据库、扩容节点、重启服务,这种时候在AlertManager的UI界面直接创建一条Silence最方便,指定匹配到某个标签的告警在某个时间段内不发送通知。

生产环境里我习惯把维护类的静默写进配置文件,走Git管理,避免有人临时在UI里创建完忘掉,维护结束后还静默着,真实故障被吞掉。配置文件方式是用matchers匹配:

silence: - matchers: - alertname = "HostDown" - instance = "192.168.1.10" starts_at: 2024-06-01 00:00:00 ends_at: 2024-06-01 04:00:00 created_by: ops-lead comment: "数据库节点维护窗口"

有一点要特别注意:静默不是“关告警”,它只是让告警不发出通知。Prometheus端告警状态仍然是firing的,静默结束后如果故障还在,会继续触发通知。这样设计的好处是,维护期结束后如果真有问题,不会因为静默而彻底漏掉。

5.3 抑制规则过滤衍生告警

抑制(Inhibit)和静默的区别是:静默是提前声明“这段时间别通知”,抑制是根据告警之间的关系自动“折叠”通知。最典型的场景是:交换机挂了,下面几十台服务器都变成unreachable,此时HostDown会有几十条。如果配置了抑制规则,一条交换机级别的critical告警出现后,下游所有warning级别的衍生告警都自动不推送,等交换机恢复再逐个通知。

前面配置里写到的inhibit_rules就是干这个的。关键点是source_matchers和target_matchers的层级关系,以及equal字段。equal里填的标签,表示“当这些标签的值相同时才抑制”。比如一个机房的交换机挂了,机房下所有主机都ping不通,你可以用机房标签做匹配,只有相同机房的告警才会被抑制,避免因为A机房故障把B机房的正常告警也吞了。

抑制规则是好东西,但不要配太激进。我建议只对同一种故障类型的衍生告警做抑制,避免两个毫无关系的告警因为临界抑制而互相覆盖。

6. 从能收到到用得好:我的踩坑记录与调优经验

6.1 高频告警:repeat_interval保守设置的教训

我最早给所有告警都设了repeat_interval: 2h,结果某次磁盘空间反复在阈值边缘抖动,每两小时就触发一次告警,值班同事直接被搞崩溃。后来我把告警按severity分别设置,再用不同的receiver分流,才把这股噪音压下来。

关键改动是把route按severity分开:

route: group_by: ['alertname'] routes: - match: severity: critical receiver: 'email-wechat' repeat_interval: 1h - match: severity: warning receiver: 'email' repeat_interval: 12h

这样critical告警走微信和邮件,warning告警只进邮件。值班人员在微信里看到的一定是必须立刻处理的,而不是一堆“可以等一等”的噪音。告警系统最核心的体验就是“每条消息都值得看”,这比什么技术参数都重要。

6.2 模板渲染结果为空和时区问题

AlertManager用Go Template渲染,有一个非常容易踩的坑:告警标签在CommonLabels里,但单个Alerts的Labels可能和CommonLabels有差异。如果在模板里直接写{{ .Labels.xxx }},当某个标签只在部分告警里存在时,渲染出来可能是空的,导致告警内容缺漏。

另一个坑是时区。AlertManager默认使用UTC时间,很多人在模板里写{{ .StartsAt }},出来的是UTC时间,和本地时间差8小时。要输出本地时间必须写成{{ .StartsAt.Local.Format "2006-01-02 15:04:05" }}。但如果AlertManager跑在容器里,容器的时区是UTC,Local取到的还是UTC,这种情况下要么在容器里设置TZ环境变量,要么直接把时间转成固定时区,比如:

{{ .StartsAt.In (time.LoadLocation "Asia/Shanghai").Format "2006-01-02 15:04:05" }}

不过AlertManager默认没有加载时区库,使用LoadLocation一般没问题,但如果跑在精简的scratch镜像里可能需要额外处理。最省心的做法还是把容器时区统一设置为Asia/Shanghai。

6.3 告警风暴时的降噪和升级策略

最后聊一个进阶话题:告警风暴时怎么办。无论分组、抑制怎么配,真正的大故障爆发时,告警数量还是一个可观的数字。我现在的做法是在转发服务里加了一级“熔断”逻辑:一分钟内同一个alertname的转发请求超过N次,后续消息直接丢弃,只保留第一条和最后一条。这个逻辑不重,但作用很大。

另外一个思路是升级策略。运维体系里可以定义:告警触发后15分钟未恢复,自动把severity从warning提升到critical;1小时未恢复,通知到更高级别的负责人。AlertManager本身不做这套编排,需要通过外部系统对接或AlertManager的webhook扩展实现。如果团队刚刚起步,我的建议是先做好“不重复推送”和“分级接收”这两件事,再考虑复杂升级策略。

我在实际使用中发现,告警工具终究只是把问题摆到人面前,真正让系统稳定的是问题被重视起来。AlertManager的配置本身不难,但把邮件、微信、分组、抑制全链路调到一个让值班同事觉得“舒服”的程度,需要时间打磨。每次故障后回头看那些告警细节,总能发现几个可以调整的地方。告警不是目的,让人能睡个好觉才是。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询