ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AlertManager告警通知实战:邮件与企业微信配置全解析

AlertManager告警通知实战:邮件与企业微信配置全解析 前阵子凌晨两点多我放在床头的手机开始不停震动。打开一看是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.26CentOS 7和Ubuntu 22.04都跑过。AlertManager就是个二进制文件从官网下载.tar.gz解压或者用容器跑都行没有特殊依赖。需要准备的东西如下Prometheus服务端一台即可负责采集和规则评估。AlertManager服务默认端口9093负责通知。企业微信后台的管理员权限用于创建自建应用、获取CorpID和AgentId。一个可用的SMTP发送账号比如企业邮箱、163邮箱等用于邮件告警。组件之间的连通关系是Prometheus把告警POST到AlertManager的APIAlertManager根据route配置决定走哪个receiverreceiver再调用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{modeidle}[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: alertexample.com smtp_auth_username: alertexample.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-teamexample.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端口导致邮件发不出去。建议优先使用465SSL或587STARTTLS这两个端口一般不会被封。举几个常见邮箱的SMTP配置参考邮箱类型SMTP服务器端口加密方式网易163smtp.163.com465SSL腾讯企业邮smtp.exmail.qq.com465SSLQQ邮箱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-teamexample.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已经触发告警了但邮箱里什么都没有”。排查链路我建议按这个顺序走先看AlertManager日志启动时加--log.leveldebug可以看到smtp请求的详细返回。如果日志显示connection refused基本是端口被封或服务器不可达。检查smtp_auth_password是否填对很多邮箱的“密码”是单独的客户端授权码不是登录密码。163、QQ邮箱都需要先去网页端开启SMTP服务并生成授权码。确认From、AuthUsername是否一致不一致会在EHLO后直接报535错误。如果日志显示发送成功但收件箱没有去垃圾箱翻一翻。大量监控邮件很容易被反垃圾规则拦截可以在邮箱里设置白名单把发件地址加入白名单。我还遇到过一种情况邮件发到团队共享邮箱结果邮箱容量满了新邮件一直退信。这种问题日志里能看见退信原因是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服务基于Flaskimport 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, paramsparams, timeout5).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, jsonmsg, timeout5).json() return json.dumps(resp), 200, {Content-Type: application/json} if __name__ __main__: app.run(host0.0.0.0, port8080)代码逻辑不复杂接收AlertManager的JSON提取关键字段拼成一段文字然后调用企业微信API发到配置的可见成员。这里的touser用的是all实际使用时可以改成具体成员的UserID或者部门ID避免无关人员被告警轰炸。4.4 用systemd托管转发服务并验证转发服务是用Flask写的部署时不建议直接前台跑我用systemd托了一下[Unit] DescriptionAlertManager WeCom Forwarder Afternetwork-online.target [Service] EnvironmentWECOM_CORPIDxxx EnvironmentWECOM_AGENTID1000002 EnvironmentWECOM_SECRETxxx ExecStart/usr/bin/python3 /opt/wechat-forwarder/app.py Restartalways RestartSec5 [Install] WantedBymulti-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设30sgroup_interval设5mrepeat_interval根据级别区分。warning级别可以是6hcritical级别可以是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跑在容器里容器的时区是UTCLocal取到的还是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提升到critical1小时未恢复通知到更高级别的负责人。AlertManager本身不做这套编排需要通过外部系统对接或AlertManager的webhook扩展实现。如果团队刚刚起步我的建议是先做好“不重复推送”和“分级接收”这两件事再考虑复杂升级策略。我在实际使用中发现告警工具终究只是把问题摆到人面前真正让系统稳定的是问题被重视起来。AlertManager的配置本身不难但把邮件、微信、分组、抑制全链路调到一个让值班同事觉得“舒服”的程度需要时间打磨。每次故障后回头看那些告警细节总能发现几个可以调整的地方。告警不是目的让人能睡个好觉才是。
返回列表