监控全绿却故障不断?Prometheus告警规则与SLI/SLO设计指南
发布时间:2026/10/21 8:48:56 锦皓数字建站

凌晨两点半手机震动把我从沙发上拽了起来。不是告警群是客服的私聊订单页面转圈很久了是不是后端挂了我眯着眼打开Grafana来回切了十几个面板——Prometheus监控里CPU、内存、磁盘、网络全部显示正常连错误率曲线都平得像一条直线。但用户在客户端那边就是提交不了订单。这种监控一切正常用户却说系统崩了的场面我见过不止一次自己也踩过。很多人第一反应是Prometheus坏了、Alertmanager漏了通知但绝大多数情况是我们监控的东西、聚合的维度、定义的阈值和用户真实体验之间有一条巨大的认知鸿沟。这篇文章就用几个我实际复盘过的场景聊聊为什么仪表盘是绿的业务却是崩的以及怎么从监控设计上把这个坑填掉。1. 监控面板全绿但用户骂娘三个高频事故的真实拆解先说结论绝大多数监控绿、业务崩的案子翻来覆去都是同几个模式。把模式看懂了后面所有改进也就顺理成章。1.1 场景一CPU内存全绿数据库连接池却没连接了有个订单系统大促期间用户反馈下单一直转圈。我打开监控MySQL实例的CPU只有25%内存用了60%磁盘IO毫无压力网络也平缓。从任何一张基础设施监控图看这套系统都健康得不能再健康了。但应用日志里全是connection timeout from connection pool。真正卡住的地方不在MySQL本身而在应用侧到MySQL的那层连接池。连接池被一批慢查询占满新请求拿不到连接在应用线程里排队用户体感就是提交没反应。而MySQL实例层面的指标还在岁月静好地波动着。这就是最典型的组件健康不等于链路健康。我们监控的是数据库这个组件用户关心的是我这个操作能不能完成。中间那层连接池、线程池、队列深度才是决定体验的关键但它们经常不在监控范围内。这类问题排查时我会优先看应用暴露出来的连接池指标比如Java侧的hikaricp_connections_active和hikaricp_connections_pending配合PromQL去算连接池使用率。hikaricp_connections_active{joborder-service} / on(job) hikaricp_connections_max{joborder-service} 0.85这个规则比任何MySQL实例级别的指标都更能反映用户能否拿到连接。后续我还见过不少团队把Redis、MQ的客户端线程池指标也加进来效果都很直接。1.2 场景二平均延迟200毫秒P99却飙到5秒另一个高频场景是接口监控。某个支付服务的看板上平均响应时间220ms看起来完美。但真正打用户投诉电话时对方说转圈转了十几秒。问题出在平均这个统计口径上。大部分请求确实在100ms左右完成但少量请求因为GC停顿、慢SQL或者下游抖动耗时直接冲到5秒、8秒。这些长尾请求只占1%却把用户耐心和业务转化率按在地上摩擦。平均值被大量快请求稀释了长尾的疼痛完全看不出来。正确的做法是看百分位数。Prometheus里用histogram_quantile计算P99histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{jobpayment-service}[5m])) by (le) )P99超过1秒就该告警。我一般同时保留P50、P95、P99三个指标分别代表一般体验、大多数人体验和最差的那批人体验。看监控时先看P99再回头对比平均值如果两者差距超过三倍说明服务一定存在明显的长尾问题哪怕平均值再漂亮也是隐患。1.3 场景三HTTP 200全正常业务码却一直在报错这个场景最隐蔽也最容易让值班人员当场怀疑人生。网关看板上某个接口的成功率99.99%5xx计数几乎为零——因为所有响应都返回HTTP 200。但前端页面弹的是系统繁忙请稍后再试后端JSON里写的是{code: 5001, message: internal error}。HTTP层协议是通的监控系统只看状态码的话永远不知道业务层已经炸了。这种问题靠Prometheus默认采集是抓不到的必须从业务埋点下手。要么在应用里显式记录业务成功/失败计数要么对响应体做语义校验。我后面会专门讲业务指标怎么进监控这里先记住一条HTTP状态码只是网络层的健康信号不是业务层的健康信号。把HTTP 200当一切正常迟早要吃亏。2. Prometheus拉取模型与指标维度那些被设计出来的盲区看完三个事故场景会发现它们有一个共同点不是Prometheus没采到数据而是采到的数据看起来正常。但Prometheus自身的架构机制也藏着一些容易让监控静默失效的坑。2.1 pull模型与采样间隔瞬态故障是怎么被错过的Prometheus默认每15秒拉一次指标这是一种采样不是全量记录。这意味着两次拉取之间发生的短暂故障很可能根本不会出现在任何一张图上。我遇到过一个典型案例某个服务的健康检查偶尔会失败进程被快速重启。重启过程只有几秒刚好落在两次scrape之间采集到的序列几乎看不到异常最后是用户投诉了半小时我们才定位到。当时Prometheus上确实有一个up指标等于0的瞬时抖动但告警规则里压根没写up 0于是这个信号就白白躺在库里了。如果业务对连续性要求高或者已经出现过瞬态故障可以把关键target的scrape_interval从15s缩短到5s、10s代价是Prometheus自身的查询压力变大需要提前做好容量评估。更优雅的方法是用Recording rule把高频采集降维成预聚合结果告警直接查预聚合数据既保留精度又不拖累查询性能。2.2 抓取失败等于静默为什么没有数据也是事故信号很多告警规则都是针对指标超过阈值而写的比如CPU超过90%或者磁盘超过85%。但有一种更可怕的情况某个target彻底挂了Prometheus抓取不到任何数据曲线直接断掉。此时阈值型告警可能全部静默——因为不存在超过阈值这个事件只有没有数据。而Grafana面板上只是多了一条断线如果没有任何规则盯着它故障就会被无声无息地吞掉。这就是为什么up和absent这两类告警必须存在。最基本的规则up 0如果某个服务实例消失这条规则立刻触发。再配合absent处理整个服务完全不存在的场景absent(process_start_time_seconds{joborder-service})我见过不只一个团队基础大盘画得漂漂亮亮但整个up系列告警是空的。等真出事故值班人才发现原来监控没报警不是没事是压根没抓到数据。补上up告警是我做监控改造时永远排第一位的动作成本极低收益却非常大。2.3 聚合粒度与维度曲线正常可能只是因为被平均了PromQL的聚合函数特别容易误用。最常见的是avg。比如5个实例的请求成功率某个实例一个小时内错误率飙到30%但其他4个实例都在99%以上avg一算整体曲线可能还是98.8%阈值设在95%的话这条规则一辈子不会触发。处理这类问题聚合函数要按场景选。要看最差的那台机器是否扛得住就用max要看是不是所有机器都正常就用min。比如CPU使用率这种资源类指标用max by (instance)更合理。而成功率、错误率这类服务级指标建议按服务维度做贴合业务口径的求和不要笼统地按全部实例平均。另外标签维度也值得想想。如果一个服务同时跑着多个版本或者不同机房混合部署聚合时把version、az标签丢掉某一段用户群体已经被故障影响时全局曲线可能依然平稳。聚合粒度越粗掩盖细节的能力越强这是监控设计时绕不开的取舍。3. 告警规则三个陷阱阈值拍脑袋、聚合掩盖、for选择困难就算指标采全了、探针也加了告警规则本身设计不当照样会在故障当天装死。3.1 阈值拍脑袋同一个阈值对不同服务含义完全不同CPU超过80%就告警这类拍脑袋阈值在真实系统中经常误伤。一个批处理任务的CPU常年顶着90%跑完全正常一个常驻的API服务平时只有20%某天突然冲到60%反而可能是异常上涨。正确思路有两个方向。一个是按服务特征分别设阈值把批处理、常驻服务、定时任务分开管理另一个是引入相对基线——当前值是否显著偏离历史同期水平。相对基线在PromQL里写起来稍复杂需要结合过去7天同时间窗口的历史数据做对比但它的判断能力远强于静态阈值。写了规则之后一定要用promtool校验promtool check rules rules.yml promtool test rules rules-test.ymlpromtool test rules支持用样例数据模拟规则触发很适合在改完告警后快速验证效果。很多团队重来没跑过这两条命令规则文件里语法错误都上线了告警自然也无法触发。3.2 for持续时间怎么选告警噪声和响应延迟的拔河Prometheus告警规则里的for字段表示条件连续满足多久才告警。设太短比如1分钟一个3秒的GC抖动就会触发告警值班人员的手机一晚上响个不停形成狼来了效应设太长比如30分钟等告警发出来用户已经在社交平台骂了半小时。我的经验是分级处理。资源饱和度类告警可以容忍较长确认时间for: 10m错误率突增、P99严重超时这类直接影响用户体验的for: 1m都嫌长再配合Alertmanager里的路由规则把不同紧急程度的告警分给不同的通知渠道。比如紧急故障立刻打电话预警类发到值班群。这样既避免噪声淹没真信号又能保证真故障被快速响应。3.3 单指标告警的局限学会用组合条件缩小误报单指标规则最大的问题是指标正常时也可能出事以及指标异常时未必是用户受影响。比如CPU升高可能只是某个后台任务在跑用户毫无感知。按单一指标告警要么漏报要么误报。更接近真相的写法是组合条件错误率升高而且延迟升高同时成立才触发。比如API服务的复合告警( sum(rate(http_requests_total{jobapi, code~5..}[5m])) / sum(rate(http_requests_total{jobapi}[5m])) ) 0.01 and histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{jobapi}[5m])) by (le)) 1这种错误率超过1%且P99超过1秒的组合意味着用户确实在经历大量慢请求和失败基本不会误报。我自己把所有高优告警都朝这个方向改了告警数量至少降一半准确率明显提升。4. 监控的本质是回答用户现在好不好用RED/USE、SLI与合成监控我们把坑都数了一遍现在该说正面方案。核心是要把监控视角从基础设施挪到用户侧让正常这两个字有明确的业务含义。4.1 RED 与 USE基础设施健康不等于服务健康监控领域有两套常用的指标方法论。USE面向资源RED面向服务。USE是Utilization、Saturation、Errors的缩写适合监控基础设施——利用率、饱和程度、错误数。比如CPU使用率、内存饱和度、磁盘错误计数这些回答的是机器和组件的健康状态。RED是Rate、Errors、Duration的缩写适合监控服务——请求速率、错误数、延迟分布。比如QPS、5xx比例、P99响应时间这些回答的是服务提供的体验质量。两者都要有但很多团队只做了USERED几乎是空白。前面说的连接池耗尽、P99长尾、HTTP 200业务错误全是USE健康但RED崩溃的例子。改进监控第一步就是给每个核心服务补齐RED指标。R和E通常可以直接从HTTP中间件或网关日志提取D则需要引入histogram类指标这几个东西都属于没有也能跑有了才能定位的关键数据。4.2 SLI/SLO落地把正常定义成可量化的承诺一切正常之所以有争议是因为压根没有定义什么叫正常。SLI是用户可感知的服务水平指标SLO是对这个指标给出的目标承诺。比如SLI下单接口成功率SLO最近30天成功率 ≥ 99.9%SLI下单接口P99延迟SLOP99延迟 ≤ 500msSLI/SLO在Prometheus里的实现并不复杂。先把核心SLI做成Recording rule比如groups: - name: slo.rules rules: - record: job:api:error_rate_5m expr: | sum(rate(http_requests_total{jobapi, code~5..}[5m])) / sum(rate(http_requests_total{jobapi}[5m]))然后用这个预聚合结果做错误预算类告警。当30天内的错误率已经逼近SLO允许的边界时提前通知团队而不是等用户全部炸了才反应。错误预算的核心思想是允许一部分故障发生但故障累积到一定程度就必须停下来处理技术债。有了SLI/SLO监控正常就变成一个可回答的问题——不是CPU低于80%就算正常而是过去30天的成功率是否还满足SLO。这一层认知转变比任何工具配置都值钱。4.3 合成监控让Prometheus主动模拟用户的每一步被动采集再全也只能看到系统自身暴露出来的视角。用户看到的页面前端、APP、第三方回调链路我们常常一无所知。合成监控就是为了补上这一块——从外部主动发起请求模拟真实用户的完整路径。Prometheus里做这件事的常用工具是blackbox_exporter。可以配置HTTP探针不只是检查端口通不通而是检查状态码、响应时间甚至响应体内容。比如检查支付回调接口是否返回携带特定业务码的成功响应modules: http_payment_callback: prober: http http: valid_http_versions: [HTTP/1.1, HTTP/2] valid_status_codes: [200] fail_if_body_not_matching_regexp: - code:\s*200把这套探针配置到Prometheus的采集任务里让它每30秒发起一次真实请求配合告警规则链路一旦变慢或返回异常就能在用户感知之前收到报警。对关键用户路径比如登录、加购、下单、支付回调都值得各配一个探针。成本很低收益却是从用户视角验证系统而不是从服务器视角猜测用户状态。4.4 业务指标进监控从只看到系统状态到看到业务状态很多团队的基础设施监控已经做得很到位但问一句今天成交了多少单、支付成功率是多少没人答得上来。订单量下跌、支付失败率升高这些业务指标才是用户体感最直接的映射。系统指标正常、业务指标崩塌的情况前面已经见过多次。业务指标的接入方式通常有三种。第一种是在应用进程里用Prometheus client暴露自定义指标比如用Python的prometheus_clientfrom prometheus_client import Counter, start_http_server import random, time order_created Counter(order_created_total, Total orders created) order_failed Counter(order_failed_total, Total failed orders, [reason]) if __name__ __main__: start_http_server(9101) while True: if random.random() 0.99: order_created.inc() else: order_failed.labels(reasonpayment_timeout).inc() time.sleep(1)第二种是从日志流里提取指标用工具把关键字匹配转换为计数。第三种是定期查询业务数据库的表计数或状态分布再写入一个exporter。不管用哪种方式只要把核心业务成功了多少、失败了多少变成可查询的指标告警规则就有了业务语义。比如最近5分钟下单失败率超过1%和最近5分钟CPU超过90%哪个更贴近用户感受答案一目了然。5. 一个订单系统的完整复盘从没告警反推到监控缺口前面讲的都是方法论最后用一个具体案例把这些串起来。这个案例我做过脱敏处理但排查链路和监控改造方案完全来自真实线上问题。5.1 现象与第一轮排查所有基础指标都正常晚上八点左右某订单系统进入晚高峰。用户反馈提交订单一直转圈什么都点不动。值班同事立刻打开Grafana逐一检查Prometheus监控里的核心面板API网关的请求量正常、5xx为零订单服务的CPU、内存、磁盘、网络全部平稳。连MySQL的慢查询数都没有明显变化。第一轮排查持续了十分钟值班同事几乎要得出可能是用户端网络问题的结论。但此时客服群里已经有更多用户出现同样反馈看着不像偶发。这个阶段暴露的问题是没有任何一个面板展示用户提交订单这个动作的成功率。系统太健康了以至于我们不知道该怀疑谁。5.2 第二轮排查从入口日志一路追到线程池既然大面板看不出问题只能翻日志。先看Nginx入口日志发现请求量没有减少但upstream_response_time大面积超过3秒。再往网关看耗时主要集中在payment服务上。继续翻payment服务的错误日志满屏都是调用风控接口超时。查到这里怀疑对象落到风控服务上。风控服务的CPU只有5%内存也正常但深入一看它的线程池活跃线程已经打满请求队列在不断堆积。线程池满的原因是最近上线了一套更复杂的规则集单个请求的处理耗时翻了好几倍吞吐量骤降CPU反而因为大量线程阻塞没有跑起来。整个链路梳理完真相是风控服务处于半瘫痪状态它自己不报错但下游所有依赖它的服务都在苦苦等待。而我们的监控体系里既没有风控服务线程池的指标也没有payment服务的P99延迟更没有订单提交失败率这类业务指标。于是Prometheus里所有的曲线都保持正常直到用户替我们发现故障。5.3 复盘后的监控改造与告警清单这次事故之后我帮该团队做了四件事。第一给核心服务补齐RED指标。每个服务必须暴露请求速率、错误率和延迟分布延迟只认P95和P99不认平均值。第二接入业务成功率指标。订单服务的导出端点上新增order_created_total和order_failed_total计数器并把订单提交失败率超过1%设为高优告警。第三给关键外部依赖增加合成探针。模拟真实用户发起支付回调探测请求验证HTTP状态码和响应体里的业务码再配一条探针失败告警。第四调整告警规则。把原先一把抓的CPU阈值替换成错误率上升同时P99上升的复合条件补上up 0和absent规则并为不同服务按基线设置差异化阈值。改造后Prometheus监控里一切正常代表的含义才真正靠近用户的真实体验。后来该团队再做例行故障演练时故意把风控服务的线程池打满告警在几分钟内准时触发值班人员第一时间就锁定了链路位置。我自己踩过太多次监控绿、业务崩的坑现在对每个新上线的接口都习惯性问三个问题用户完成一次操作,什么状态才算成功这个操作失败时监控能不能感知如果不能那故障最晚多久才会被发现定期做一点故障演练主动杀掉某个下游服务看看告警能否如约而至也是我一直推荐的做法。监控最终要回答的不是CPU用了多少而是用户此刻到底好不好用。前者只是手段后者才是意义。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。