运维最佳实践
前面十七章讲的都是「怎么用 Prometheus」。这一章讲的是**「怎么让 Prometheus 活下去」**。
这两件事的区别,在小规模时几乎感觉不到。50 个目标、20 万条序列,一台 4 核 8G 的机器能稳稳跑上一年,你不需要懂任何调优知识。
但当规模翻十倍——或者更常见的情况:规模没翻十倍,但有人给某个指标加了一个 user_id 标签——一切都会在几个小时内崩塌:
- Prometheus 内存从 6G 涨到 60G,被 OOM Killer 杀掉。
- 重启后进入 WAL 重放,花了 40 分钟才恢复服务,期间监控全瞎。
- 好不容易起来了,因为要重新加载所有序列,内存又爆了,进入无限重启循环。
- 看板全部超时,告警规则大面积跳过求值,
prometheus_rule_group_iterations_missed_total一路飙升。 - 磁盘在两天内被写满。
这不是危言耸听,这是每个跑过大规模 Prometheus 的团队都经历过的剧本。这一章要讲的,就是如何避免它,以及它发生时如何抢救。
读完本章你会掌握:
- 基数(cardinality)的确切含义、爆炸的传导路径,以及用
/status/tsdb和 PromQL 做诊断的完整流程 - 哪些标签是「高基数陷阱」,以及用
metric_relabel_configs、sample_limit、label_limit建立防线 - 磁盘容量与内存的估算公式,
--storage.tsdb.retention.time与retention.size的取舍 - 抓取间隔、
--query.max-samples、GOMEMLIMIT等性能相关参数的实际影响 - 记录规则的减负原理、命名规范、以及「什么该做什么不该做」
- 联邦(federation)的能力边界,什么时候必须换成远程写入 + 长期存储
- 告警疲劳的量化诊断与分诊体系设计
一、基数:Prometheus 的第一杀手
什么是基数
基数 = 时间序列的条数。
在 Prometheus 里,指标名 + 一组标签键值对唯一确定一条时间序列。所以:
某个指标的序列数 = 各标签取值数的乘积举例,http_requests_total 有这些标签:
job : 1 个值(api)
instance : 20 个值
method : 4 个值(GET/POST/PUT/DELETE)
status : 8 个值
path : 50 个值序列数 = 1 × 20 × 4 × 8 × 50 = 32000。
这个数字还算健康。现在假设有人加了一个 user_id 标签,日活 10 万:
序列数 = 32000 × 100000 = 32 亿。
这就是基数爆炸。 注意它的关键特征:是乘法,不是加法。加一个标签不是「多一点开销」,而是「乘以这个标签的取值数」。
一个 Histogram 指标实际上会产生 桶数 + 2 条序列:每个桶一条 _bucket,加上 _sum 和 _count。
默认的 Go 客户端 Histogram 有 11 个桶(外加 +Inf),也就是 14 条序列。
所以「给 Histogram 加一个有 50 个取值的 path 标签」,实际增加的是 50 × 14 = 700 条序列,而不是 50 条。再乘以 20 个实例,就是 14000 条序列——一个指标就吃掉了整个系统基数预算的一大块。
Histogram 上加标签要按 14 倍来估算代价。 这也是原生直方图(native histogram)最大的价值:它把一个直方图压缩成一条序列。
基数爆炸的传导路径
理解「为什么高基数这么致命」,需要知道 Prometheus 的内存结构。
Prometheus 的 TSDB 分成 head block(内存中的最近 2 小时数据) 和 persistent blocks(磁盘上已压缩的历史块)。head block 里,每一条活跃序列都需要常驻内存:
- 一个正在写入的 chunk(约 1 到 2 KB,压缩后存放最近的样本)
- 倒排索引的一部分(标签名到序列 ID 的映射、标签值到序列 ID 的 posting list)
- 序列的元数据(标签集本身的字符串,虽然有 interning 去重,但仍占空间)
经验值是每条活跃序列 3 到 8 KB 内存,取决于标签数量和标签值长度。取中间值 5 KB 算:
- 100 万序列 ≈ 5 GB
- 500 万序列 ≈ 25 GB
- 3200 万序列 ≈ 160 GB
而且这只是 head block 的基础开销。查询时还要额外分配内存:一次 sum(rate(x[5m])) 扫描 100 万条序列,中间结果会再占用几个 GB。
更糟的是序列流失(churn)。如果标签值本身在不断变化(比如 pod 名字每次发布都变、request_id 每次请求都变),那么「活跃序列」和「历史序列」都在暴涨:
- 活跃序列:当前正在写入的
- 历史序列:head block 里 2 小时内出现过、但已经不再写入的——它们的索引仍然占着内存,直到 head block 被压缩落盘
每次发布导致所有 pod 名字变化,就意味着基数在 2 小时内翻倍。 一个发布频繁的环境,churn 造成的内存压力可能比稳态基数还大。
诊断第一站:/status/tsdb
Prometheus Web UI 的 Status → TSDB Status 页面(URL 是 /tsdb-status 或 /status/tsdb,取决于版本)是排查基数问题最快的入口。它直接给出:
- Head Stats:head block 里的序列总数、样本总数、时间范围
- Top 10 series count by metric names:哪些指标的序列最多——九成的问题在这一栏就能看出来
- Top 10 label names with value count:哪些标签的取值最多
- Top 10 series count by label value pairs:哪个具体的标签值组合关联了最多序列
- Top 10 label names with high memory usage:哪些标签名占内存最多(长字符串标签会在这里暴露)
这个页面是本地计算的,不消耗查询资源,应该成为你排查的第一反应。
诊断第二站:PromQL
页面看不出全貌时,用查询继续深挖。
# 【最重要的一条】按指标名统计序列数,取 Top 20
topk(20, count by (__name__) ({__name__=~".+"})){__name__=~".+"} 匹配所有序列。在一个 500 万序列的 Prometheus 上执行,会瞬间加载全部序列的索引,可能直接触发 --query.max-samples 限制或者把内存打高一截。
排查时手动执行一次没问题,绝对不要放进 Grafana 看板或者记录规则。 更安全的做法是优先看 /status/tsdb 页面,它给的是同样的信息但零查询成本。
如果一定要查询,可以先缩小范围:
topk(10, count by (__name__) ({job="suspect-job"}))其他诊断查询:
# 按 job 看序列分布
topk(20, count by (job) ({__name__=~".+"}))
# 某个指标的哪个标签基数最高(逐个标签试)
count(count by (path) (http_requests_total))
count(count by (user_id) (http_requests_total))
count(count by (instance) (http_requests_total))
# 每次抓取拉回来多少样本(不需要扫索引,很便宜,可以放看板)
topk(20, scrape_samples_scraped)
# 经过 metric_relabel_configs 处理后实际入库的样本数
topk(20, scrape_samples_post_metric_relabeling)
# 这个差值就是被 drop 掉的量,可以验证 relabel 规则有没有生效
scrape_samples_scraped - scrape_samples_post_metric_relabeling
# head block 当前的序列总数(这是最该长期监控的一个指标)
prometheus_tsdb_head_series
# 序列流失率:每秒新建了多少序列
rate(prometheus_tsdb_head_series_created_total[5m])
rate(prometheus_tsdb_head_series_removed_total[5m])
# 样本摄入速率(samples/s)
rate(prometheus_tsdb_head_samples_appended_total[5m])
# 有没有目标因为超过 sample_limit 被拒绝
increase(prometheus_target_scrapes_exceeded_sample_limit_total[1h]) > 0# 流失率 / 稳态序列数。这个比值越高,内存压力越大
rate(prometheus_tsdb_head_series_created_total[1h]) * 3600
/
prometheus_tsdb_head_series这个值等于「一小时内新建的序列数占总序列数的比例」。
- 小于 0.1 → 健康,序列基本稳定。
- 大于 1 → 每小时都在把整个序列集换一遍,说明有标签在高频变化。典型元凶:
pod名字(每次发布变)、container_id、随机端口、时间戳、批次号。
治理方向不是「减少标签」,而是「让标签值变稳定」——比如用 deployment 名代替 pod 名,用 statefulset 序号代替随机后缀。
高基数标签黑名单
这些标签,永远不要放进 Prometheus 指标:
| 标签 | 基数量级 | 为什么危险 |
|---|---|---|
user_id / account_id | 用户总数 | 直接乘以日活,几乎必然爆炸 |
email / phone | 用户总数 | 同上,外加 PII 合规风险 |
request_id / trace_id | 请求总数 | 每条序列只有一个样本,纯粹的浪费 |
session_id | 会话总数 | 同上 |
timestamp / 日期字符串 | 无上限 | 时间本来就是 TSDB 的维度,放标签里是双重记账 |
| 完整 URL(含 query string) | 无上限 | ?page=1、?page=2 各算一条序列 |
| 完整错误消息 | 无上限 | 错误消息里常带变量(ID、时间、路径) |
| SQL 语句原文 | 无上限 | 同上,且字符串很长,索引内存翻倍 |
| IP 地址(客户端) | 很大 | 服务端 IP 可以,客户端 IP 绝对不行 |
container_id | 高流失 | 每次重启就变,churn 灾难 |
| 版本号(每次构建变) | 中等但持续增长 | 用 build_info 单独一条指标承载,别贴到业务指标上 |
好标签的取值集合是有界且稳定的:环境名、集群名、服务名、状态码、HTTP 方法、路由模板、错误类别、数据中心。这些东西的取值数量是有限的,而且不会天天变。
坏标签的取值集合随业务量增长:用户、请求、会话、订单、消息。
遇到「我就是想知道某个用户的情况」这类需求,答案是:那不是 Prometheus 该干的事。 用日志(Loki / ELK)或链路追踪(Jaeger / Tempo)。Prometheus 是聚合系统,不是明细查询系统。这个边界一旦模糊,系统必然崩溃。
有一个折中方案:把高基数维度降维成有界维度。比如不存 user_id,而是存 user_tier(free / pro / enterprise,3 个值);不存完整 URL,而是存路由模板 /api/users/:id(有界);不存错误消息原文,而是存 error_code(有界枚举)。
建立防线
第一道防线:metric_relabel_configs——在入库前丢掉。
scrape_configs:
- job_name: 'app'
static_configs:
- targets: ['app:8080']
metric_relabel_configs:
# 丢掉整个高基数指标
- source_labels: [__name__]
regex: 'app_request_detail_.*'
action: drop
# 丢掉某个指标的某个高基数标签(保留指标本身)
- source_labels: [__name__]
regex: 'http_requests_total'
action: labeldrop
# 注意:labeldrop 的 regex 匹配的是「标签名」,不看 source_labels
# 上面这条写法是错的,正确写法见下
# 正确的 labeldrop:直接用 regex 匹配标签名
- regex: 'user_id|session_id|request_id'
action: labeldrop
# 只保留白名单内的指标(最激进也最有效)
- source_labels: [__name__]
regex: 'http_requests_total|http_request_duration_seconds_bucket|up|process_.*|go_goroutines'
action: keep
# 截断超长的标签值(避免索引内存爆炸)
- source_labels: [error_message]
regex: '(.{0,64}).*'
target_label: 'error_message'
replacement: '${1}'labeldrop 和 labelkeep 这两个 action 有一个和其他 action 完全不同的语义:它们的 regex 匹配的是标签名本身,而且忽略 source_labels 字段。
# ✅ 正确:丢掉所有名字匹配 user_id 或 session_id 的标签
- regex: 'user_id|session_id'
action: labeldrop
# ❌ 错误:source_labels 会被忽略,这条实际上会丢掉所有名字匹配
# 'http_requests_total' 的标签(当然一个也匹配不上),等于什么都没做
- source_labels: [__name__]
regex: 'http_requests_total'
action: labeldrop想「只对某个指标丢标签」是做不到的——labeldrop 作用于该 job 的所有指标。如果只想处理一个指标,得用 replace 把标签置空(Prometheus 会自动移除空值标签):
- source_labels: [__name__]
regex: 'http_requests_total'
target_label: 'user_id'
replacement: ''注意这条只在 __name__ 匹配时才执行替换,不匹配时规则整体跳过,其他指标的 user_id 得以保留。
第二道防线:抓取层的硬限制。
global:
scrape_interval: 30s
scrape_timeout: 10s
# 全局兜底:单次抓取超过这么多样本就整个丢弃(0 表示不限制)
sample_limit: 20000
# 单个指标的标签数上限
label_limit: 30
# 标签名长度上限
label_name_length_limit: 100
# 标签值长度上限
label_value_length_limit: 512
# 单个 job 的目标数上限
target_limit: 500
scrape_configs:
# 对不受信任的目标收紧限制
- job_name: 'third-party-app'
sample_limit: 2000
label_limit: 15
static_configs:
- targets: ['vendor-app:9090']sample_limit 的行为需要特别理解:超限时整次抓取全部丢弃,up 会变成 0,并且 prometheus_target_scrapes_exceeded_sample_limit_total 递增。这是故意设计成很痛的——它逼着业务方立刻处理,而不是悄悄地污染 TSDB。
直接给所有 job 加 sample_limit 会误伤(有些 job 本来就合理地暴露很多样本)。推荐的落地顺序:
- 先观察,不限制。 用
topk(20, scrape_samples_scraped)看清楚现状。 - 按 job 分别设置,取「当前值的 2 到 3 倍」作为初始阈值,给正常增长留空间。
- 加一条预警告警,在接近限制时就通知,而不是等到超限被丢:
- alert: ScrapeSampleCountHigh
expr: 'scrape_samples_scraped > 15000'
for: 30m
labels:
severity: 'warning'
annotations:
summary: '{{ $labels.job }} / {{ $labels.instance }} 单次抓取 {{ $value }} 个样本,接近上限'
- alert: ScrapeSampleLimitExceeded
expr: 'increase(prometheus_target_scrapes_exceeded_sample_limit_total[10m]) > 0'
for: 0m
labels:
severity: 'critical'
annotations:
summary: '有目标因超过 sample_limit 被整体丢弃,该目标的监控已完全失效'- 新接入的 job 一律先加严格限制,这是最有效的一条——防患于未然比事后治理便宜一百倍。
第三道防线:长期监控基数趋势。
groups:
- name: prometheus-cardinality
interval: 1m
rules:
- record: prometheus:head_series:current
expr: prometheus_tsdb_head_series
- alert: HeadSeriesGrowingFast
expr: |
prometheus_tsdb_head_series
/
(prometheus_tsdb_head_series offset 1d) > 1.5
for: 1h
labels:
severity: 'warning'
annotations:
summary: 'Prometheus 序列数 24 小时内增长超过 50%(当前 {{ $value }} 倍)'
- alert: HeadSeriesTooHigh
expr: 'prometheus_tsdb_head_series > 4000000'
for: 30m
labels:
severity: 'warning'
- alert: HighSeriesChurn
expr: |
rate(prometheus_tsdb_head_series_created_total[1h]) * 3600
/
prometheus_tsdb_head_series > 1
for: 2h
labels:
severity: 'warning'
annotations:
summary: '序列流失率过高,每小时新建的序列数超过了总序列数'周四下午 3 点,Prometheus 开始频繁 OOM 重启。运维查到:
prometheus_tsdb_head_series从 80 万涨到 620 万,涨幅发生在下午 2 点到 2 点 40 分之间- 内存从 8G 涨到 48G(机器只有 48G)
- 同一时间窗口内有三个团队分别做了发布
请给出:
- 一套按顺序执行的诊断步骤(说明每一步用什么工具/查询、能得出什么结论)。注意 Prometheus 正在反复 OOM,你的查询本身也可能把它打挂。
- 假设定位到是
payment服务的payment_transaction_duration_seconds这个 Histogram 加了merchant_id标签(约 4 万个商户,12 个桶),请计算它贡献了多少序列,并验证是否能解释 540 万的增量。 - 给出紧急止血方案和长期治理方案。
- 设计一套能在下次「涨到 620 万之前」就发现问题的告警。
二、保留期与存储容量估算
计算公式
Prometheus 的磁盘用量有一个相当准的估算公式:
所需磁盘 = 保留时间(秒) × 每秒摄入样本数 × 每样本字节数三个因子分别怎么来:
每秒摄入样本数:
samples/s = 活跃序列数 / 抓取间隔(秒)或者直接查:
rate(prometheus_tsdb_head_samples_appended_total[5m])每样本字节数: Prometheus 用 Gorilla 风格的双 delta 编码,压缩率极高。官方给出的经验值是 1 到 2 字节/样本。想知道你自己环境的准确值:
rate(prometheus_tsdb_compaction_chunk_size_bytes_sum[1h])
/
rate(prometheus_tsdb_compaction_chunk_samples_sum[1h])变化平缓的 Gauge(比如 node_memory_MemTotal_bytes)能压到 0.5 字节以下;抖动剧烈的值可能到 2 到 3 字节。规划时用 2 字节比较稳妥。
一个完整的算例
假设:
- 活跃序列:100 万
- 抓取间隔:30 秒
- 保留期:30 天
- 每样本:2 字节
samples/s = 1,000,000 / 30 ≈ 33,333
30 天秒数 = 30 × 86400 = 2,592,000
磁盘 = 2,592,000 × 33,333 × 2 字节
≈ 172,800,000,000 字节
≈ 161 GiB再乘以 1.3 到 1.5 的安全系数(索引开销、压缩过程中的临时空间、WAL、以及块压缩时需要同时存在新旧两份数据),实际应该准备 220 到 250 GB。
TSDB 的块压缩会把多个小块合并成一个大块。合并过程中,新块和旧块会同时存在于磁盘上,直到新块写完、旧块才被删除。
最大的一次压缩会涉及「保留期 10% 或 31 天中较小者」大小的块。对 30 天保留期,最大块约 3 天的数据。压缩这个块时的磁盘峰值大约是稳态用量 + 一个大块的大小。
磁盘使用率超过 80% 就应该告警,不要等到 95%——压缩失败会导致数据无法落盘,最终 head block 撑爆内存。
内存估算
内存 ≈ 活跃序列数 × (3 到 8 KB) + 查询峰值开销更靠谱的做法是直接测量你自己的环境:
# 每条序列实际占多少字节
process_resident_memory_bytes{job="prometheus"}
/
prometheus_tsdb_head_series
# Go 堆的实际使用
go_memstats_heap_inuse_bytes{job="prometheus"}
# 内存使用率
process_resident_memory_bytes{job="prometheus"}
/
node_memory_MemTotal_bytes查询峰值开销是最容易被低估的部分。一次扫描 100 万序列的查询,中间结果可能占用好几 GB。所以:
- 稳态内存不要超过机器内存的 50%,剩下的留给查询和 GC。
- 一定要设
GOMEMLIMIT(下一节详述)。
两个保留期参数
prometheus \
--storage.tsdb.path=/var/lib/prometheus \
--storage.tsdb.retention.time=30d \
--storage.tsdb.retention.size=200GB--storage.tsdb.retention.time:按时间保留,默认 15d。--storage.tsdb.retention.size:按大小保留。支持B、KB、MB、GB、TB、PB后缀(也接受KiB这类二进制单位)。
两者同时设置时,谁先触发就按谁清理。 这是最推荐的配置方式:
retention.time保证「不会保留超过业务需要的时长」retention.size保证「不会写爆磁盘」
一个容易踩的细节:--storage.tsdb.retention.size 统计的不只是持久化的块,它还包括 WAL 和 m-mapped 的 head chunks。
这意味着如果你把 retention.size 设成和磁盘容量一样大,仍然可能写满——因为 Prometheus 是在超过限制之后才删除最老的块,而删除是以「整块」为单位的。
建议 retention.size 设成磁盘容量的 70% 到 80%。 剩下的空间留给压缩峰值和意外增长。
删除是以块为单位的,不是按样本精确裁剪。所以实际保留的数据量总是略多于配置值,最多多出一个块的时长。
降采样与分层保留
Prometheus 本身不支持降采样(downsampling),也不支持「按指标设置不同保留期」。这是它的设计取舍——保持简单。
需要这些能力时,选择是:
- Thanos:sidecar 上传块到对象存储,Compactor 做降采样(5m 和 1h 两级),Store Gateway 提供查询。
- Cortex / Mimir:多租户,水平扩展,通过 remote write 接收数据。
- VictoriaMetrics:单体或集群模式,压缩率更高,支持按租户设置保留期。
用两套 Prometheus 分层是一个不需要引入新组件的土办法:
# prometheus-short.yml —— 高精度短期
global:
scrape_interval: 15s
# --storage.tsdb.retention.time=7d
# prometheus-long.yml —— 低精度长期,只联邦记录规则的结果
global:
scrape_interval: 60s
evaluation_interval: 60s
# --storage.tsdb.retention.time=400d
scrape_configs:
- job_name: 'federate-recording-rules'
scrape_interval: 60s
honor_labels: true
metrics_path: '/federate'
params:
'match[]':
- '{__name__=~"job:.*"}'
- '{__name__=~"instance:.*"}'
static_configs:
- targets: ['prometheus-short:9090']这个方案的关键是:长期实例只联邦记录规则的输出(用命名前缀 job: 和 instance: 筛选),序列数只有原始数据的百分之一量级,400 天保留期完全扛得住。
你要为一个新集群规划 Prometheus,已知:
- 300 台机器,每台跑 node_exporter,每个 exporter 约 1200 条序列
- 80 个应用实例,每个应用暴露约 3000 条序列(含 Histogram)
- 12 个中间件 exporter(Redis/MySQL/Kafka),每个约 800 条序列
- 计划抓取间隔:node_exporter 30s,应用 15s,中间件 30s
- 需要保留 45 天
- 预计一年内机器数增长 50%,应用实例数翻倍
请计算:
- 总序列数与每秒样本摄入量。
- 45 天所需磁盘(给出计算过程和推荐配置值)。
- 推荐的内存规格。
- 一年后的规模,以及现在应该怎么规划才不用推倒重来。
- 如果磁盘预算只有 500 GB,有哪些调整手段?分别能省多少?
三、性能调优
抓取间隔:最大的那个旋钮
抓取间隔和摄入量、磁盘、CPU 全部成反比。把间隔从 15s 改成 30s,这些指标全部减半。
但它也直接决定了监控的时间分辨率:
| 抓取间隔 | 最快发现异常 | 适用场景 |
|---|---|---|
| 5s | 约 15 秒 | 极少数关键链路,代价很高,慎用 |
| 15s | 约 45 秒 | 核心业务服务的默认值 |
| 30s | 约 90 秒 | 大多数场景的最佳平衡点 |
| 60s | 约 3 分钟 | 基础设施、变化缓慢的指标 |
| 5m | 约 15 分钟 | 批处理、外部 API 配额、业务日报类指标 |
Prometheus 有一个 --query.lookback-delta 参数,默认 5 分钟。查询某个时刻的瞬时值时,Prometheus 会往前找最近 5 分钟内的样本,找不到就认为该序列在这个时刻「不存在」。
如果抓取间隔超过 5 分钟,会出现图表断断续续的诡异现象。
更实际的约束来自 rate():rate(x[5m]) 需要区间内至少有 2 个样本才能计算。抓取间隔 3 分钟时,5 分钟窗口里可能只有 1 个点,rate 直接返回空。
经验法则:rate 的区间至少是抓取间隔的 4 倍。 30s 间隔配 [2m] 及以上,60s 间隔配 [4m] 及以上。日常统一用 [5m] 是最省心的选择。
抓取间隔的硬上限建议是 2 分钟。 需要更长的(比如每小时才更新一次的业务指标),应该用 Pushgateway 或者把值做成 Gauge 由应用维护,而不是拉长抓取间隔。
按 job 差异化配置,而不是全局一个值:
global:
scrape_interval: 30s
scrape_timeout: 10s
evaluation_interval: 30s
scrape_configs:
# 核心交易链路:高频
- job_name: 'payment-api'
scrape_interval: 15s
scrape_timeout: 10s
# 普通应用:默认
- job_name: 'internal-services'
# 基础设施:低频
- job_name: 'node'
scrape_interval: 60s
# 变化极慢的:更低频
- job_name: 'ssl-cert-checker'
scrape_interval: 5m
scrape_timeout: 30s关键启动参数
prometheus \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/var/lib/prometheus \
--storage.tsdb.retention.time=30d \
--storage.tsdb.retention.size=200GB \
--query.max-samples=50000000 \
--query.max-concurrency=20 \
--query.timeout=2m \
--query.lookback-delta=5m \
--web.enable-lifecycle \
--web.enable-admin-api \
--log.level=info| 参数 | 默认值 | 说明 |
|---|---|---|
--query.max-samples | 50000000 | 单次查询能加载到内存的最大样本数。这是防止一条查询打挂整个实例的最后防线 |
--query.max-concurrency | 20 | 并发查询数上限。超过后排队。Grafana 看板多时可能需要调大,但调大会增加内存峰值 |
--query.timeout | 2m | 查询超时 |
--query.lookback-delta | 5m | 瞬时查询往前找样本的窗口 |
--web.enable-lifecycle | 关闭 | 开启后支持 POST /-/reload 热加载配置 |
--web.enable-admin-api | 关闭 | 开启后支持删除序列、快照等管理 API |
--storage.tsdb.head-chunks-write-queue-size | 0 | head chunk 异步写队列长度。高摄入场景设为 10000 左右能降低写延迟毛刺 |
环境变量层面,GOMEMLIMIT 是最重要的一个:
# systemd unit 里
Environment="GOMEMLIMIT=26GiB"
Environment="GOGC=75"GOMEMLIMIT(Go 1.19+)给 Go 运行时设一个软内存上限。接近时 GC 会变得非常激进,宁可多花 CPU 也要把内存压下去。设成机器物理内存的 80% 左右。 效果是把「被 OOM Killer 杀掉」变成「变慢但活着」——对监控系统来说这个取舍毫无疑问是对的。GOGC默认 100(堆增长一倍才 GC)。降到 75 或 50 能让内存曲线更平缓,代价是 CPU 上升 10% 到 20%。设了GOMEMLIMIT之后通常不需要再调GOGC。
找出慢查询
# prometheus.yml
global:
query_log_file: /var/log/prometheus/query.log开启后,每一条查询都会以 JSON 格式记录,包含完整的查询语句、各阶段耗时、以及调用来源:
# 找出最慢的 20 条查询
cat /var/log/prometheus/query.log \
| jq -r '[.stats.timings.evalTotalTime, .params.query] | @tsv' \
| sort -rn | head -20这个日志文件会长得很快,建议只在排查期间开启,或者配好 logrotate。
配套的自监控指标:
# 规则组求值耗时 vs 求值间隔,接近 1 就危险
prometheus_rule_group_last_duration_seconds
/
prometheus_rule_group_interval_seconds
# 有多少次求值因为上一轮没跑完被跳过(不为 0 就是严重问题)
increase(prometheus_rule_group_iterations_missed_total[1h])
# 规则求值失败次数
increase(prometheus_rule_evaluation_failures_total[1h])
# 抓取耗时接近超时的目标
topk(10, scrape_duration_seconds / on(job) group_left() (
max by (job) (scrape_duration_seconds)
))
# 抓取超时的目标
scrape_duration_seconds > 8
# TSDB 压缩耗时
histogram_quantile(0.99, rate(prometheus_tsdb_compaction_duration_seconds_bucket[1h]))
# WAL 截断耗时(重启慢通常和这个有关)
prometheus_tsdb_wal_truncate_duration_seconds_sum一套最小可用的 Prometheus 自监控告警:
groups:
- name: prometheus-self
rules:
- alert: PrometheusRuleEvaluationsMissed
expr: 'increase(prometheus_rule_group_iterations_missed_total[10m]) > 0'
for: 10m
labels:
severity: 'critical'
annotations:
summary: '规则组 {{ $labels.rule_group }} 求值被跳过,告警可能已失效'
- alert: PrometheusRuleEvaluationFailures
expr: 'increase(prometheus_rule_evaluation_failures_total[10m]) > 0'
for: 5m
labels:
severity: 'warning'
- alert: PrometheusRuleGroupSlow
expr: |
prometheus_rule_group_last_duration_seconds
/
prometheus_rule_group_interval_seconds > 0.8
for: 15m
labels:
severity: 'warning'
annotations:
summary: '规则组 {{ $labels.rule_group }} 求值耗时占间隔的 {{ $value }},即将开始跳过'
- alert: PrometheusTargetScrapeTimeout
expr: 'scrape_duration_seconds > 0.9 * 10'
for: 15m
labels:
severity: 'warning'
- alert: PrometheusTSDBCompactionFailing
expr: 'increase(prometheus_tsdb_compactions_failed_total[1h]) > 0'
labels:
severity: 'critical'
- alert: PrometheusTSDBWALCorruption
expr: 'increase(prometheus_tsdb_wal_corruptions_total[1h]) > 0'
labels:
severity: 'critical'
- alert: PrometheusNotIngestingSamples
expr: 'rate(prometheus_tsdb_head_samples_appended_total[10m]) <= 0'
for: 10m
labels:
severity: 'critical'
annotations:
summary: 'Prometheus 已 10 分钟没有摄入任何样本'
- alert: PrometheusConfigReloadFailed
expr: 'prometheus_config_last_reload_successful == 0'
for: 5m
labels:
severity: 'critical'
summary: '配置重载失败,当前运行的是旧配置'PrometheusRuleEvaluationsMissed 是这里面最重要的一条。 规则求值被跳过意味着告警规则没有执行——你的整套告警在那段时间里是瞎的,而且不会有任何提示。这是监控系统最危险的失效模式。
当然,这些告警由 Prometheus 自己评估,如果它彻底挂了就没人报了。完整方案需要一个「监控监控系统的监控系统」:Alertmanager 的 Watchdog 心跳(Prometheus 持续发一条永远 firing 的告警,外部系统检测到心跳中断就报警),或者两个 Prometheus 互相抓取对方的 up。
四、记录规则减负
减负的原理
记录规则的价值在于把「查询时计算」变成「写入时计算」:
- 一个复杂查询如果被 10 个看板面板引用,每个面板 30 秒刷新一次,那就是每分钟执行 20 次。
- 做成记录规则后,它每 30 秒执行 1 次,看板直接读结果。
- 计算量降低了 20 倍,而且是把不可预测的突发负载变成了平稳的固定负载。
第二个好处同样重要:记录规则的结果序列数极少。一个扫描 50 万序列的查询,结果可能只有 10 条序列。看板读这 10 条序列几乎零成本。
命名规范
官方推荐的格式:
level:metric:operations- level:聚合层级,也就是结果保留了哪些标签(
job、instance、cluster、namespace) - metric:原始指标名
- operations:做了什么操作,从右往左读(
rate5m、sum_rate5m、ratio_rate5m)
groups:
- name: http-recording
interval: 30s
rules:
# 按 job 聚合的 QPS
- record: job:http_requests:rate5m
expr: sum by (job) (rate(http_requests_total[5m]))
# 按 job + status 聚合
- record: job_status:http_requests:rate5m
expr: sum by (job, status) (rate(http_requests_total[5m]))
# 每个实例的 QPS
- record: instance:http_requests:rate5m
expr: sum by (instance) (rate(http_requests_total[5m]))
# 错误率
- record: job:http_errors:ratio_rate5m
expr: |
sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by (job) (rate(http_requests_total[5m]))
# P99 延迟
- record: job:http_duration_seconds:p99_rate5m
expr: |
histogram_quantile(0.99,
sum by (job, le) (rate(http_request_duration_seconds_bucket[5m]))
)这个命名规范的好处是从名字就能知道能不能再聚合。看到 job:http_requests:rate5m,你知道它已经按 job 聚合过了,不能再 sum by (instance);看到 instance:...,你知道它保留了实例维度。
1. 不要对已经聚合的结果再算 rate。
# ❌ 错误
- record: job:http_requests:rate5m
expr: rate(sum by (job) (http_requests_total)[5m:])sum 会掩盖 Counter 重置——某个实例重启时它的 Counter 归零,但求和的结果只是「下降了一点」,rate 会把这个下降当成重置来处理,产生完全错误的尖峰。
永远是 sum(rate(...)),不是 rate(sum(...))。
2. rate 的区间不要小于规则的 interval。
groups:
- name: bad
interval: 5m
rules:
- record: job:x:rate1m
expr: sum by (job) (rate(x_total[1m])) # ❌每 5 分钟求值一次,每次只看最近 1 分钟——中间 4 分钟的数据被完全跳过,算出来的速率会系统性失真(如果流量有波动,可能偏高也可能偏低)。
规则:rate 区间 ≥ 规则 interval,且 ≥ 4 倍抓取间隔。
3. 同一 group 内顺序执行,跨 group 并行。
同一个 group 里的规则严格按书写顺序串行执行,所以后面的规则可以引用前面规则的结果,拿到的是本轮的新值。
不同 group 之间并行执行,跨 group 引用会拿到上一轮的旧值(甚至可能拿不到)。
所以:有依赖关系的规则必须放在同一个 group,并按依赖顺序书写。
groups:
- name: slo-base # 基础层
interval: 30s
rules:
- record: job:requests:rate5m
expr: sum by (job) (rate(http_requests_total[5m]))
- record: job:errors:rate5m
expr: sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
- record: job:errors:ratio_rate5m # 依赖上面两条,必须在同一 group 且在其后
expr: job:errors:rate5m / job:requests:rate5m什么该做、什么不该做
该做:
- 被多个看板面板引用的表达式
- 告警规则里的复杂表达式(尤其是多窗口 SLO 那一套)
- 长窗口聚合(
[1d]、[7d]、[30d]) - 跨指标的除法(错误率、命中率、饱和度)
- 联邦要暴露给上层的数据
- 需要长期保留的核心业务指标
不该做:
- 只用一次的查询——记录规则本身也有成本(占 CPU、占序列、占磁盘),一次性的探索用即席查询就好。
- 产生高基数结果的规则——一条
sum by (instance, path, method, status)的规则可能产生几万条新序列。记录规则是用来降维的,不是用来复制数据的。 - 简单到不值得的表达式——
rate(x[5m])这种直接查就行,做成规则反而多了一层间接。 topk类查询——结果集每轮都在变,序列会疯狂流失。
# 这条规则每轮产生多少条序列
count(job:http_requests:rate5m)
# 这条规则求值花多久(需要按 group 看)
prometheus_rule_group_last_duration_seconds{rule_group="http-recording"}
# 整个规则体系产生了多少条序列
count({__name__=~"^[a-z_]+:[a-z_]+:.*"})判断标准: 如果一条记录规则产生的序列数超过原始序列数的 10%,说明它没有起到降维作用,应该重新设计聚合维度。
另外记得:记录规则产生的序列同样计入基数,同样占内存和磁盘。见过团队为了「方便」,把所有指标都按各种维度组合预聚合一遍,结果记录规则的输出比原始数据还多——这属于用记录规则制造问题而不是解决问题。
五、联邦与长期存储的边界
联邦能做什么、不能做什么
联邦(federation)的机制是:上层 Prometheus 抓取下层 Prometheus 的 /federate 端点,用 match[] 参数指定要拉哪些序列。
scrape_configs:
- job_name: 'federate'
scrape_interval: 60s
scrape_timeout: 30s
honor_labels: true
metrics_path: '/federate'
params:
'match[]':
- '{__name__=~"job:.*"}'
- '{__name__=~"instance:.*"}'
- 'up'
static_configs:
- targets:
- 'prometheus-bj:9090'
- 'prometheus-sh:9090'
- 'prometheus-gz:9090'honor_labels: true 是必需的——它让下层 Prometheus 上报的 job 和 instance 标签保持原样,而不是被上层的 job 配置覆盖。
最常见的误用是这样:
params:
'match[]':
- '{__name__=~".+"}' # ❌ 拉取全部序列这行不通,原因有三:
1. 一次抓取要在 scrape_timeout 内完成。 下层有 100 万条序列,/federate 要在一次 HTTP 响应里把 100 万个样本序列化并传输完。这通常需要几十秒甚至几分钟,远超 scrape_timeout,抓取永远失败。
2. 分辨率丢失。 上层以 60s 间隔抓取,下层的 15s 精度数据被压缩成 60s——中间的点直接丢了。你在上层看到的图和下层看到的图不一样,排障时会非常困惑。
3. 上层成为新的瓶颈。 三个下层各 100 万序列,上层就是 300 万序列,你只是把问题搬了个家,还多了一层故障点。
联邦的正确用法是「只拉聚合结果」:下层用记录规则把数据聚合成几百到几千条序列,上层只拉这些。序列数从 100 万降到 1000,抓取在 1 秒内完成,上层可以轻松承载几十个下层。
什么时候必须换成远程写入
联邦解决的是「跨集群的全局视图」,它解决不了:
- 长期存储(联邦上来的数据仍然存在上层 Prometheus 的本地盘,仍然受保留期限制)
- 高可用(下层挂了,那段数据就永久丢了,没有补偿机制)
- 水平扩展(上层仍然是单点)
- 降采样(Prometheus 不支持)
- 原始精度的历史查询(联邦本身就降低了分辨率)
需要这些能力时,就该上远程写入 + 长期存储方案了:
remote_write:
- url: 'http://mimir:8080/api/v1/push'
queue_config:
capacity: 10000 # 每个分片的内存队列容量
max_shards: 50 # 最大并行分片数
min_shards: 1
max_samples_per_send: 2000
batch_send_deadline: 5s
min_backoff: 30ms
max_backoff: 5s
write_relabel_configs:
# 只把需要长期保存的送出去,能省一大笔存储和带宽费用
- source_labels: [__name__]
regex: 'job:.*|instance:.*|up|slo:.*'
action: keep远程写入的健康度必须监控:
# 积压:待发送的样本数
prometheus_remote_storage_samples_pending
# 发送失败率
rate(prometheus_remote_storage_samples_failed_total[5m])
# 【最重要】写入延迟:远端已确认的时间戳落后本地多少秒
(
prometheus_remote_storage_highest_timestamp_in_seconds
-
ignoring(remote_name, url) group_right
prometheus_remote_storage_queue_highest_sent_timestamp_seconds
)
# 当前分片数(持续贴着 max_shards 说明需要调大)
prometheus_remote_storage_shards方案选型速查
| 方案 | 适合规模 | 核心优势 | 主要代价 |
|---|---|---|---|
| 单 Prometheus | 100 万序列以内 | 零额外组件,运维极简 | 无 HA、无长期存储 |
| Prometheus 分片 + 联邦 | 几百万序列 | 不引入新组件,故障域隔离 | 跨分片查询能力弱,只能看聚合值 |
| Thanos | 千万级 | 对象存储成本低、降采样、去重、全局查询 | 组件多(sidecar/store/query/compact/ruler),查询链路长 |
| Cortex / Mimir | 亿级 | 多租户、水平扩展、写入侧高可用 | 架构复杂,运维门槛最高 |
| VictoriaMetrics | 千万到亿级 | 压缩率高、资源占用低、单体模式部署简单 | 与上游 Prometheus 生态有细微行为差异 |
Thanos、Mimir 这些方案的运维复杂度是 Prometheus 的好几倍。它们引入了对象存储依赖、多个需要独立扩缩容的组件、以及一套全新的故障模式(compactor 卡住、store gateway 内存爆炸、查询扇出超时)。
在单个 Prometheus 还能扛住(低于 500 万序列、保留期需求低于 90 天)的时候,绝对不要引入它们。
按优先级排的演进路径是:
- 先治理基数——大多数「Prometheus 扛不住了」的问题,本质是基数问题,不是架构问题。删掉几个高基数指标能省下的资源,往往比引入 Thanos 还多。
- 再加记录规则——把查询压力降下来。
- 再按功能分片——
prometheus-infra/prometheus-app,故障域隔离,各自独立扩容。 - 最后才考虑长期存储方案——而且要先想清楚:真的需要 400 天原始精度数据吗?还是「400 天的聚合值 + 30 天的原始值」就够了?后者用两层 Prometheus 就能做到。
六、告警疲劳与分诊
量化你的告警质量
告警疲劳不是一个感觉问题,它是可以测量的。ALERTS 这个内置指标记录了所有告警的状态,用它做一次「告警体检」:
# 过去 7 天里,哪些告警触发得最频繁(找出最吵的)
topk(20, count_over_time(ALERTS{alertstate="firing"}[7d]))
# 过去 7 天每天平均触发多少条告警
count_over_time(ALERTS{alertstate="firing"}[7d]) / 7
# 哪些告警从来没触发过(可能是阈值设得太宽,形同虚设)
count_over_time(ALERTS[30d]) == 0
# 「闪烁」的告警:触发次数多但每次持续时间短 —— 典型的噪音源
count_over_time(ALERTS{alertstate="firing"}[7d]) > 50
# 长期挂着不恢复的告警 —— 说明它不是「事件」而是「状态」,
# 要么阈值不对,要么这事根本没人管
ALERTS{alertstate="firing"} and (ALERTS{alertstate="firing"} offset 7d)
# 按严重级别看告警量分布
sum by (severity) (count_over_time(ALERTS{alertstate="firing"}[7d]))信号一:某条告警占了总告警量的 30% 以上。 这条告警一定有问题——要么阈值不对,要么它监控的是一个没人打算修的问题。值班人员早就学会了忽略它,而且会顺带忽略它旁边的其他告警。
信号二:有告警连续 firing 超过一周。 告警的定义是「需要人介入的异常事件」。挂了一周还在的,说明它不是事件,是现状。要么修掉根因,要么调整阈值,要么降级成看板指标——留着不管是最坏的选择,因为它会永久性地污染告警列表。
信号三:每天 critical 告警超过 5 条。 人的注意力有限。超过这个量,值班人员会进入「批量已读」模式,真正的故障就会被淹没。Google SRE 的经验数据是:一次值班班次(12 小时)内的 page 数量应该控制在 2 条以内。
分诊体系
一套可持续的告警体系应该是三层的,而不是「所有告警一视同仁」:
| 层级 | 判断标准 | 通知方式 | 响应时间 | 数量目标 |
|---|---|---|---|---|
| P1 / Page | 用户正在受影响,且需要立刻人工介入 | 电话、短信、值班 App | 5 分钟内 | 每周不超过 2 条 |
| P2 / Ticket | 会演变成 P1,或已影响部分用户,但可以等 | 工单、企业微信/钉钉群 | 工作时间内 | 每天不超过 10 条 |
| P3 / Info | 需要知道但不需要行动 | 看板、周报 | 无 | 不进告警通道 |
判断一条告警该放哪一层,只需要问一个问题:
「如果这条告警在凌晨 3 点响,我会起床处理吗?」
- 会 → P1
- 不会,但明天上班第一件事要处理 → P2
- 明天也不一定看 → P3,不该发通知
传统方式下,一个服务可能有几十条告警:CPU、内存、磁盘、连接数、队列长度、GC、线程数、慢查询、5xx 率、延迟……每一条都可能在半夜响,但其中大部分用户根本没感觉。
改成 SLO 告警之后,面向用户的告警只有 4 条(可用性和延迟各两档燃烧率)。它们只在「用户真的在受影响,且预算真的在快速消耗」时才响。
那些 CPU、内存、GC 的指标怎么办?降级成 P2 或 P3。它们的价值是排障时的诊断信息,而不是叫醒人的理由。CPU 100% 但用户毫无感知,那说明系统设计得很好(资源利用充分),不是故障。
这个转变的心理阻力通常很大——「万一 CPU 满了不报警,出事了怎么办?」答案是:出事了 SLO 告警会响。SLO 告警是「症状告警」,它天然覆盖了所有会影响用户的原因,包括那些你没想到的原因。而阈值告警是「原因告警」,你永远只能覆盖到你想得到的那些原因。
保留少量的「容量类」原因告警是合理的(比如磁盘 7 天内会写满、证书 15 天内过期),因为这些问题发生时已经来不及了,必须提前预警。但它们应该是 P2 工单,不是 P1 电话。
工程手段
Alertmanager 侧的四件套:
route:
# 1. 分组:把同一批告警收成一条通知
group_by: ['alertname', 'cluster', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'default-ticket'
routes:
# 2. 分级路由
- matchers:
- severity = "critical"
- page = "true"
receiver: 'oncall-phone'
group_wait: 10s
repeat_interval: 1h
continue: false
- matchers:
- severity = "warning"
receiver: 'team-ticket'
repeat_interval: 12h
# 3. 按团队路由
- matchers:
- team = "payment"
receiver: 'payment-group'
# 4. 抑制:根因压掉衍生告警
inhibit_rules:
# 实例宕机时,压掉该实例上的所有其他告警
- source_matchers:
- alertname = "InstanceDown"
target_matchers:
- severity =~ "warning|critical"
equal: ['instance']
# 整个集群不可用时,压掉单个服务的告警
- source_matchers:
- alertname = "ClusterDown"
target_matchers:
- severity =~ "warning|critical"
equal: ['cluster']
# critical 压掉同一告警的 warning 版本
- source_matchers:
- severity = "critical"
target_matchers:
- severity = "warning"
equal: ['alertname', 'instance']规则侧的三件套:
# 1. 合适的 for:过滤瞬时抖动
- alert: HighErrorRate
expr: 'job:http_errors:ratio_rate5m > 0.05'
for: 10m # 不是 0m,也不是 1h
# 2. keep_firing_for:防止告警在恢复边缘反复抖动
- alert: HighErrorRate
expr: 'job:http_errors:ratio_rate5m > 0.05'
for: 10m
keep_firing_for: 15m
# 3. 有意义的 annotation:让人不用查看板就知道该干什么
annotations:
summary: '{{ $labels.job }} 错误率 {{ humanizePercentage $value }}'
description: |
服务 {{ $labels.job }} 在过去 10 分钟内 5xx 错误率持续超过 5%。
影响范围:{{ $labels.cluster }} 集群
runbook_url: 'https://wiki.internal/runbook/{{ $labels.alertname }}'
dashboard_url: 'https://grafana.internal/d/api-overview?var-job={{ $labels.job }}'生产 Prometheus 出现以下症状:
- Grafana 上一半的面板显示
Query timeout prometheus_rule_group_iterations_missed_total在过去 6 小时增长了 340- CPU 稳定在 95%,内存 22G / 32G(没有 OOM)
prometheus_tsdb_head_series是 180 万,过去一周基本没变- 抓取正常,
up全是 1,scrape_duration_seconds都在 2 秒以内
请:
- 判断瓶颈在哪一环(摄入?存储?查询?规则求值?),说明依据。
- 给出定位问题的具体查询和命令。
- 给出至少四条优化措施,按「见效速度」排序。
- 写一套能防止同类问题复发的告警。
你接手一个已经运行两年的监控系统,现状:
- 共 287 条告警规则
- 上周触发了 4300 次告警(平均每天 614 次)
- 其中一条
DiskUsageHigh占了 1800 次 - 有 12 条告警连续 firing 超过 30 天
- 值班群里,工程师对告警的平均响应时间是 2 小时(对 critical 也是)
- 上个月有一次真实故障,告警在 3 分钟内触发,但没人看到,最后是客户投诉才发现的
请设计一次系统性治理,包括:
- 用哪些查询做「告警体检」,各能发现什么问题。
- 一套分类整改的决策流程(每条告警该怎么处置)。
- 针对
DiskUsageHigh这类「最吵告警」的具体改造。 - 针对「12 条长期 firing」的处置原则。
- 整改后如何衡量效果,以及如何防止半年后又退化。
小结
- 基数是 Prometheus 的第一杀手。 序列数 = 各标签取值数的乘积,Histogram 还要再乘以「桶数 + 2」。诊断从
/status/tsdb页面开始(零成本),再用topk(20, count by (__name__)(...))深挖(很贵,只在排障时用)。 - 高基数标签黑名单:用户/请求/会话 ID、邮箱手机号、完整 URL、错误消息原文、客户端 IP、容器 ID。判断标准是「取值集合会随业务量无限增长吗」。降维(
user_tier代替user_id、路由模板代替完整 URL)是标准解法。 - 三道防线:
metric_relabel_configs在入库前丢、sample_limit等硬限制兜底、基数趋势告警提前预警。注意labeldrop的regex匹配的是标签名且忽略source_labels。 - 容量公式:磁盘 = 保留秒数 × 摄入样本/秒 × 每样本字节数(1 到 2 字节),再乘 1.4 的安全系数。内存 = 序列数 × 3 到 8 KB,再留 3 到 4 倍给查询峰值。
GOMEMLIMIT是投入产出比最高的一个配置,它把硬 OOM 变成软降级。 - 抓取间隔是最大的性能旋钮,改一倍所有资源消耗都改一倍。30s 是大多数场景的最佳平衡点。间隔不要超过 2 分钟(
lookback-delta是 5 分钟),rate的区间至少是抓取间隔的 4 倍。 - 记录规则把「查询时计算」变成「写入时计算」。命名用
level:metric:operations。三条铁律:永远sum(rate(...))不是rate(sum(...));rate区间不小于规则 interval;有依赖的规则放同一 group 并按序书写。大 group 要拆,同组串行、跨组并行。 - 联邦只能拉聚合结果,用
match[]配合记录规则的命名前缀筛选。它解决不了长期存储、HA 和水平扩展。演进路径:治基数 → 加记录规则 → 按功能分片 → 最后才上 Thanos/Mimir。 prometheus_rule_group_iterations_missed_total是最该盯的自监控指标——规则求值被跳过意味着告警已经失效,而且是无声的。查询和规则求值共享并发配额,必要时降低--query.max-concurrency来保护规则。- 告警疲劳可以量化:日均告警量、critical 日均量、单条告警占比、长期 firing 条数。判断一条告警级别只需要问「凌晨 3 点响了我会起床吗」。用 SLO 告警替代阈值告警是最有效的减噪手段。
- 告警是「事件」不是「状态」。 长期 firing 的告警一定是设计错误。「磁盘 80% 满」是状态,「24 小时内会写满」才是事件。
- 治理成果必须靠机制维持:用告警监控告警(
AlertIsTooNoisy、AlertFiringTooLong)、CI 检查 runbook、每月 15 分钟评审。靠自觉的治理,三个月必然退化。
这一章的所有内容可以浓缩成一句话:Prometheus 的运维,本质上是在管理三种预算——基数预算、资源预算、和人的注意力预算。 三者都是有限的,都会被「多加一点点」的心态慢慢耗尽,也都需要明确的机制来守住。