Prometheus告警规则模板库:从零搭建到生产调优实战
1. 为什么我坚持维护一套自己的告警规则模板库
做运维和SRE这些年,我经手过的监控系统少说也有十几套了。每次新环境搭建、每次迁移、每次接手别人的集群,最耗时间的事情往往不是装Prometheus、不是配Grafana,而是告警规则到底该怎么写。装软件半小时搞定,规则调优能折腾你三个月。这个项目标题说的“常用prometheus告警规则模板”,本质上就是把我这些年反复写、反复改、反复踩坑之后沉淀下来的那批规则,整理成一套可以直接拿来用、也可以按需裁剪的模板集合。
先说清楚它是什么。这是一套面向Prometheus生态的告警规则模板,覆盖主机、容器、中间件、数据库、应用层这几个最常见的监控维度,用PromQL表达,以YAML格式组织,可以直接通过rule_files加载到Prometheus里,也可以配合Alertmanager做路由分发。它能解决的核心问题是:让你在搭建完监控采集之后,不用从零开始想“该告什么警”。很多人采集指标配得很全,但告警规则要么空着,要么只配了个“实例挂了”,结果真出问题时要么没告警、要么告警风暴,值班的人被淹没在噪音里。
这套模板适合谁?我觉得三类人最用得上。第一类是刚接触Prometheus的运维或开发,手上有一套新集群,指标已经采上来了,但不知道哪些指标值得告、阈值该定多少,拿模板当起点最省事。第二类是已经有告警但噪音很大的团队,可以拿这套模板做对照,看看自己的规则是不是缺了for、是不是聚合维度写错了、是不是该用rate的地方用了increase。第三类是做标准化建设的,比如要给多个业务线统一告警规范,这套模板可以作为基线,各业务在此基础上做增量。
需要提前说明一点:模板不是拿来即用的银弹。每个环境的业务特征、流量模型、硬件规格都不一样,阈值必须结合实际情况调。我在下面会把每条规则背后的判断逻辑讲清楚,你理解了“为什么这么写”,改起来才不会心虚。这也是我写这篇东西的核心目的——给的是思路和骨架,不是让你闭眼复制。
2. 整体设计与规则组织思路拆解
2.1 为什么按“层次+对象”来组织规则文件
规则文件怎么拆分,这个看似是小事,实际上直接影响后期维护成本。我见过把所有规则塞进一个alerts.yml的做法,几百行下去,改一条规则要滚半天,多人协作时冲突不断。也见过按告警级别拆的,critical.yml、warning.yml各一个文件,听起来合理,但实际用起来很别扭——同一个对象的严重告警和警告往往要一起看、一起调,拆开反而割裂。
我最终采用的是按监控对象分层的组织方式,大致是这样:
rules/ host/ # 主机层:CPU、内存、磁盘、负载、文件句柄 container/ # 容器层:Pod状态、重启、资源限制、OOM middleware/ # 中间件:各类消息队列、缓存、网关 database/ # 数据库:连接数、慢查询、复制延迟、磁盘 application/ # 应用层:请求量、错误率、延迟、饱和度 meta/ # 元监控:采集目标存活、规则评估自身健康这么拆的理由很实在。第一,故障排查天然是按层走的——主机挂了先看主机,容器异常先看容器,定位到中间件再看中间件,规则文件和排查路径对齐,找起来快。第二,权限和职责能对上——主机规则通常由基础架构团队维护,应用规则由业务团队维护,文件边界清晰,Code Review和变更审批都好划分。第三,复用粒度合适——新上一个中间件,把对应目录下的模板拷过来改改就行,不用在几百行里挑挑拣拣。
每个目录内部,我习惯再按“资源维度”细分,比如主机层里CPU、内存、磁盘各成一组。这样单文件不会太长,通常控制在80到150行之间,读起来不累。
2.2 告警分级与for时长的取舍逻辑
告警级别我固定用三档:critical、warning、info。这个分级不是为了好看,而是直接对应Alertmanager的路由策略和值班响应。critical意味着“现在就得有人起来处理”,走电话或强提醒;warning是“工作时间处理,或者观察趋势”;info基本是记录性质,进工单不进值班。
比级别更容易被忽视、但影响更大的是for字段。for决定了一个条件要持续多久才真正触发告警,它是抑制抖动的最重要手段。我踩过最典型的坑是:CPU使用率规则没写for,或者只写了30s,结果一次正常的批处理任务跑起来,CPU瞬间冲到90%,告警就响了,值班的人半夜被叫醒,一看是定时任务,气得不行。
我的经验取值是这样的:
| 告警类型 | 建议for | 理由 |
|---|---|---|
| 实例存活类 | 1m | 采集本身有间隔,太短会误报,太长发现慢 |
| 资源使用率类 | 5m~10m | 过滤瞬时峰值,反映真实持续压力 |
| 错误率类 | 2m~5m | 兼顾灵敏度和抗抖动 |
| 容量趋势类 | 30m~1h | 趋势问题不急,看的是长期走向 |
| 配置/状态类 | 1m | 状态异常通常需要尽快知道 |
这里有个容易被忽略的细节:for的计时是从表达式第一次为真开始算的,如果中间有一次评估为假,计时会重置。所以对于波动大的指标,光靠for不够,还得配合avg_over_time之类的聚合函数先把曲线平滑掉,再交给for判断。这两者是配合关系,不是二选一。
2.3 阈值为什么不能照抄,以及怎么定
模板里我会给一个默认阈值,但我必须强调:默认值只是起点,不是答案。原因很简单,一台4核8G的机器和一台64核256G的机器,CPU告警阈值能一样吗?一个IO密集型和CPU密集型的服务,内存水位线能一样吗?
我定阈值的习惯做法是“三步走”。第一步,先让指标裸跑一到两周,不做任何告警,把数据拉出来看分布——P50、P95、P99各是多少,峰值出现在什么时段。第二步,以P99为参考,往上留出合理余量作为warning线,再往上留出更大余量作为critical线。第三步,上线后持续观察误报率,误报多的往上调,漏报的往下调,一般迭代两三轮就稳了。
举个具体例子。某服务的请求延迟,P99稳定在200ms左右,偶发峰值到400ms。那warning可以定在500ms、for 5m,critical定在1s、for 2m。这样正常波动不会触发,真出问题时又能及时抓到。如果直接抄一个“延迟超过100ms就告警”的模板,那这个服务天天告警,规则很快就没人看了——没人看的告警等于没有告警,这是运维里最危险的假安全感。
3. 核心规则模板逐类拆解与实操要点
3.1 主机层:CPU、内存、磁盘、负载的规则写法
主机层是最基础的一层,规则也最通用。先看CPU。很多人第一反应是写100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 > 80,这个写法本身没错,但有两个细节要注意。一是要按instance聚合,否则多核多实例混在一起算平均值,一台机器满载会被其他空闲机器稀释掉;二是rate的窗口要和采集间隔匹配,采集间隔15s的话,[5m]窗口里有20个点,足够平滑。
我常用的CPU规则是这样的:
- alert: HostHighCpuUsage expr: | 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85 for: 10m labels: severity: warning annotations: summary: "主机 CPU 使用率持续偏高 (实例 {{ $labels.instance }})" description: "CPU 空闲率低于 15%,已持续 10 分钟,当前值 {{ $value | printf \"%.1f\" }}%。"注意for: 10m这个取值。CPU短时冲高太常见了,编译、批处理、日志轮转都会造成尖峰,10分钟能过滤掉绝大多数瞬时波动。如果业务对CPU极其敏感,可以缩到5分钟,但别再短了。
内存规则有个坑:node_memory_MemAvailable_bytes和node_memory_MemFree_bytes不是一回事。后者只算完全空闲的内存,前者还包含了可回收的缓存,判断内存压力应该用Available而不是Free。用Free的话,Linux会把大量内存拿去做页缓存,Free常年很低,告警天天响。正确写法是算Available占Total的比例:
- alert: HostHighMemoryUsage expr: | (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90 for: 10m labels: severity: warning磁盘规则要分两个维度:使用率和剩余空间绝对值。只看使用率会漏掉小分区——一个10G的分区用到95%只剩500M,可能比一个1T分区用到85%更危险。所以我会同时配两条,使用率阈值定在85%,剩余空间定在低于5G告警。另外磁盘预测也值得配一条,用predict_linear算未来4小时是否会写满,这个对提前介入特别有用:
- alert: HostDiskWillFillIn4Hours expr: | predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}[1h], 4 * 3600) < 0 for: 30m labels: severity: warning负载规则要注意,node_load1的绝对值没有普适意义,必须结合CPU核数看。我的做法是算负载与核数的比值,超过1.5告警。核数通过count by (instance) (node_cpu_seconds_total{mode="idle"})拿到。
3.2 容器层:Pod状态、重启与资源限制的规则
容器层的规则和主机层思路不同,因为容器的生命周期短、变化快,规则要更关注“状态”而不是“水位”。最基础的是Pod非Running状态告警,但这里有个细节:kube_pod_status_phase会为每个phase各输出一条时间序列,值为1表示当前处于该phase。所以判断“不是Running”不能简单写phase != "Running",而要针对具体异常phase写,比如Pending和Failed:
- alert: PodPendingTooLong expr: | kube_pod_status_phase{phase="Pending"} == 1 for: 15m labels: severity: warningfor: 15m是因为Pod启动、调度、拉镜像都需要时间,刚创建就Pending是正常的,15分钟还Pending才说明有问题,可能是资源不足或镜像拉不下来。
重启次数告警我用的是increase而不是直接看计数器。kube_pod_container_status_restarts_total是累计值,直接判断大于某个数没意义——一个跑了半年的Pod重启过5次很正常。正确做法是看一段时间内的增量:
- alert: PodRestartingFrequently expr: | increase(kube_pod_container_status_restarts_total[1h]) > 3 for: 5m labels: severity: warning资源限制这块,我强烈建议配“接近limit”的告警。容器内存接近limit会被OOM Kill,CPU接近limit会被限流,这两种情况都会造成服务抖动但未必立刻挂掉,很容易被忽视。用container_memory_working_set_bytes除以kube_pod_container_resource_limits算比例,超过90%告警。这里用working_set而不是usage,是因为working_set排除了可回收的缓存,更接近真实压力。
3.3 应用层:错误率、延迟与饱和度的黄金指标
应用层规则我基本围绕四个黄金指标展开:流量、错误、延迟、饱和度。这四个词不是玄学,是Google SRE那套方法论里反复验证过的最小充分集。流量本身一般不告警(除非骤降),重点在错误率、延迟和饱和度。
错误率规则的关键是分母要对。我见过有人写rate(http_requests_total{status=~"5.."}[5m]) > 0.1,这个写法告的是“每秒5xx请求数超过0.1”,不是错误率。流量大的服务这个阈值形同虚设,流量小的服务又永远触发不了。正确做法是算比例:
- alert: HighErrorRate expr: | sum by (service) (rate(http_requests_total{status=~"5.."}[5m])) / sum by (service) (rate(http_requests_total[5m])) > 0.05 for: 5m labels: severity: critical5%这个阈值是经验值,核心链路可以收紧到1%,非核心可以放宽到10%。另外要注意分母为0的情况,服务完全没流量时这个表达式会得到NaN,不会触发告警,这通常是符合预期的——没流量自然没错误。
延迟规则要用直方图的分位数,而不是平均值。平均值会把长尾掩盖掉,一个P99是2秒但平均值200ms的服务,用户体验其实很差。用histogram_quantile算P99:
- alert: HighLatencyP99 expr: | histogram_quantile(0.99, sum by (service, le) (rate(http_request_duration_seconds_bucket[5m])) ) > 1 for: 5m labels: severity: warning这里有个实操要点:histogram_quantile必须配合sum by (le)使用,且le标签必须保留在聚合维度里,否则算出来的分位数是错的。这个坑我见过不止一个人踩。
饱和度我一般看并发连接数或线程池使用率,接近上限时告警。这类指标因框架而异,模板里给的是通用骨架,具体指标名要按实际技术栈替换。
3.4 元监控:别让监控系统自己瞎了
这一类规则最容易被忽略,但我觉得它比任何业务规则都重要——监控系统自己出问题时,你得知道。核心就两条:采集目标是否存活,规则评估是否正常。
目标存活用up == 0判断,但要加for,因为采集偶尔失败一次很正常:
- alert: TargetDown expr: up == 0 for: 3m labels: severity: warning规则评估健康度看prometheus_rule_evaluation_failures_total的增量,以及prometheus_rule_group_last_duration_seconds是否超过评估间隔。规则评估失败意味着告警可能根本没在算,这是最隐蔽的故障。还有一条值得配:prometheus_notifications_dropped_total增量大于0,说明告警发不出去,Alertmanager那边可能有问题。
4. 实操落地:从模板到可用规则的完整流程
4.1 规则文件的加载与校验
模板拿到手,第一步不是直接扔进生产,而是先做语法校验。Prometheus自带promtool,这是最靠谱的检查工具:
promtool check rules rules/host/*.yml这个命令会检查YAML语法、PromQL表达式合法性、告警名称和标签是否符合规范。我习惯把它加进CI流程,规则文件变更时自动跑一遍,避免把语法错误带到线上。实测下来,promtool能抓出90%以上的低级错误,比如括号不匹配、函数名拼错、for格式不对。
加载方式是在prometheus.yml里配rule_files:
rule_files: - "rules/host/*.yml" - "rules/container/*.yml" - "rules/application/*.yml"支持通配符,按目录加载很方便。改完规则后,Prometheus支持热加载,发个SIGHUP或者调/-/reload接口就行,不用重启。但热加载有个前提,启动时要加--web.enable-lifecycle参数。
4.2 阈值调优的实操记录
我拿一个真实场景说下调优过程。某服务刚接入监控时,错误率规则用的是模板默认的5%、for 5m。上线第一周,告警响了十几次,值班同事怨声载道。我把告警历史拉出来看,发现大部分是发布期间的短暂抖动——新版本滚动发布时,旧Pod下线、新Pod还没完全就绪,会有几十秒的5xx。
处理办法有两个。一是把for从5m调到8m,覆盖发布窗口;二是在发布期间用维护窗口静默。我两个都做了,for调到8m作为常态防护,发布时再叠加静默。调整后一周,误报降到两次,都是真实问题。这个例子说明,阈值调优不是拍脑袋,是拿数据说话,每次调整都要有依据、有记录。
延迟规则的调优也类似。最初P99阈值定在500ms,结果某服务因为依赖的下游接口本身P99就在600ms左右,天天告警。跟业务方确认后,这个延迟是下游的固有特征,不是本服务的问题,于是把阈值放宽到800ms,同时在下游服务那边单独配了规则。告警要告到能处理的人头上,告到处理不了的人那里就是噪音。
4.3 与Alertmanager的路由配合
规则里的severity标签最终要靠Alertmanager的路由来落地。我的路由配置大致是这样:critical走即时通知渠道,warning走工作时间汇总,info只进工单系统。分组用alertname加service,避免同一服务的多条告警刷屏。抑制规则也值得配,比如主机宕机的告警应该抑制该主机上所有其他告警,不然一台机器挂了能给你发几十条。
这里有个细节:规则里的annotations要写清楚,summary一句话说清是什么问题,description带上当前值和关键上下文。值班的人半夜看到告警,第一眼要能判断严重程度和大致方向,而不是再去翻文档。我见过description只写“出问题了”的规则,这种规则等于没写。
5. 常见问题与排查技巧实录
5.1 告警不触发或延迟触发的排查
最常见的问题是“明明指标超了,告警没响”。排查顺序我一般是这样的:先确认规则有没有被加载,访问Prometheus的/rules页面能看到所有已加载规则和它们的当前状态;再看表达式的即时值,在Graph页面把expr贴进去执行,看返回什么;然后看for状态,/alerts页面会显示pending中的告警和已持续时间;最后看Alertmanager有没有收到,查它的/api/v2/alerts。
按这个顺序走,基本能定位到卡在哪一环。我遇到过最隐蔽的一次是规则加载了、表达式也对、for也满足了,但Alertmanager没收到——查下来是规则里的标签和路由配置的匹配条件对不上,告警被路由到了空接收器。这种问题光看Prometheus看不出来,必须两边对照。
5.2 告警风暴的抑制与收敛
告警风暴的成因通常有三类:一是某个基础组件挂了,依赖它的所有服务一起告警;二是阈值定得太敏感,正常波动就触发;三是聚合维度写错,一个实例的问题被放大成几十条。对应的处理手段分别是:配抑制规则、调阈值加for、修正聚合维度。
抑制规则我必配的一条是:TargetDown触发时,抑制同一实例上的所有其他告警。因为实例都挂了,上面的CPU、内存、应用指标告警都没有意义。另一条是critical抑制同名的warning,避免同一问题两个级别一起响。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 规则不生效 | 文件未加载或语法错误 | promtool check rules,查/rules页面 |
| 告警频繁误报 | for太短或阈值过低 | 拉历史数据看分布,调for和阈值 |
| 告警延迟很久才响 | for过长或评估间隔大 | 查evaluation_interval和for设置 |
| 同一问题多条告警 | 聚合维度缺失 | 检查by子句是否漏了关键标签 |
| 告警发不出去 | Alertmanager路由或抑制配置 | 查Alertmanager日志和路由匹配 |
| 分位数算出来不对 | le标签未保留在聚合中 | 确认sum by (le)写法 |
5.4 几条踩坑换来的经验
第一条,别在规则里写死实例名或服务名。模板要通用,具体对象靠标签匹配,写死了就没法复用了。第二条,annotations里的$value记得格式化,默认会输出一长串小数,用printf "%.1f"处理一下,值班的人看着舒服。第三条,规则命名要有统一前缀,比如Host、Pod、App开头,告警列表里一眼能看出属于哪一层。第四条,定期清理僵尸规则,业务下线了规则还留着,不仅浪费评估资源,还会在指标消失时产生奇怪的告警。
我个人在实际操作中的体会是,告警规则这件事没有一劳永逸,它是个持续迭代的过程。模板给的是起点和骨架,真正让它好用的是你在自己环境里一轮轮的调优。每次误报都是一次改进机会,每次漏报都是一次补课。把这套模板当成你的第一版草稿,而不是最终答案,心态就对了。后续如果团队规模上来,还可以考虑把规则做成可参数化的形式,用配置管理工具按环境渲染,那样多集群维护会轻松很多。