凌晨三点十二分,手机从床头柜上震起来。屏幕上是PagerDuty发来的CRITICAL事件:某个推理服务的内存使用率超过了阈值。我爬起来打开电脑,登录平台一看,服务明明健康得很,只是一次常规的批量预测任务把内存短暂抬了一下。等我把状态确认完、准备躺回去的时候,已经快四点了。这种经历多了之后,团队里的告警已经彻底变成背景噪音。真正出故障的时候,微信群里反而安安静静——因为大家默认"报警了也不用管,多半又是阈值配错了"。那段时间,我们的监控体系形同虚设,直到一次真实的模型推理事故因为没有告警被拖了将近一个小时,我才下定决心重新整理整个报警链路。这篇文章想聊的,就是我在某智能研发AI平台上,基于Alertmanager和PagerDuty搭建报警机制时是怎么一步步把阈值从"拍脑袋"变成"有方法论"的。包括该盯哪些指标、阈值怎么算、Alertmanager里怎么分组抑制、PagerDuty侧怎么映射和升级,以及阈值上线之后如何验证它到底合不合理。如果你也在维护类似的AI平台,或者正在被"狼来了"式的告警折磨,这篇东西应该能给你一些直接能用的思路。1. 报警疲劳背后的真实成本:误报和漏报都不便宜1.1 误报的代价不止是打扰睡眠误报最直接的代价是让团队成员产生"习得性无助"。一开始大家收到电话还会紧张一下,后来发现十次里有八次是虚惊,就开始习惯性忽略。我见过最夸张的一次,一个告警连续触发了一周,响应人每次都只是看一眼就沉默地点击Acknowledge,然后继续做自己的事。等到阈值真的需要调整时,已经没有人愿意去处理了。误报的隐性成本其实更高。每一次被误报打断,意味着当下的事情被迫停下,工作记忆被清空。对于写代码或调模型的人来说,这种打断的代价不是几分钟,而是重新进入状态可能需要半小时。我们后来统计过一个月的告警数据,发现有效告警不到总量的两成。换句话说,团队每个月有八成的时间在为一堆无效事件付出认知成本。1.2 漏报更可怕,因为它是无声的漏报比误报更隐蔽,因为它不会主动打扰你,只会让故障在阴影里发酵。那一次推理事故就是典型:模型版本上线后,一种异常输入导致推理服务的错误率从0.1%涨到8%,但因为我们的报警阈值是按稳态流量设置的,加上告警的持续时间限制设置得太长,这个故障整整运行了57分钟才被人肉巡检发现。事后复盘时,那条告警规则其实一直在触发,但每次只持续两三分钟就恢复了,没有达到for子句里设置的"持续5分钟"门槛。阈值设置得太宽、持续时间设置得太长、没有区分"瞬时波动"和"持续劣化",三个问题叠加,结果就是真实故障被静默吞掉了。所以我在文章开头就把这个观点抛出来:报警机制的优化目标永远是"在误报率和漏报率之间取一个平衡点",而不是单纯地调低某个数字。调低阈值可以减少漏报但会引入大量误报,调高阈值可以减少误报但可能掩盖真实故障。没有一劳永逸的答案,只有适合当前业务状态的配置。2. 先盘点指标再谈阈值:AI研发平台到底该盯什么2.1 从四个方面梳理可观测指标很多人在配告警时一上来就写PromQL,结果盯着一堆CPU、内存指标,配完了发现该漏的还是漏。我的习惯是先把平台上的监控对象按层级拆开,从上到下全部列一遍,再决定哪些指标值得做告警。我负责的这个智能研发AI平台,核心使用者是算法工程师,平台主要承载三件事:模型训练、模型推理、效果评估与版本管理。因此监控指标至少需要分成四层:基础设施层:GPU利用率、显存占用、PCIe链路错误、NPU/GPU温度、节点CPU和内存、磁盘IO和网络吞吐。AI平台和普通业务平台最大的差异在于GPU/NPU资源是最核心的资产,一旦坏卡或驱动异常,大批训练任务会卡住。资源调度层:K8s Pod状态、Pending/Evicted事件、OOMKilled次数、容器重启次数、节点NotReady、GPU算力碎片化程度。这些指标决定了训练任务能否按时跑完。服务推理层:推理请求QPS、P95/P99延迟、错误率、超时率、排队深度、推理引擎的批处理水位。这一层直接面向线上业务,属于用户可感知的指标。模型质量层:模型版本是否漂移、输入数据分布是否偏移、预测置信度是否下降、抽检准确率是否劣化、训练任务心跳是否上报、checkpoint落盘是否超时。这层是AI平台独有的,普通Web监控很难覆盖。我建议把这个清单写进团队的监控规范文档里。先列指标,再标来源(Prometheus exporter、自定义exporter、还是平台自身的上报链路),最后标负责人。避免出现"指标很多但没人说得清哪个是系统核心"的混乱状态。2.2 指标是给你看的,告警是触动你行动的我经常和身边同事强调一个区分:不是所有指标都要配告警。指标用于观测和排障,而告警用于提醒"现在需要有人行动"。一个指标是否值得做告警,我会问三个问题:它劣化时,是否代表用户或业务已经受到了影响?收到告警后,响应人是否有明确的处置动作?如果这个指标持续异常三天没人管,会带来什么实际损失?如果三个问题都答不上来,那这个指标先去dashboard上放着,不要急着接入Alertmanager。比如"GPU利用率低于20%",这个指标在很多团队里被当作一条告警规则。但对AI平台来说,GPU利用率低未必是故障,可能是当前没有足够的推理流量,也可能是训练任务正处于数据加载阶段。这种指标更适合放在监控大屏上供人观察,而不是半夜把人叫醒。2.3 一张可以直接抄的告警指标清单下面这个表格是我在平台上梳理告警规则时用到的清单,它定义了每个监控对象对应的核心指标、告警级别和建议阈值起点。注意这只是起点,具体数值一定要结合你自己的业务数据重新算。监控对象核心指标告警级别建议阈值起点推理服务延迟P95/P99延迟P1基线P95 × 1.5,持续10分钟推理服务错误率5xx与超时占比P2错误率超过0.5%,持续5分钟推理引擎队列排队请求数/等待时间P2队列深度超过50,持续5分钟GPU健康状态温度、NVML错误、卡掉线P1卡掉线或温度超上限,立即训练任务心跳心跳上报间隔P1连续3个周期未上报Pod调度Pending时间、Evicted事件P2Pending超过10分钟内存/显存使用率、OOMKilled次数P2OOMKilled在15分钟内超过3次模型数据漂移PSI、KS值P3漂移指数超过0.1,持续30分钟设置告警级别时我一般遵循一个原则:P1是必须立刻叫醒人的,P2是需要尽快处理的,P3是白天看一眼就行的,P4只是记录不发出通知。级别定得越清晰,后面的PagerDuty配置就越简单。3. 阈值不是拍脑袋定的:三种工程化的阈值设定方法3.1 方法一:基于历史基线的倍数法最基础的阈值设定方法,是把历史正常数据当成基准,再乘以一个安全系数。适合业务相对稳定、有足够历史监控数据的场景。比如推理服务的P95延迟,正常状态下大约在1.2秒左右,偶尔因为批量推理会冲到2秒。那么阈值完全可以定为1.2 × 1.5 = 1.8秒,加上for: 10m作为持续时间限制,既不会对一次性尖峰过敏,又能在持续劣化时及时发出告警。实际操作中,我一般会先看Prometheus里最近30天的数据。用一条log query算出过去一个月的P95延迟作为baseline。histogram_quantile(0.95, sum by (le, service) ( rate(http_request_duration_seconds_bucket{service="inference"}[5m]) ) )然后在告警规则里写成:- alert: InferenceLatencyP95High expr: | histogram_quantile(0.95, sum by (le, service) ( rate(http_request_duration_seconds_bucket{service="inference"}[5m]) ) ) > 1.8 for: 10m labels: severity: critical annotations: summary: "推理服务P95延迟超过1.8秒,持续10分钟"这里有一个关键点:阈值不能只看单点值,还必须要配for子句。for的作用是要求表达式持续为真一段时间才触发告警。没有for,任何一次毛刺都可能把你叫醒。基线倍数法最大的坑是"基线会漂移"。业务增长、模型升级、流量峰值变化都会让历史基线失真。所以我会在每个新版本上线后重新取一次基线,至少每月校准一次。3.2 方法二:动态阈值与3σ区间基线倍数法处理不了强周期、波动大的场景。比如AI平台的在线推理服务,白天流量高、夜里流量低,不同时间段延迟分布差异很大。这时候用固定阈值,要么白天误报频繁,要么夜里漏报严重。我常用的做法是动态阈值:用过去一段时间的滑动窗口计算均值和标准差,把告警阈值设为均值 ± n倍标准差,一般是3σ。这个思路本质上是在回答"当前值和正常波动相比,偏差是否大到不寻常"。下面是一个简化版的思路,用Python从Prometheus拉取历史P95序列,计算动态阈值:import requests import numpy as np # 拉取过去24小时的P95延迟,step=60s q = ('histogram_quantile(0.95, ' 'sum(rate(http_request_duration_seconds_bucket{service="inference"}[5m])) by (le))') resp = requests.get( 'http://prometheus:9090/api/v1/query_range', params={ 'query': q, 'start': <now-24h>, 'end': <now>, 'step': '60s', }, ) vals = [float(v[1]) for v in resp.json()['data']['result'][0]['values']] mean = float(np.mean(vals)) std = float(np.std(vals)) dynamic_threshold = max(mean + 3 * std, 2.0) # 保留一个下界把这个dynamic_threshold写进告警规则,或者通过配置管理工具批量渲染规则文件,就能实现"阈值随业务状态自动调整"。动态阈值并不是银弹。它有几个前提:历史窗口内不能混入已经故障或明显异常的数据,否则均值被污染,阈值就会被拉高;窗口太短,波动大时阈值抖得厉害;窗口太长,对业务变化的响应又太慢。我的建议是窗口取7天或30天,并且排除发布窗口、压测时段和已知故障时段。3.3 方法三:分档阈值,别把所有风险都拉满固定阈值和动态阈值可以组合使用,形成分档体系。同一个指标,我通常会配置warning和critical两档,甚至更多。以推理错误率为例:warning:错误率超过0.5%且持续5分钟,发送到协作群,不呼叫手机。critical:错误率超过2%且持续3分钟,发送到PagerDuty,触发P2及以上级别的呼叫。分档的意义在于让告警的"升级路径"变得清晰。warning阶段是给团队一个观察窗口,如果问题在warning阶段就恢复了,就不需要惊动on-call;如果问题持续恶化,critical才会把人叫醒。这样既保证了告警及时性,又把人力消耗控制在合理水平。设置分档时要注意:两档阈值不要靠得太近。如果warning是0.5%,critical是0.6%,那实际上所有warning都可能瞬间升级成critical,分档就失去了意义。我一般把critical设在warning的3到4倍,并且critical的持续时间可以比warning更短,因为它代表的是一个更紧急的状态。3.4 三种方法怎么选我的选型经验大致如下:场景推荐方法业务量稳定、历史数据充足基线倍数法流量周期性明显、波动大动态阈值需要人工介入的复杂指标分档阈值新上线服务,没有任何历史数据先固定兜底阈值,积累两周后再换任何方法都要有一个"兜底下限"。比如我们给推理延迟设动态阈值时,会保证它不低于2秒。因为即使历史均值很低,也不能让阈值小到一次普通抖动就触发呼叫。兜底值从哪来?从服务SLO倒推,或者直接问业务方:这个指标超过多少,你们可以接受?问出来的值就是兜底下限。4. Alertmanager里的关键调优:分组、抑制和时间窗口4.1 告警从产生到触达的完整链路在配Alertmanager之前,先明确整条链路是怎么一回事。Prometheus负责采集指标和评估告警规则,当表达式满足条件并持续了for指定的时间后,告警进入Alertmanager。Alertmanager拿到告警后,按照路由树匹配标签,做分组、抑制和静默,最后发送到接收器。PagerDuty在Alertmanager里是一个原生接收器。当告警被路由到对应的receiver后,Alertmanager会向PagerDuty的Event API发送事件,PagerDuty根据事件的严重级别和对应服务上的Escalation Policy来决定通知谁。所以阈值只是第一道门槛,真正决定"要不要打扰人"的,还有Alertmanager里的分组、抑制和时间窗口。这个设计非常关键:哪怕你的PromQL阈值设置得很准,如果分组不合理、抑制缺失,告警照样轰炸。4.2 路由与分组:把警报装进合适的"桶"里Alertmanager的路由树决定了每条告警去哪个receiver,分组则决定了哪些告警会被合并成同一条通知。我在这套AI平台上的配置思路是:先按severity分大方向,再按service做分组。critical走PagerDuty,warning走协作群,info只记录不通知。下面是一份简化版配置,配合PagerDuty integration key使用:route: receiver: default group_by: - alertname - cluster - service group_wait: 30s group_interval: 5m repeat_interval: 4h routes: - matchers: - severity = "critical" receiver: pd-critical group_wait: 10s - matchers: - severity = "warning" receiver: pd-warning repeat_interval: 24h inhibit_rules: - source_matchers: - severity = "critical" target_matchers: - severity = "warning" equal: - service - cluster receivers: - name: default webhook_configs: - url: "http://chatbot/webhook/alert" - name: pd-critical pagerduty_configs: - routing_key: "<critical-routing-key>" severity: "{{ .CommonLabels.severity }}" send_resolved: true - name: pd-warning pagerduty_configs: - routing_key: "<warning-routing-key>" severity: "{{ .CommonLabels.severity }}" send_resolved: true在这个配置里,group_by我选择了alertname + cluster + service。这样的话,同一个集群同一个服务的不同告警会合并到一条通知里,而不同服务不会被混在一起。比如某个推理服务同时触发了延迟高和错误率高的告警,会合并成一条消息,而不会让on-call收到两条独立的通知。4.3 抑制规则:别让一次故障变成"全平台的烟花"抑制规则解决的问题是:当高等级告警已经触发时,低等级的同类告警不需要再重复打扰人。举一个实际案例:某个GPU节点发生故障,节点上运行着的所有Pod都会受影响。这时如果节点本身有critical告警,同时又有好几个Pod的延迟告警、调度告警、内存告警,Alertmanager会把这一大堆告警一次性全部发出去。on-call在半夜收到几十条消息,根本分不清主次。抑制规则的作用就是让这些低等级告警暂时闭嘴。上面配置里写的就是:当存在severity=critical的告警时,同一service、同一cluster下的warning告警全部被抑制。等critical被解决,抑制解除,warning如果还在,再按正常逻辑发送。这个规则帮我们把PagerDuty的incident数量从"一次故障几十个"降到了"一次故障三四个",效果非常明显。4.4 时间窗口参数:三个要命的小数Alertmanager里三个时间参数经常被忽略,但它们对报警体验的影响远大于阈值本身。group_wait:第一批告警进入分组后等待多久再发送。目的是等待同一批告警聚齐。我一般设30秒到1分钟,critical可以缩短到10秒。group_interval:组内新增告警后,再次发送的间隔。设5分钟左右比较合理。太短会让一次持续故障刷屏,太长会让新出现的关键告警得不到及时关注。repeat_interval:同一条告警重复通知的间隔。这个参数直接决定了"同一问题多久打扰你一次"。我用过最激进的配置是P1设30分钟,P3设24小时。很多团队只调PromQL阈值,不调这三个参数,结果阈值改得很合理了,实际体验还是差。我建议把这三个参数写进告警评审清单,每次改阈值时顺手检查一遍。5. PagerDuty侧联动:严重级别、轮班和升级策略5.1 从routing_key打通Alertmanager到PagerDutyPagerDuty侧的第一步,是在对应服务下创建一个Event API v2类型的Integration。创建后会生成一个routing_key,这个key就是Alertmanager的pagerduty_configs里需要填的核心凭证。每个PagerDuty Service可以绑定多个Integration,为了保持配置清晰,我给每个告警级别都创建了专用的Integration key,这样在PagerDuty的管理界面里能一眼看出告警是从哪条链路进来的。配置时要注意routing_key属于敏感信息,不要在Alertmanager配置里明文写死。我一般用环境变量或配置管理工具注入。Alertmanager原生支持从环境变量引用敏感字段,比如routing_key: "{{ .Env.PD_CRITICAL_ROUTING_KEY }}"或者用外部配置管理系统渲染,这就避免key泄露到仓库里。5.2 严重级别映射关系Alertmanager告警里的labelseverity如何映射成PagerDuty的严重级别,需要在一开始就定好。我目前使用的是下面这个映射:Alertmanager severityPagerDuty severityPagerDuty通知方式典型场景criticalcritical / P1电话+App推送推理服务不可用、训练任务大面积中断errorerror / P2App推送错误率明显上升、节点故障warningwarning / P3协作群通知资源水位偏高、漂移指数异常infoinfo / P4不通知,仅记录例行状态变更映射规则最好让每个团队都清楚。我的经验是,P1留给那些"现在不处理就会造成业务损失"的事件,P2是"半小时内需要有人看一下",P3和P4无论如何都不应该触发电话通知。如果PagerDuty里P1和P2的占比超过10%,说明阈值严重不合理,需要立刻回头看规则。5.3 Escalation Policy与轮班设计PagerDuty的incident触发后,会先通知当前处于on-call状态的第一响应人。如果第一响应人在设定时间内没有确认,incident会自动升级到下一层。我在AI平台的实践中,Escalation Policy是这么设置的:Level 1:当前on-call工程师,等待10分钟,不确认则升级。Level 2:值班小组负责人,等待10分钟,不确认则升级。Level 3:平台技术负责人,等待10分钟,不确认则再次通知。这里有一个很实际的经验:轮班表上同一时刻只安排一个人作为第一响应人。如果同时安排多个人,每个人都会觉得"别人已经处理了",最后反而没人处理。的确认是通知到了,但责任是模糊的。PagerDuty的on-call轮换支持按人、按团队、按时间来设置,只要保证每个时间片有且仅有一个第一响应人。对于AI平台,我还会额外加一条:训练任务相关告警的on-call要有"算法工程师"角色参与,而不是只安排运维。因为很多训练中断的问题,只有懂模型和训练流程的人才能快速判断,运维接到告警后常常还要二次找人,浪费宝贵的处理时间。5.4 incident的确认、恢复与僵尸告警Alertmanager发送告警到PagerDuty后,会生成一个incident。响应人需要在PagerDuty里点击Acknowledge(确认)表示接管,然后进入处理流程。当Alertmanager里的告警恢复时,会向PagerDuty发送resolve事件,自动关闭incident。这里必须强调一点:send_resolved一定要设成true。如果不设,Alertmanager只在触发时通知PagerDuty,恢复时不会主动关闭incident。结果就是PagerDuty里堆出一堆"僵尸incident",on-call每天光清理这些假事件就够呛,真实的告警反而被淹没。如果你在Alertmanager里配置了group_by,恢复事件是按同一个分组key回传的,所以PagerDuty能正确匹配到对应的incident并关闭它。这也是为什么分组条件必须稳定——如果告警恢复时label和触发时不一致,resolve事件可能匹配不到incident,就会出现"告警还在但让你点关闭"的尴尬情况。6. 阈值上线后的验证闭环:从误报率到告警质量分6.1 用数据说话:误报率和漏报率怎么算阈值调完不是终点,需要在线上验证一段时间。我把告警质量拆成两个可量化的指标:误报率和漏报率。误报率 = 无效incident数量 / 收到的incident总数。无效指的是响应人确认后不需要任何处置的告警。漏报率 = 真实故障中没有任何incident触发的数量 / 全部真实故障数量。真实故障的来源包括用户反馈、业务方汇报、线上日志异常等。这两个指标天然会打架。误报率多了,说明阈值太敏感;漏报率高了,说明阈值太迟钝。我的目标是让误报率低于20%,漏报率低于10%,超过这个范围就需要调整。新阈值上线后的第一周,我会每天在PagerDuty的incident列表里过一遍,标记哪些是"NOOP"(No Operation Needed)。一周之后统计误报率,如果明显偏高,就逐个规则分析原因,是baseline取错了,还是for时间太短,还是分组导致同一个故障重复触发。6.2 告警质量分:让调整有据可依除了误报漏报,我还会对每条告警规则打一个"质量分"。这个评分是我自己拍脑袋总结的,不一定科学,但很有用。评分维度评分标准得分可解释性告警触发时,能清楚说出业务影响1 - 5有处置动作响应人收到后有明确操作步骤1 - 5低频且稳定一周内触发次数不超过3次1 - 5正常恢复率告警触发后自动恢复的比例合理1 - 5总分低于12分的告警规则,我会重新评估是否值得保留。这个评分机制让我们砍掉了好几条"存在感很高但毫无价值"的告警,比如某条"GPU使用率超过90%"的规则,它触发频率高,但实际多数时候是正常的高负载,并没有引发任何故障,后来被改成"GPU使用率超过90%且排队任务数超过阈值"才真正有意义。6.3 调优流程:小步快跑,别一口气全改阈值调优最忌讳一次性大改。我踩过一次很深的坑:某次调整,我同时改了五条核心服务的告警阈值和持续时间,结果上线后误报率从15%飙到60%,整个on-call链路人仰马翻,最后花了两天时间才逐条回滚到接近之前的配置。现在的做法是"每次只改一个变量"。要调延迟阈值,就先只调延迟阈值,不改错误率,也不动for;要动repeat_interval,就单独观察一周。每次改动前,先在测试环境或影子服务上验证一遍,确认表达式返回的数据符合预期,再全量发布配置。发布之后保持至少48小时的观察窗口,期间不叠加新的改动。还有一个判断原则:告警在有疑问时宁可先收紧,不要放松。收紧最多是误报多一点,但至少不会漏掉真实故障;放松一旦过了头,新的漏报风险往往要等真正出事才会暴露,代价太大。复盘那台半夜被误报叫醒的经历,现在看其实一点也不冤枉。那正是整个报警体系失控的信号:阈值凭感觉设置,没有基线依据;Alertmanager分组和抑制完全没调;PagerDuty的升级策略只配了一层,没有任何兜底。后来我们把"阈值从数据里来、告警按分级走、恢复必须有闭环"这三件事变成了硬规矩,报警质量发生了质的改变。如果你也在维护类似的平台,我强烈建议你从自己的告警历史数据里翻一翻,把过去两周的incident全部过一遍,看看有多少是真正值得触发即时通知的。相信我,这个数字会让你立刻有动力动手调优。
凌晨三点十二分,手机从床头柜上震起来。屏幕上是PagerDuty发来的CRITICAL事件:某个推理服务的内存使用率超过了阈值。我爬起来打开电脑,登录平台一看,服务明明健康得很,只是一次常规的批量预测任务把内存短暂抬了一下。等我把状态确认完、准备躺回去的时候,已经快四点了。这种经历多了之后,团队里的告警已经彻底变成背景噪音。真正出故障的时候,微信群里反而安安静静——因为大家默认"报警了也不用管,多半又是阈值配错了"。那段时间,我们的监控体系形同虚设,直到一次真实的模型推理事故因为没有告警被拖了将近一个小时,我才下定决心重新整理整个报警链路。这篇文章想聊的,就是我在某智能研发AI平台上,基于Alertmanager和PagerDuty搭建报警机制时是怎么一步步把阈值从"拍脑袋"变成"有方法论"的。包括该盯哪些指标、阈值怎么算、Alertmanager里怎么分组抑制、PagerDuty侧怎么映射和升级,以及阈值上线之后如何验证它到底合不合理。如果你也在维护类似的AI平台,或者正在被"狼来了"式的告警折磨,这篇东西应该能给你一些直接能用的思路。1. 报警疲劳背后的真实成本:误报和漏报都不便宜1.1 误报的代价不止是打扰睡眠误报最直接的代价是让团队成员产生"习得性无助"。一开始大家收到电话还会紧张一下,后来发现十次里有八次是虚惊,就开始习惯性忽略。我见过最夸张的一次,一个告警连续触发了一周,响应人每次都只是看一眼就沉默地点击Acknowledge,然后继续做自己的事。等到阈值真的需要调整时,已经没有人愿意去处理了。误报的隐性成本其实更高。每一次被误报打断,意味着当下的事情被迫停下,工作记忆被清空。对于写代码或调模型的人来说,这种打断的代价不是几分钟,而是重新进入状态可能需要半小时。我们后来统计过一个月的告警数据,发现有效告警不到总量的两成。换句话说,团队每个月有八成的时间在为一堆无效事件付出认知成本。1.2 漏报更可怕,因为它是无声的漏报比误报更隐蔽,因为它不会主动打扰你,只会让故障在阴影里发酵。那一次推理事故就是典型:模型版本上线后,一种异常输入导致推理服务的错误率从0.1%涨到8%,但因为我们的报警阈值是按稳态流量设置的,加上告警的持续时间限制设置得太长,这个故障整整运行了57分钟才被人肉巡检发现。事后复盘时,那条告警规则其实一直在触发,但每次只持续两三分钟就恢复了,没有达到for子句里设置的"持续5分钟"门槛。阈值设置得太宽、持续时间设置得太长、没有区分"瞬时波动"和"持续劣化",三个问题叠加,结果就是真实故障被静默吞掉了。所以我在文章开头就把这个观点抛出来:报警机制的优化目标永远是"在误报率和漏报率之间取一个平衡点",而不是单纯地调低某个数字。调低阈值可以减少漏报但会引入大量误报,调高阈值可以减少误报但可能掩盖真实故障。没有一劳永逸的答案,只有适合当前业务状态的配置。2. 先盘点指标再谈阈值:AI研发平台到底该盯什么2.1 从四个方面梳理可观测指标很多人在配告警时一上来就写PromQL,结果盯着一堆CPU、内存指标,配完了发现该漏的还是漏。我的习惯是先把平台上的监控对象按层级拆开,从上到下全部列一遍,再决定哪些指标值得做告警。我负责的这个智能研发AI平台,核心使用者是算法工程师,平台主要承载三件事:模型训练、模型推理、效果评估与版本管理。因此监控指标至少需要分成四层:基础设施层:GPU利用率、显存占用、PCIe链路错误、NPU/GPU温度、节点CPU和内存、磁盘IO和网络吞吐。AI平台和普通业务平台最大的差异在于GPU/NPU资源是最核心的资产,一旦坏卡或驱动异常,大批训练任务会卡住。资源调度层:K8s Pod状态、Pending/Evicted事件、OOMKilled次数、容器重启次数、节点NotReady、GPU算力碎片化程度。这些指标决定了训练任务能否按时跑完。服务推理层:推理请求QPS、P95/P99延迟、错误率、超时率、排队深度、推理引擎的批处理水位。这一层直接面向线上业务,属于用户可感知的指标。模型质量层:模型版本是否漂移、输入数据分布是否偏移、预测置信度是否下降、抽检准确率是否劣化、训练任务心跳是否上报、checkpoint落盘是否超时。这层是AI平台独有的,普通Web监控很难覆盖。我建议把这个清单写进团队的监控规范文档里。先列指标,再标来源(Prometheus exporter、自定义exporter、还是平台自身的上报链路),最后标负责人。避免出现"指标很多但没人说得清哪个是系统核心"的混乱状态。2.2 指标是给你看的,告警是触动你行动的我经常和身边同事强调一个区分:不是所有指标都要配告警。指标用于观测和排障,而告警用于提醒"现在需要有人行动"。一个指标是否值得做告警,我会问三个问题:它劣化时,是否代表用户或业务已经受到了影响?收到告警后,响应人是否有明确的处置动作?如果这个指标持续异常三天没人管,会带来什么实际损失?如果三个问题都答不上来,那这个指标先去dashboard上放着,不要急着接入Alertmanager。比如"GPU利用率低于20%",这个指标在很多团队里被当作一条告警规则。但对AI平台来说,GPU利用率低未必是故障,可能是当前没有足够的推理流量,也可能是训练任务正处于数据加载阶段。这种指标更适合放在监控大屏上供人观察,而不是半夜把人叫醒。2.3 一张可以直接抄的告警指标清单下面这个表格是我在平台上梳理告警规则时用到的清单,它定义了每个监控对象对应的核心指标、告警级别和建议阈值起点。注意这只是起点,具体数值一定要结合你自己的业务数据重新算。监控对象核心指标告警级别建议阈值起点推理服务延迟P95/P99延迟P1基线P95 × 1.5,持续10分钟推理服务错误率5xx与超时占比P2错误率超过0.5%,持续5分钟推理引擎队列排队请求数/等待时间P2队列深度超过50,持续5分钟GPU健康状态温度、NVML错误、卡掉线P1卡掉线或温度超上限,立即训练任务心跳心跳上报间隔P1连续3个周期未上报Pod调度Pending时间、Evicted事件P2Pending超过10分钟内存/显存使用率、OOMKilled次数P2OOMKilled在15分钟内超过3次模型数据漂移PSI、KS值P3漂移指数超过0.1,持续30分钟设置告警级别时我一般遵循一个原则:P1是必须立刻叫醒人的,P2是需要尽快处理的,P3是白天看一眼就行的,P4只是记录不发出通知。级别定得越清晰,后面的PagerDuty配置就越简单。3. 阈值不是拍脑袋定的:三种工程化的阈值设定方法3.1 方法一:基于历史基线的倍数法最基础的阈值设定方法,是把历史正常数据当成基准,再乘以一个安全系数。适合业务相对稳定、有足够历史监控数据的场景。比如推理服务的P95延迟,正常状态下大约在1.2秒左右,偶尔因为批量推理会冲到2秒。那么阈值完全可以定为1.2 × 1.5 = 1.8秒,加上for: 10m作为持续时间限制,既不会对一次性尖峰过敏,又能在持续劣化时及时发出告警。实际操作中,我一般会先看Prometheus里最近30天的数据。用一条log query算出过去一个月的P95延迟作为baseline。histogram_quantile(0.95, sum by (le, service) ( rate(http_request_duration_seconds_bucket{service="inference"}[5m]) ) )然后在告警规则里写成:- alert: InferenceLatencyP95High expr: | histogram_quantile(0.95, sum by (le, service) ( rate(http_request_duration_seconds_bucket{service="inference"}[5m]) ) ) > 1.8 for: 10m labels: severity: critical annotations: summary: "推理服务P95延迟超过1.8秒,持续10分钟"这里有一个关键点:阈值不能只看单点值,还必须要配for子句。for的作用是要求表达式持续为真一段时间才触发告警。没有for,任何一次毛刺都可能把你叫醒。基线倍数法最大的坑是"基线会漂移"。业务增长、模型升级、流量峰值变化都会让历史基线失真。所以我会在每个新版本上线后重新取一次基线,至少每月校准一次。3.2 方法二:动态阈值与3σ区间基线倍数法处理不了强周期、波动大的场景。比如AI平台的在线推理服务,白天流量高、夜里流量低,不同时间段延迟分布差异很大。这时候用固定阈值,要么白天误报频繁,要么夜里漏报严重。我常用的做法是动态阈值:用过去一段时间的滑动窗口计算均值和标准差,把告警阈值设为均值 ± n倍标准差,一般是3σ。这个思路本质上是在回答"当前值和正常波动相比,偏差是否大到不寻常"。下面是一个简化版的思路,用Python从Prometheus拉取历史P95序列,计算动态阈值:import requests import numpy as np # 拉取过去24小时的P95延迟,step=60s q = ('histogram_quantile(0.95, ' 'sum(rate(http_request_duration_seconds_bucket{service="inference"}[5m])) by (le))') resp = requests.get( 'http://prometheus:9090/api/v1/query_range', params={ 'query': q, 'start': <now-24h>, 'end': <now>, 'step': '60s', }, ) vals = [float(v[1]) for v in resp.json()['data']['result'][0]['values']] mean = float(np.mean(vals)) std = float(np.std(vals)) dynamic_threshold = max(mean + 3 * std, 2.0) # 保留一个下界把这个dynamic_threshold写进告警规则,或者通过配置管理工具批量渲染规则文件,就能实现"阈值随业务状态自动调整"。动态阈值并不是银弹。它有几个前提:历史窗口内不能混入已经故障或明显异常的数据,否则均值被污染,阈值就会被拉高;窗口太短,波动大时阈值抖得厉害;窗口太长,对业务变化的响应又太慢。我的建议是窗口取7天或30天,并且排除发布窗口、压测时段和已知故障时段。3.3 方法三:分档阈值,别把所有风险都拉满固定阈值和动态阈值可以组合使用,形成分档体系。同一个指标,我通常会配置warning和critical两档,甚至更多。以推理错误率为例:warning:错误率超过0.5%且持续5分钟,发送到协作群,不呼叫手机。critical:错误率超过2%且持续3分钟,发送到PagerDuty,触发P2及以上级别的呼叫。分档的意义在于让告警的"升级路径"变得清晰。warning阶段是给团队一个观察窗口,如果问题在warning阶段就恢复了,就不需要惊动on-call;如果问题持续恶化,critical才会把人叫醒。这样既保证了告警及时性,又把人力消耗控制在合理水平。设置分档时要注意:两档阈值不要靠得太近。如果warning是0.5%,critical是0.6%,那实际上所有warning都可能瞬间升级成critical,分档就失去了意义。我一般把critical设在warning的3到4倍,并且critical的持续时间可以比warning更短,因为它代表的是一个更紧急的状态。3.4 三种方法怎么选我的选型经验大致如下:场景推荐方法业务量稳定、历史数据充足基线倍数法流量周期性明显、波动大动态阈值需要人工介入的复杂指标分档阈值新上线服务,没有任何历史数据先固定兜底阈值,积累两周后再换任何方法都要有一个"兜底下限"。比如我们给推理延迟设动态阈值时,会保证它不低于2秒。因为即使历史均值很低,也不能让阈值小到一次普通抖动就触发呼叫。兜底值从哪来?从服务SLO倒推,或者直接问业务方:这个指标超过多少,你们可以接受?问出来的值就是兜底下限。4. Alertmanager里的关键调优:分组、抑制和时间窗口4.1 告警从产生到触达的完整链路在配Alertmanager之前,先明确整条链路是怎么一回事。Prometheus负责采集指标和评估告警规则,当表达式满足条件并持续了for指定的时间后,告警进入Alertmanager。Alertmanager拿到告警后,按照路由树匹配标签,做分组、抑制和静默,最后发送到接收器。PagerDuty在Alertmanager里是一个原生接收器。当告警被路由到对应的receiver后,Alertmanager会向PagerDuty的Event API发送事件,PagerDuty根据事件的严重级别和对应服务上的Escalation Policy来决定通知谁。所以阈值只是第一道门槛,真正决定"要不要打扰人"的,还有Alertmanager里的分组、抑制和时间窗口。这个设计非常关键:哪怕你的PromQL阈值设置得很准,如果分组不合理、抑制缺失,告警照样轰炸。4.2 路由与分组:把警报装进合适的"桶"里Alertmanager的路由树决定了每条告警去哪个receiver,分组则决定了哪些告警会被合并成同一条通知。我在这套AI平台上的配置思路是:先按severity分大方向,再按service做分组。critical走PagerDuty,warning走协作群,info只记录不通知。下面是一份简化版配置,配合PagerDuty integration key使用:route: receiver: default group_by: - alertname - cluster - service group_wait: 30s group_interval: 5m repeat_interval: 4h routes: - matchers: - severity = "critical" receiver: pd-critical group_wait: 10s - matchers: - severity = "warning" receiver: pd-warning repeat_interval: 24h inhibit_rules: - source_matchers: - severity = "critical" target_matchers: - severity = "warning" equal: - service - cluster receivers: - name: default webhook_configs: - url: "http://chatbot/webhook/alert" - name: pd-critical pagerduty_configs: - routing_key: "<critical-routing-key>" severity: "{{ .CommonLabels.severity }}" send_resolved: true - name: pd-warning pagerduty_configs: - routing_key: "<warning-routing-key>" severity: "{{ .CommonLabels.severity }}" send_resolved: true在这个配置里,group_by我选择了alertname + cluster + service。这样的话,同一个集群同一个服务的不同告警会合并到一条通知里,而不同服务不会被混在一起。比如某个推理服务同时触发了延迟高和错误率高的告警,会合并成一条消息,而不会让on-call收到两条独立的通知。4.3 抑制规则:别让一次故障变成"全平台的烟花"抑制规则解决的问题是:当高等级告警已经触发时,低等级的同类告警不需要再重复打扰人。举一个实际案例:某个GPU节点发生故障,节点上运行着的所有Pod都会受影响。这时如果节点本身有critical告警,同时又有好几个Pod的延迟告警、调度告警、内存告警,Alertmanager会把这一大堆告警一次性全部发出去。on-call在半夜收到几十条消息,根本分不清主次。抑制规则的作用就是让这些低等级告警暂时闭嘴。上面配置里写的就是:当存在severity=critical的告警时,同一service、同一cluster下的warning告警全部被抑制。等critical被解决,抑制解除,warning如果还在,再按正常逻辑发送。这个规则帮我们把PagerDuty的incident数量从"一次故障几十个"降到了"一次故障三四个",效果非常明显。4.4 时间窗口参数:三个要命的小数Alertmanager里三个时间参数经常被忽略,但它们对报警体验的影响远大于阈值本身。group_wait:第一批告警进入分组后等待多久再发送。目的是等待同一批告警聚齐。我一般设30秒到1分钟,critical可以缩短到10秒。group_interval:组内新增告警后,再次发送的间隔。设5分钟左右比较合理。太短会让一次持续故障刷屏,太长会让新出现的关键告警得不到及时关注。repeat_interval:同一条告警重复通知的间隔。这个参数直接决定了"同一问题多久打扰你一次"。我用过最激进的配置是P1设30分钟,P3设24小时。很多团队只调PromQL阈值,不调这三个参数,结果阈值改得很合理了,实际体验还是差。我建议把这三个参数写进告警评审清单,每次改阈值时顺手检查一遍。5. PagerDuty侧联动:严重级别、轮班和升级策略5.1 从routing_key打通Alertmanager到PagerDutyPagerDuty侧的第一步,是在对应服务下创建一个Event API v2类型的Integration。创建后会生成一个routing_key,这个key就是Alertmanager的pagerduty_configs里需要填的核心凭证。每个PagerDuty Service可以绑定多个Integration,为了保持配置清晰,我给每个告警级别都创建了专用的Integration key,这样在PagerDuty的管理界面里能一眼看出告警是从哪条链路进来的。配置时要注意routing_key属于敏感信息,不要在Alertmanager配置里明文写死。我一般用环境变量或配置管理工具注入。Alertmanager原生支持从环境变量引用敏感字段,比如routing_key: "{{ .Env.PD_CRITICAL_ROUTING_KEY }}"或者用外部配置管理系统渲染,这就避免key泄露到仓库里。5.2 严重级别映射关系Alertmanager告警里的labelseverity如何映射成PagerDuty的严重级别,需要在一开始就定好。我目前使用的是下面这个映射:Alertmanager severityPagerDuty severityPagerDuty通知方式典型场景criticalcritical / P1电话+App推送推理服务不可用、训练任务大面积中断errorerror / P2App推送错误率明显上升、节点故障warningwarning / P3协作群通知资源水位偏高、漂移指数异常infoinfo / P4不通知,仅记录例行状态变更映射规则最好让每个团队都清楚。我的经验是,P1留给那些"现在不处理就会造成业务损失"的事件,P2是"半小时内需要有人看一下",P3和P4无论如何都不应该触发电话通知。如果PagerDuty里P1和P2的占比超过10%,说明阈值严重不合理,需要立刻回头看规则。5.3 Escalation Policy与轮班设计PagerDuty的incident触发后,会先通知当前处于on-call状态的第一响应人。如果第一响应人在设定时间内没有确认,incident会自动升级到下一层。我在AI平台的实践中,Escalation Policy是这么设置的:Level 1:当前on-call工程师,等待10分钟,不确认则升级。Level 2:值班小组负责人,等待10分钟,不确认则升级。Level 3:平台技术负责人,等待10分钟,不确认则再次通知。这里有一个很实际的经验:轮班表上同一时刻只安排一个人作为第一响应人。如果同时安排多个人,每个人都会觉得"别人已经处理了",最后反而没人处理。的确认是通知到了,但责任是模糊的。PagerDuty的on-call轮换支持按人、按团队、按时间来设置,只要保证每个时间片有且仅有一个第一响应人。对于AI平台,我还会额外加一条:训练任务相关告警的on-call要有"算法工程师"角色参与,而不是只安排运维。因为很多训练中断的问题,只有懂模型和训练流程的人才能快速判断,运维接到告警后常常还要二次找人,浪费宝贵的处理时间。5.4 incident的确认、恢复与僵尸告警Alertmanager发送告警到PagerDuty后,会生成一个incident。响应人需要在PagerDuty里点击Acknowledge(确认)表示接管,然后进入处理流程。当Alertmanager里的告警恢复时,会向PagerDuty发送resolve事件,自动关闭incident。这里必须强调一点:send_resolved一定要设成true。如果不设,Alertmanager只在触发时通知PagerDuty,恢复时不会主动关闭incident。结果就是PagerDuty里堆出一堆"僵尸incident",on-call每天光清理这些假事件就够呛,真实的告警反而被淹没。如果你在Alertmanager里配置了group_by,恢复事件是按同一个分组key回传的,所以PagerDuty能正确匹配到对应的incident并关闭它。这也是为什么分组条件必须稳定——如果告警恢复时label和触发时不一致,resolve事件可能匹配不到incident,就会出现"告警还在但让你点关闭"的尴尬情况。6. 阈值上线后的验证闭环:从误报率到告警质量分6.1 用数据说话:误报率和漏报率怎么算阈值调完不是终点,需要在线上验证一段时间。我把告警质量拆成两个可量化的指标:误报率和漏报率。误报率 = 无效incident数量 / 收到的incident总数。无效指的是响应人确认后不需要任何处置的告警。漏报率 = 真实故障中没有任何incident触发的数量 / 全部真实故障数量。真实故障的来源包括用户反馈、业务方汇报、线上日志异常等。这两个指标天然会打架。误报率多了,说明阈值太敏感;漏报率高了,说明阈值太迟钝。我的目标是让误报率低于20%,漏报率低于10%,超过这个范围就需要调整。新阈值上线后的第一周,我会每天在PagerDuty的incident列表里过一遍,标记哪些是"NOOP"(No Operation Needed)。一周之后统计误报率,如果明显偏高,就逐个规则分析原因,是baseline取错了,还是for时间太短,还是分组导致同一个故障重复触发。6.2 告警质量分:让调整有据可依除了误报漏报,我还会对每条告警规则打一个"质量分"。这个评分是我自己拍脑袋总结的,不一定科学,但很有用。评分维度评分标准得分可解释性告警触发时,能清楚说出业务影响1 - 5有处置动作响应人收到后有明确操作步骤1 - 5低频且稳定一周内触发次数不超过3次1 - 5正常恢复率告警触发后自动恢复的比例合理1 - 5总分低于12分的告警规则,我会重新评估是否值得保留。这个评分机制让我们砍掉了好几条"存在感很高但毫无价值"的告警,比如某条"GPU使用率超过90%"的规则,它触发频率高,但实际多数时候是正常的高负载,并没有引发任何故障,后来被改成"GPU使用率超过90%且排队任务数超过阈值"才真正有意义。6.3 调优流程:小步快跑,别一口气全改阈值调优最忌讳一次性大改。我踩过一次很深的坑:某次调整,我同时改了五条核心服务的告警阈值和持续时间,结果上线后误报率从15%飙到60%,整个on-call链路人仰马翻,最后花了两天时间才逐条回滚到接近之前的配置。现在的做法是"每次只改一个变量"。要调延迟阈值,就先只调延迟阈值,不改错误率,也不动for;要动repeat_interval,就单独观察一周。每次改动前,先在测试环境或影子服务上验证一遍,确认表达式返回的数据符合预期,再全量发布配置。发布之后保持至少48小时的观察窗口,期间不叠加新的改动。还有一个判断原则:告警在有疑问时宁可先收紧,不要放松。收紧最多是误报多一点,但至少不会漏掉真实故障;放松一旦过了头,新的漏报风险往往要等真正出事才会暴露,代价太大。复盘那台半夜被误报叫醒的经历,现在看其实一点也不冤枉。那正是整个报警体系失控的信号:阈值凭感觉设置,没有基线依据;Alertmanager分组和抑制完全没调;PagerDuty的升级策略只配了一层,没有任何兜底。后来我们把"阈值从数据里来、告警按分级走、恢复必须有闭环"这三件事变成了硬规矩,报警质量发生了质的改变。如果你也在维护类似的平台,我强烈建议你从自己的告警历史数据里翻一翻,把过去两周的incident全部过一遍,看看有多少是真正值得触发即时通知的。相信我,这个数字会让你立刻有动力动手调优。