
凌晨2点47分手机在床头柜上连续震了三下。我眯着眼睛解锁屏幕Alertmanager推送显示PRODUCTION环境FastAPI服务的5xx错误率超过了5%。披上外套坐到书房打开Grafana错误率曲线在发布后的三分钟里确实跳了一下但现在已经归零了。人被吵醒之后想再入睡基本是奢望。这种场景我经历过不止一次每次复盘都会发现吵醒我的告警和真正需要处理的问题之间往往隔着一道巨大的鸿沟。FastAPI项目上线之后告警是躲不掉的必修课。今天不聊理论就从半夜被吵醒这个最真实的痛点出发把告警从指标采集、规则配置到通知分发的整条链路完整捋一遍。1. 先承认吧告警吵醒你的那一刻系统已经赢了1.1 被吵醒不等于服务挂了告警疲劳的代价那次半夜告警的最终结论是发布流程里健康检查把流量摘错了一小段失败请求触发了错误率阈值。服务本身没有任何问题但那个晚上已经被毁掉了。类似的故事在每个团队都反复发生区别只是被吵醒的人不同。我后来总结过一个很朴素的规律一次虚假告警的代价远不止你损失的二十分钟睡眠。它真正可怕的地方在于会慢慢消耗团队对告警的信任。人是很现实的动物连续被假警报折腾三次之后再响警报时第一反应不再是出事了而是又来吧估计又是误报。这种心态一旦形成当真实故障发生时响应速度会慢到你无法想象。告警疲劳才是运维体系里最贵的成本。所以我在给团队设计告警方案时永远把减少无意义告警放在扩大监控覆盖前面。监控覆盖不足最多是漏报漏报在大多数时候不会吵醒你告警规则失控是一群狼来了它会让整个值班体系失去意义。1.2 快速复盘FastAPI项目告警失控的三个典型原因接手过几个FastAPI项目之后我发现告警让人半夜崩溃的原因高度雷同基本逃不出下面三个第一个原因监控覆盖要么太薄要么太乱。很多项目上线初期只在Uvicorn日志里看请求记录没有指标采集没有错误追踪。等线上出问题时你手里只有服务好像挂了这一个信息连是请求量暴涨还是数据库超时都说不清楚。反过来也见过一上来就装七八个exporter、把默认告警规则全部打开的内存使用率偶尔跳到50%也要发一条消息结果重要告警被淹没在一堆无关消息里。第二个原因阈值靠拍脑袋定而不是靠数据算。错误率超过5%就告警这种规则看起来很专业但实际运行中可能会天天误报。QPS只有个位数的时候一次爬虫抓取、一条慢请求就能把错误率瞬间拉到50%。低峰期和高峰期同一个阈值代表的意义完全不同不区分场景的阈值一定会误伤你。第三个原因告警没有分级也没有恢复通知。P0级别的核心故障和P3级别的磁盘空间不足提醒发到同一个群、通知同一批人。值班的人看到一条P3告警根本不知道要不要处理。更坑的是告警恢复了也不通知大家各自盯着屏幕猜现在到底好了没有。这三个问题不解决后面接什么工具都白搭。告警不是装个Prometheus再挂个Alertmanager就完事它是一个需要反复打磨的系统。2. 画一张告警地图FastAPI服务到底需要盯住什么要设计告警先得知道自己的系统里有哪些东西值得盯。很多人的第一反应是盯服务挂没挂但这个认知恰恰是半夜被吵醒的根源。服务没挂不代表业务正常服务挂了也不一定立刻要人去处理。2.1 区分探活与就绪/healthz和/readyz别混用FastAPI项目最常见的做法是写一个/healthz接口返回200就认为服务还活着。但这里有个概念必须掰扯清楚进程活着不等于能正常处理请求。如果你的服务依赖数据库、Redis、消息队列而数据库连不上了进程本身还活着/healthz照样返回200。这时候Prometheus抓取没问题探活正常但实际请求进来会大量报错。所以我一直建议FastAPI项目至少暴露两个端点/healthz负责进程存活/readyz负责依赖就绪检查。from fastapi import FastAPI, HTTPException from sqlalchemy import text app FastAPI() app.get(/healthz) def healthz(): # 进程还活着就返回200Kubernetes livenessProbe打这里 return {status: alive} app.get(/readyz) def readyz(): # 检查依赖是否就绪Kubernetes readinessProbe打这里 try: with engine.connect() as conn: conn.execute(text(SELECT 1)) except Exception: raise HTTPException(status_code503, detaildatabase not ready) return {status: ready}这两个端点配合Kubernetes的探针使用逻辑是完全不同的。liveness失败会触发容器重启readiness失败只会把Pod从Service的Endpoints里摘掉不再接新流量。如果混用一个接口一旦数据库故障探活失败导致容器被反复重启故障范围反而会扩大。这一点在写告警规则之前一定要先理顺。2.2 性能与错误延迟、QPS、5xx率要一起看第二个要盯的是接口性能。单看QPS没有意义单看错误率也没有意义两个指标要放在一起才有价值。QPS从100涨到1000错误率同步上升说明是流量压垮了系统QPS没变错误率飙升说明是代码或依赖出了问题。延迟指标我会用分位数而不是平均值。平均响应时间是最容易骗人的指标一次超时10秒的请求就能把平均值拉得很高但p95可能只有200毫秒。通常盯住p50表示普通用户的体感p95代表大多数慢请求的上限p99代表极端情况。FastAPI项目有个比较特殊的坑如果你用同步def定义路由FastAPI会把它们丢到线程池里执行默认线程池的大小有限通常40个左右。一旦某个同步依赖发生长时间阻塞线程池被占满后续接口会出现排队表现出来就是延迟一路飙升但CPU和内存看着都不高。这种故障光盯资源指标是会漏掉的必须把延迟和错误率纳入告警地图。另外注意4xx错误一般不要设置成告警规则。客户端传了非法参数返回400这是业务正常逻辑跟系统故障无关。真正要告警的是5xx和延迟恶化。有些时候跨域配置CORS错误会导致浏览器端请求全部失败但服务端可能连5xx都不会出现从服务端指标看一切正常用户却一直反馈页面打不开。这种看不见的错误靠HTTP指标发现不了要么做前端埋点要么用外部拨测模拟真实用户请求属于另一个话题但排查思路要提前有数。2.3 资源水位CPU、内存与连接池第三层是基础设施资源。CPU、内存、磁盘、网络这些指标很多团队用现成exporter就能采到但要注意别把阈值定成默认值。CPU使用率不是超过80%就必须告警。如果一台机器部署了多个容器或者业务有明显的峰谷特性瞬时CPU高可能只是正常的业务波动。我自己的习惯是看持续时长和负载曲线单核CPU持续3分钟超过85%同时load average超过核数的2倍这才值得告警。内存也一样FastAPI进程无故上涨往往不是故障本身而是代码泄漏的信号需要结合重启次数看趋势而不是看到内存高就立即打扰值班人。比CPU和内存更容易翻车的是连接池。FastAPI服务实例本身可以随时重启但数据库连接池、Redis连接池一旦被打满整个系统都会跟着抖。我见过不止一次线上事故根因是某个慢SQL占满了数据库连接然后所有依赖数据库的接口全部超时。所以告警地图里一定要包含数据库活跃连接数、等待连接数、Redis连接数这几个外围指标。它们看起来不属于应用本身却往往是压垮应用的最后一根稻草。2.4 业务指标服务活着但业务已经崩了最后这一层很多团队会忽略但它恰恰是最有告警价值的。服务进程活着、HTTP状态码全是200、延迟也正常不代表业务正常。比如支付回调失败、消息队列积压、订单下单失败率飙升这些业务层面的坏事在HTTP层根本看不出来。业务指标需要你们自己埋点把关键业务的成功和失败次数暴露出来。埋点越具体后续告警规则越好写。如果你现在手头接了一个FastAPI项目我建议在正式设计告警规则前先和业务方梳理一遍哪些事件发生代表系统实际上已经不正常了。比如用户下单接口失败率超过5%、消息队列积压量超过阈值、定时任务连续失败3次把这些清单列出来再进入下一步。3. 把指标暴露出来FastAPI接入Prometheus的完整姿势告警地图画完之后下一步是把这些指标真正暴露出来。FastAPI本身不提供/metrics端点但接Prometheus生态有现成方案不需要从零写埋点中间件。3.1 一条命令接入基础监控我用得最多的是prometheus-fastapi-instrumentator这个库安装和接入都非常简单pip install prometheus-fastapi-instrumentatorfrom fastapi import FastAPI from prometheus_fastapi_instrumentator import Instrumentator app FastAPI(titledemo) Instrumentator().instrument(app).expose(app) app.get(/hello) def hello(): return {message: hello}启动服务后访问/metrics就能看到Prometheus格式的指标。这个库本质上就是往FastAPI里塞了一个中间件自动记录每个请求的方法、路径、状态码、耗时最后聚合成Counter和Histogram类型暴露出来。一条命令接入基础监控对项目初期非常友好。curl -s http://127.0.0.1:8000/metrics | head -30看到类似http_requests_total、http_request_duration_seconds_bucket的指标名就说明采集成功。这里提醒一句不同版本的库暴露的指标名和标签名有细微差异比如状态码标签可能是status也可能是status_code。写PromQL规则之前一定先拉一下实际输出以你项目里安装的版本为准。3.2 自定义业务指标把订单失败变成可告警数据基础监控只能告诉你HTTP层的健康状况业务指标还是得自己埋。我用Prometheus客户端库直接在FastAPI里注册Counter和Histogramfrom prometheus_client import Counter, Histogram ORDER_FAILURES Counter( order_failures_total, 订单处理失败次数按原因拆分, [reason], ) PAYMENT_DURATION Histogram( payment_duration_seconds, 支付接口调用耗时, buckets(0.1, 0.3, 0.5, 1, 2, 5), ) app.post(/orders) async def create_order(): try: # 业务逻辑 pass except TimeoutError: ORDER_FAILURES.labels(reasonupstream_timeout).inc() raiseCounter适合统计累计次数比如失败次数、请求总数画错误率时用rate()函数算增量和比例。Histogram适合统计耗时分布比如支付接口的响应时间后面用histogram_quantile算p95、p99。埋点设计上有一个经验labels要敢拆但不能无脑拆。按失败原因拆label告警时能直接告诉你上游超时还是参数错误但如果把用户ID、订单ID这种高基数信息放进labelPrometheus会直接被拖垮。一个label的取值如果有几十种那可以接受如果有几十万种请把它放到日志系统里去分析。3.3 多进程部署与/metrics安全最容易翻车的两个细节FastAPI项目在生产环境通常会用gunicorn拉起多个Uvicorn worker来提升并发能力这时会遇到一个很多人踩过的坑prometheus_client的指标默认存在单进程内存里多worker模式下每个worker各自维护一份Counter抓取到的数据会互相覆盖或者只反映某一个worker的状态。解决思路是给prometheus_client开启多进程模式设置prometheus_multiproc_dir环境变量指向一个共享目录让所有worker把指标数据写到同一个目录下抓取时再聚合。同时要注意多进程模式下不能再使用Gauge中的set()方法只能使用inc()、dec()等方式具体限制需要翻阅你用的prometheus_client版本文档。另一个容易忽略的问题是/metrics端点的暴露范围。这个端点会暴露你所有的接口路径、请求量、状态码分布如果直接裸奔到公网别人能顺着接口路径猜出你的业务结构。最简单的方式是让Prometheus走内网抓取不要把这个端口映射到公网如果架构上实在没法隔离至少在nginx层给/metrics加一层BasicAuth。安全这件事不用做到多复杂但完全裸奔肯定不行。4. 告警规则的艺术怎么让PromQL不再狼来了指标已经采集上来了接下来是整条链路里最考验功力的一环告警规则怎么写。规则写得粗凌晨被吵醒规则写得细漏掉真实故障。这里面的平衡点我拆成三块来讲。4.1 for持续时间是给你的冷静期PromQL里每一条告警规则都可以设置for它表示这个指标异常状态持续多长时间之后才真正触发告警。很多团队写规则时直接跳过for结果就是一个瞬时抖动也会立即告警。低峰期的错误率暴涨往往来去很快。一条慢SQL拖垮了接口三秒钟错误率飞到50%但五秒钟之后又恢复正常了。这种情况如果没有冷静期你半夜就醒了如果设置了for: 5mPrometheus会等异常持续5分钟后再决定是否通知这次抖动就不会打扰任何人。但冷静期也不是越长越好。for设成30分钟意味着故障已经影响了半小时用户才有人响应这对核心业务来说太晚了。我的经验是分场景设置核心接口的5xx错误率for: 2m延迟恶化和非核心错误for: 10m资源类趋势告警for: 15m以上。告警的价值在于及时性冷静期过长会让规则形同虚设。4.2 一套能落地的Prometheus告警规则下面给一套完整的告警规则文件包含服务不可用、5xx错误率、延迟高三个最常见的场景groups: - name: fastapi-rules rules: - alert: FastAPIInstanceDown expr: up{jobfastapi} 0 for: 2m labels: severity: page annotations: summary: FastAPI服务不可达 description: 实例 {{ $labels.instance }} 已经2分钟无法抓取请立即检查。 - alert: FastAPIErrorRateHigh expr: | ( sum(rate(http_requests_total{jobfastapi, status~5..}[5m])) / clamp_min(sum(rate(http_requests_total{jobfastapi}[5m])), 1e-9) ) 0.05 for: 10m labels: severity: page annotations: summary: FastAPI 5xx错误率超过5% description: 当前错误率 {{ $value | humanizePercentage }}持续时间超过10分钟。 - alert: FastAPIHighLatency expr: | histogram_quantile( 0.95, sum by (le) (rate(http_request_duration_seconds_bucket{jobfastapi}[5m])) ) 1 for: 15m labels: severity: warning annotations: summary: FastAPI p95延迟超过1秒 description: 最近5分钟p95延迟为 {{ $value }}s持续15分钟未恢复。几个细节解释一下。第一5xx错误率要用rate()除以总请求量这比直接用5xx次数做阈值更科学因为它考虑了流量大小。第二分母上包了一层clamp_min(..., 1e-9)防止低峰期总请求量接近0时除零。第三histogram_quantile计算分位数时必须先用sum by (le)把多个实例的bucket数据合并直接对着单实例的histogram算会漏掉全局视图。这套规则作为一个起点完全够用后续根据业务再往里面加磁盘、连接池、业务指标。在prometheus.yml里把规则文件引用进来rule_files: - /etc/prometheus/rules/*.yml4.3 阈值是算出来的不是拍出来的阈值怎么定是整个告警体系里最主观的一个环节。拍脑袋定出来的阈值几乎都会在某个凌晨以一种你完全没想到的方式反噬你。我的习惯是拿历史数据说话。Prometheus本身存了时间序列Grafana可以方便地查看过去30天的p99、均值、最大值。比如某个接口的p95延迟平均值是200毫秒峰值偶尔到500毫秒那么把告警阈值设在1秒就是合理的反过来如果你连这个接口的历史数据都没看过直接写个500毫秒很有可能会被正常的业务波动反复触发。更进阶一点的做法是用相对阈值。对于流量波动明显的业务绝对值超过多少告警其实不太合理。相对阈值可以这样写过去24小时的中位数是基线如果当前指标超过基线3倍且持续10分钟才触发告警。这种规则对缓慢增长型故障特别有效比如磁盘使用率持续线性增长你可以在它还有24小时才用完的时候就收到趋势预警而不是等到磁盘满了才被通知。另外有一条很重要的个人经验不要只依赖Prometheus规则去兜底配置类错误。FastAPI项目的配置读取如果写得不健壮比如配置文件里少了个冒号、环境变量没加载到服务启动时可能不会立刻报错等流量进来才开始大量失败。这种问题应该通过启动时的配置校验直接拦截而不是等到凌晨让告警来告诉你。把配置校验前置到CI或启动阶段远比事后加告警规则舒服。5. Alertmanager不是转发工具而是告警的闸门Prometheus负责发现异常但怎么把告警发出去、发给谁、要不要合并、要不要静默这些事归Alertmanager管。很多人把它当成一个简单的Webhook转发器那就大材小用了。它的三个核心能力正好能解决半夜被吵醒的大半问题。5.1 路由树让告警找到该找的人Alertmanager的路由配置可以看作一棵树。进来的每一条告警会按你定义的规则一路往下匹配最终进入某个receiver。我最常用的分法是按severity标签分P0级别打电话P1级别推APPP2级别发IM群。route: receiver: default group_by: [alertname] group_wait: 30s group_interval: 5m repeat_interval: 12h routes: - matchers: - severity page receiver: oncall-phone - matchers: - severity warning receiver: dev-wechat receivers: - name: oncall-phone webhook_configs: - url: http://your-alerting-platform/hook/oncall - name: dev-wechat webhook_configs: - url: http://your-alerting-platform/hook/wechat配置里的matchers是Alertmanager较新版本推荐的写法老版本用match和match_re如果还在用老版本语法上要做一点转换。路由的意义在于让告警精准地到达需要处理它的人手里而不是所有人收到所有消息。后端的同学不需要半夜被磁盘告警吵醒值班的SRE也不需要在工作群里看几百条接口抖动消息。5.2 group_wait和group_interval告警风暴的灭火器告警风暴是大促和故障时的典型场景数据库一挂依赖它的20个FastAPI实例同时报错如果不做任何聚合Alertmanager会在几分钟内把几十条消息砸向所有人。group_by: [alertname]的作用是让同一个告警规则产生的重复告警合并成一条。group_wait: 30s的意思是某条告警第一次触发后先等30秒把30秒内到达的同组其他告警攒在一起再统一发送。group_interval: 5m控制的是新告警加入已有分组的等待时间。这组参数配好一个数据库故障产生的风暴通常会变成一条FastAPI 5xx错误率超过5%的消息而不是五十条。repeat_interval: 12h经常被忽略但它恰恰是半夜被反复吵醒的关键。它的意思是同一个告警如果一直没恢复多久后再通知一次。默认值可能只有几小时意味着一个持续到天亮的故障会在半夜反复轰炸你。我建议非紧急级别的告警把这个值调到12小时以上紧急级别的调到1小时左右让值班人知道这个故障还在持续但不至于被消息淹没。5.3 silence与inhibit维护窗口和依赖故障的官方静音半夜发布是常态发布期间必然会出现一些暂时性的告警比如服务重启导致的抓取失败、错误率瞬时抖动。你不可能频繁改规则去适配发布窗口这时要用静默机制。Alertmanager提供Silence API可以在特定时间段内屏蔽匹配的告警。发布前执行一条命令curl -X POST http://localhost:9093/api/v2/silences \ -H Content-Type: application/json \ -d { matchers: [ {name: alertname, value: FastAPIErrorRateHigh, isRegex: false} ], startsAt: 2025-01-01T00:00:00Z, endsAt: 2025-01-01T04:00:00Z, createdBy: devops, comment: 发布窗口静默 }这里有个小坑API里的时间是UTC国内服务要记得换算时区不然静默窗口会比你预期少8个小时。inhibit是另一个容易被忽视的功能它的作用是如果某个根因告警已经触发那么由它引起的连带告警就不再发送。比如PostgreSQL挂了所有依赖数据库的FastAPI服务必然出现一连串连接超时告警但这些告警对值班人来说只是噪音真正需要处理的是数据库本身。配置如下inhibit_rules: - source_matchers: - alertname PostgresDown target_matchers: - severity warning意思是一旦PostgresDown这个告警已经触发所有severity为warning的告警都被抑制。这样值班人先看到根因不会受到几十条下游告警的干扰。但抑制规则要克制条件设得过宽真实故障也会被一起吞掉。我建议抑制规则只在明确知道上下游依赖关系的告警之间使用不要全局一刀切。6. 彻底根治半夜吵醒值班体系的最后一公里工具链已经齐了但很多团队发现还是会半夜被吵醒问题出在流程上。告警的最终归宿是人人怎么被通知、怎么升级、事后怎么复盘这套机制不建立起来再漂亮的规则配置都白搭。6.1 值班升级为什么不能一上来就打电话一个很常见的错误设计是任何告警都直接给值班人打电话。结果就是半夜2点被一个磁盘空间不足的warning叫醒而这东西明明明天上午处理就来得及。电话应该留给机器已经判断为严重故障且无人认领的场景而不是所有场景。比较合理的升级链路是这样告警触发后先推到APP和IM群值班人在规定时间内确认ACK并开始处理如果超时没人确认才升级到电话呼叫电话还没人接再升级到上一级负责人。这种设计既保证故障有人响应又不会让一两个人被低级别告警耗死。告警分级也要对应不同的响应时限级别典型场景通知方式响应时限P0核心服务完全不可用、支付失败电话APP推送15分钟P1错误率持续偏高、延迟明显恶化APP推送IM群1小时P2磁盘增长、证书即将过期IM群消息工作时间处理P0级别的告警数量应该被严格控制。我自己心里的底线是P0每月的触发次数如果超过5次这个系统本身就不健康该做稳定性改造而不是继续靠人扛。6.2 接收渠道怎么选IM、APP、电话各干一摊接收渠道的选择直接关系到告警会不会被淹没。很多团队一开始图省事把所有告警都推到同一个企业微信群里结果重要告警被普通聊天刷上去等发现时故障已经持续半小时了。渠道噪音水平半夜唤醒能力适合场景IM机器人企微/钉钉/飞书高弱P2、普通通知、告警记录Alertmanager APP推送中中P1、值班确认电话/短信网关低强P0、升级链路IM群适合当告警的留痕区不适合当唯一的通知手段。APP推送适合值班人主动关注但依赖个人设置有些人开了免打扰就收不到。电话是成本最高的方式一般通过短信网关或者专门的告警平台接入只留给P0。如果你所在团队还没有告警平台先用Alertmanager的企业微信/钉钉机器人加一个简单的值班表系统也能跑起来关键是让不同级别的告警走不同渠道而不是一个群收全部。6.3 每周复盘告警质量把误报率当成团队KPI告警规则的优化不是一次性工作。我见过一个团队规则上线后三个月没动过业务流量翻了十倍但阈值还是当初拍脑袋定的那个数字结果就是平台天天误报最终整个团队对告警彻底麻木。我的习惯是每周花半小时复盘告警记录。看三个数这周一共触发了多少条告警其中真正需要人工介入的有几条误报和可自愈的有几条。所谓有效告警率就是用确实需要人处理的告警数除以触发总数。我一般希望这个比例在80%以上。如果低于50%说明告警规则大概率是在制造噪音应该调阈值、加冷静期或者干脆删掉这条规则。复盘时也要记录每次半夜告警的根因。之前有一次线上半夜告警抖动查了半天发现是配置中心下发的一份配置文件缺少关键字段服务启动失败后又被负载均衡摘除恢复后马上又摘除形成了告警抖动。从那以后我们把启动阶段配置校验列入了开发规范在应用初始化时对必填配置做强校验加载不到就快速失败而不是带病启动。这种事情靠告警规则是兜不住的只能靠流程改进。6.4 自愈优先机器能处理的不要麻烦人最后说一个方向性的思路告警体系的终点不是把规则越堆越多而是把常见故障自动化处理掉让留给人的告警都必须是需要人来决策的问题。Kubernetes的liveness探针保证了进程假死会被自动重启readiness探针保证了不健康的Pod不会接流量HPA可以按负载自动扩容连接池饱和时可以用限流和降级来保护系统而不是等着告警。这些机制能吸收掉很大一部分半夜出事但等人上去处理时已经自己恢复了的情况。我经常跟团队说如果一个月下来某条告警触发了十次但每次等你登录服务器的时候问题都已经消失了那这条告警就该考虑改成自动处理而不是继续消耗人的精力。告警体系做得好的标志不是什么都被监控到而是需要人处理的东西越来越少。当你误报率降下去、P0数量控制在个位数、自愈机制覆盖掉大部分常见故障之后半夜被吵醒这件事基本就只剩两种情况要么是确实需要你爬起来处理的事故要么是你很久没维护告警规则了。按这个标准去要求自己的系统比用什么花哨的工具都管用。最后再分享一个我个人的操作习惯每季度重新评估一遍所有告警规则的阈值和for时长结合线上的真实数据和业务变化做调整。一个团队如果一个月收到的有效告警不到三条这系统才是真的健康。半夜被吵醒这种事没法完全避免但能做到每一次被吵醒都值得这套体系就算过关了。