Prometheus 只用四种指标类型就能描述世间万物的监控需求:Counter、Gauge、Histogram、Summary。
这看似简单,但背后藏着一整套设计哲学:
- 如何用最简单的模型描述最复杂的系统?
- 如何在灵活性和约束之间找到平衡?
- 为什么"允许 Counter 重置"反而是更优雅的设计?
这不是一篇工具手册,而是一次对 Prometheus 设计智慧的深度探讨。当你理解了这套设计哲学,你就能理解为什么 Prometheus 能用如此简单的模型,看透复杂系统的本质。
目录
1. 设计哲学:简单优先的三个体现
Prometheus 的设计哲学可以用一句话概括:用最简单的方式解决问题。
这个哲学在三个关键设计上体现得淋漓尽致。
1.1 时间序列的本质
Prometheus 是一个时间序列数据库(TSDB)。理解它的第一步,是理解什么是时间序列。
时间序列的定义
时间序列就是"数值随时间变化的记录"。更准确地说,它是一个三元组:
scss
(指标名, 标签集合, 时间点) → 数值
当你访问 Exporter 的 /metrics 端点时,看到的是这样的文本:
ini
# HELP http_requests_total 总HTTP请求数
# TYPE http_requests_total counter
http_requests_total{method="GET", status="200"} 1234
http_requests_total{method="POST", status="200"} 567
http_requests_total{method="GET", status="404"} 42
每一行都是一个"数据点",但这里有个问题:时间戳在哪?
时间戳由谁添加?
答案是:Prometheus 在抓取时添加时间戳。
流程是这样的:
- Exporter 暴露指标:只说"现在的值是 1234"
- Prometheus 定时抓取(默认 15 秒一次):记录"我在 2024-12-29 10:00:00 抓到的值是 1234"
- 存储到 TSDB:形成时间序列
为什么这样设计?
这个设计看似简单,但很巧妙:
| 如果由 Exporter 提供时间戳 | 如果由 Prometheus 添加时间戳 |
|---|---|
| 100 个 Exporter,100 个时钟 | 1 个 Prometheus,1 个时钟 |
| 时钟可能不同步 | 时钟统一 |
| Exporter 需要管理时间戳 | Exporter 无状态,简单 |
| 时间戳可能错误 | 时间戳可靠 |
这是 Prometheus "简单优先" 设计哲学的第一个体现:让 Exporter 尽可能简单,复杂的事情由 Prometheus 来做。
1.2 "类型"只是约定,不是强制
看看 /metrics 输出的第二行:
bash
# TYPE http_requests_total counter
这里声明了 http_requests_total 是一个 counter 类型。那么问题来了:Prometheus 真的在乎这个类型声明吗?
答案是:不在乎。
客户端、传输、存储三个视角
让我们从三个层面看"类型":
1. 客户端层(有类型)
在代码里,你用官方库定义指标时,类型是有意义的:
go
// Go 代码示例
counter := prometheus.NewCounter(...) // Counter 类型
counter.Inc() // 只能增加,不能减少
counter.Dec() // 编译错误!Counter 没有 Dec() 方法
gauge := prometheus.NewGauge(...) // Gauge 类型
gauge.Set(42) // 可以随意设置
gauge.Inc() // 也可以增加
gauge.Dec() // 也可以减少
客户端库通过 API 的设计来"强制"类型约束。
2. 传输层(类型是注释)
但到了 /metrics 输出,类型就只是一行注释:
bash
# TYPE my_counter counter ← 这只是给人看的注释
my_counter 100
my_counter 50 ← Prometheus 不会因为 Counter 减少而报错
3. 存储层(无类型)
到了 Prometheus 的存储层,所有指标都是"时间序列":
less
my_counter{}
@1735462800 → 100
@1735462815 → 50
@1735462830 → 75
Prometheus 看到的就是"一串数字随时间变化",它不知道也不在乎这个指标声明的类型是什么。
为什么这样设计?
这又是 Prometheus "简单优先" 的体现:
- 存储引擎简单:不需要复杂的类型系统,只需要存 (timestamp, value) 对
- 查询灵活:你可以对任何指标用任何函数(虽然可能没意义)
- 性能更好:不需要在查询时检查类型兼容性
- 向后兼容:后续添加新类型不影响存储层
那类型有什么用?
类型的作用在于约定 和文档:
- 帮助你选择正确的 API(客户端层)
- 帮助你选择正确的函数 (比如 Counter 用
rate(),Gauge 用delta()) - 帮助工具理解指标(比如 Grafana 可以根据类型提供不同的可视化选项)
但这一切都是"软约束",不是"硬限制"。
1.3 Counter 可以重置:为什么允许?
这是 Prometheus 设计哲学中最容易引起困惑的一点。
一个真实的场景
你的应用运行在 Kubernetes 上,有个 Counter 指标 http_requests_total:
makefile
10:00:00 → 1000
10:01:00 → 1500
10:02:00 → Pod 重启了
10:02:15 → 0 ← Counter 重置为 0
10:03:00 → 50
这时候,有人可能会问:为什么不把 Counter 持久化,重启后继续累加?
比如可以这样设计:
ini
Pod 重启前:http_requests_total = 1500
写入磁盘:1500
Pod 重启后:从磁盘读取 1500
继续累加:http_requests_total = 1500 + 新请求数
为什么 Prometheus 不这么做?
让我们对比两种方案:
| 方案 | 持久化 Counter | 允许重置 |
|---|---|---|
| Exporter 复杂度 | 需要文件/数据库存储 | 无状态,简单 |
| 容器友好性 | 需要挂载持久化存储 | 容器可以随意销毁重建 |
| 故障处理 | 文件损坏?并发冲突? | 没有状态,没有故障 |
| 监控目的 | 精确累积值 | 速率和趋势 |
Prometheus 的选择是:允许重置,专注于速率。
这个选择背后的哲学是:
"Prometheus is designed for operational monitoring, not for billing or accounting."
翻译一下:
Prometheus 是为运维监控而设计的,不是为计费或会计核算而设计的。
监控 vs 计费的区别
| 监控关心的问题 | 计费关心的问题 |
|---|---|
| 现在请求速率是多少? | 总共处理了多少个请求? |
| 错误率是否在上升? | 总共有多少个错误? |
| 系统是否健康? | 用户应该付多少钱? |
监控只需要知道:
- 每秒处理多少请求(速率)
- 速率是否异常(告警)
- 趋势是上升还是下降
不需要知道:
- 从系统上线到现在总共处理了 1,234,567,890 个请求
后者应该由数据库事务、日志系统、或专门的计费系统来保证。
rate() 如何处理重置
Prometheus 的 rate() 函数会自动检测 Counter 重置:
promql
rate(http_requests_total[5m])
原理很简单:
- 检测重置:如果当前值 < 上一个值,认为发生了重置
- 调整计算:只用重置后的数据计算速率
- 继续正常:下一个周期正常计算
举例:
ini
时间 值 rate() 计算
10:00 1000
10:01 1500 (1500-1000)/60 = 8.33/s
10:02 0 检测到重置,跳过
10:03 50 (50-0)/60 = 0.83/s
10:04 120 (120-50)/60 = 1.16/s
所以即使 Counter 重置,rate() 依然能给出正确的速率。
总结这一节
Prometheus 允许 Counter 重置,是因为:
- 简化 Exporter 设计:无状态更简单、更可靠
- 适应云原生:容器随时重启是常态
- 专注监控本质:关心速率和趋势,不关心精确累积值
- rate() 能处理:自动检测和调整
这就是 Prometheus 的第一个设计智慧:牺牲一点"精确性",换取"简单性"和"可靠性"。
这个取舍背后的深层思考是:监控系统的价值不在于"记录了多少数字",而在于"能否及时发现问题"。只要速率和趋势是对的,绝对累积值不那么重要。
2. 四种指标:用最少的类型描述最多的场景
如果让你设计一套指标类型,你会设计几种?
可能会想:HTTP 请求一种、数据库查询一种、内存使用一种、CPU 使用一种...很快就会有几十种类型。
但 Prometheus 只用四种:Counter、Gauge、Histogram、Summary。
这是如何做到的?答案是:抓住本质,而不是现象。
- Counter 的本质:只增不减的累积
- Gauge 的本质:可增可减的状态
- Histogram 的本质:分布统计
- Summary 的本质:预计算
只要理解了本质,一个 Counter 就能描述所有"累积"的场景。
2.1 Counter(计数器)
定义
Counter 是单调递增的计数器,只增不减(除非重置)。
典型场景
回答"发生了多少次"的问题:
ini
真实场景:API 服务
http_requests_total{method="GET", path="/api/users", status="200"} 15234
http_requests_total{method="GET", path="/api/users", status="404"} 42
http_requests_total{method="POST", path="/api/users", status="200"} 8901
http_requests_total{method="POST", path="/api/users", status="400"} 156
error_log_total{level="error", service="user-service"} 234
error_log_total{level="error", service="order-service"} 89
db_queries_total{database="mysql", operation="select"} 89234
db_queries_total{database="mysql", operation="insert"} 12456
注意几个设计细节:
- 命名后缀
_total:这是约定俗成的规范,表示"总数" - 标签选择 :
method、path、status是低基数的标签,不会无限增长 - 不要用高基数标签 :比如不要用
user_id、request_id
常用函数
1. rate() - 计算每秒速率
promql
# 过去 5 分钟的平均每秒请求速率
rate(http_requests_total[5m])
# 结果示例
http_requests_total{method="GET", path="/api/users"} 150.5
→ 表示过去 5 分钟,这个接口平均每秒处理 150.5 个请求
为什么几乎总是用 rate()?因为 Counter 的原始值(比如 15234)没什么意义,你关心的是"现在每秒多少个请求"。
2. increase() - 计算时间窗口内的增量
promql
# 过去 1 小时增加了多少个请求
increase(http_requests_total[1h])
# 结果示例
http_requests_total{method="GET"} 540000
→ 过去 1 小时,增加了 54 万个请求
注意 increase() 和 rate() 的关系:
scss
increase(metric[1h]) = rate(metric[1h]) × 3600
3. topk() - 找出前 N 名
promql
# 找出请求量最大的前 10 个接口
topk(10, rate(http_requests_total[5m]))
# 找出错误最多的前 5 个服务
topk(5, increase(error_log_total[1h]))
实战技巧
错误示例 1:直接用 Counter 的原始值
promql
# 错误:直接看 Counter 的值没什么意义
http_requests_total > 10000
# 正确:看速率是否过高
rate(http_requests_total[5m]) > 100
错误示例 2:时间窗口太短
promql
# 问题:1 分钟窗口太短,容易受瞬时波动影响
rate(http_requests_total[1m])
# 建议:至少用 5 分钟窗口
rate(http_requests_total[5m])
错误示例 3:在高基数标签上使用 Counter
promql
# 错误:user_id 有百万级别,会产生百万条时间序列
http_requests_total{user_id="123456"}
# 正确:只按服务和接口聚合
http_requests_total{service="api", path="/users"}
Counter 的设计智慧
Counter 的设计看似简单(只增不减),但蕴含着深刻的智慧:
- 专注本质:监控关心"现在怎么样"(速率),不关心"总共多少"(累积值)
- 容忍重置:牺牲绝对精确,换取系统简单和可靠
- 配合 rate():设计了配套函数来处理重置,让用户无感知
这就是"简单模型看透复杂系统"的第一个例子:一个只增不减的计数器,配合一个 rate() 函数,就能描述世间所有"事件累积"的场景。
2.2 Gauge(仪表盘)
定义
Gauge 是可以任意增减的指标,反映"当前状态"。
典型场景
回答"现在是多少"的问题:
ini
真实场景:服务器监控
node_memory_MemAvailable_bytes{instance="server-01"} 8589934592
node_cpu_usage_percent{instance="server-01", cpu="0"} 45.2
node_filesystem_free_bytes{instance="server-01", mountpoint="/"} 107374182400
真实场景:应用监控
current_connections{service="database", pool="main"} 45
current_connections{service="database", pool="readonly"} 12
queue_length{service="message-queue", queue="orders"} 156
goroutine_count{service="api-server"} 234
真实场景:业务监控
current_online_users{region="asia"} 15234
current_online_users{region="europe"} 8901
inventory_count{product="iphone-15", warehouse="shanghai"} 450
Gauge vs Counter 的选择
这是个常见的困惑。怎么判断用哪个?
| 问题 | 类型 | 原因 |
|---|---|---|
| 总共处理了多少请求? | Counter | 只增不减 |
| 现在有多少活跃连接? | Gauge | 可增可减 |
| 总共发生了多少错误? | Counter | 只增不减 |
| 现在队列里有多少任务? | Gauge | 可增可减 |
| CPU 时间累计多少秒? | Counter | 时间累积,只增不减 |
| CPU 使用率是多少? | Gauge | 百分比,可增可减 |
简单规则:
- 累积的、永远增长的 → Counter
- 当前的、可上可下的 → Gauge
常用函数
1. 直接查询当前值
promql
# 看当前内存使用
node_memory_MemAvailable_bytes
# 看当前连接数
current_connections
2. delta() - 计算变化量
promql
# CPU 温度在过去 2 小时的变化
delta(cpu_temp_celsius{host="server-01"}[2h])
# 内存使用在过去 1 小时的变化
delta(node_memory_usage_bytes[1h])
3. avg_over_time() - 计算平均值
promql
# 过去 5 分钟的平均 CPU 使用率
avg_over_time(node_cpu_usage_percent[5m])
# 过去 1 小时的平均队列长度
avg_over_time(queue_length[1h])
4. predict_linear() - 预测未来
这是个很强大的函数:
promql
# 基于过去 1 小时的数据,预测 4 小时后磁盘剩余空间
predict_linear(node_filesystem_free_bytes[1h], 4*3600)
# 如果预测值 < 0,说明磁盘会满
predict_linear(node_filesystem_free_bytes[1h], 4*3600) < 0
原理是简单线性回归:
- 看过去 1 小时的趋势
- 假设趋势延续
- 推算 4 小时后的值
实战技巧
技巧 1:Gauge 可以用 rate(),但要理解含义
promql
# 对 Gauge 用 rate() 是可以的
rate(queue_length[5m])
# 但含义是:队列长度的变化速率
# 如果结果是正数:队列在增长
# 如果结果是负数:队列在减少
技巧 2:内存"可用"vs"空闲"
promql
# 错误:MemFree 不是真正可用的内存
node_memory_MemFree_bytes
# 正确:MemAvailable 才是真正可用的
node_memory_MemAvailable_bytes
# 原因:Linux 会用空闲内存做 cache/buffer
# MemFree 只是完全未使用的
# MemAvailable = Free + 可回收的 cache
技巧 3:计算百分比
promql
# 内存使用率
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100
# 磁盘使用率
(1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100
Gauge 的设计智慧
Gauge 的本质是"快照":它记录的是某一时刻的状态。
这个设计的精妙之处在于:
- 不关心历史:只告诉你"现在是多少",过去的事情不重要
- 可增可减:真实反映系统状态的变化
- 天然支持预测:用 predict_linear() 可以基于趋势预测未来
这是"简单模型"的第二个例子:一个可以任意设置的数值,就能描述所有"当前状态"的场景。没有复杂的状态机,没有历史追踪,简单而直接。
2.3 Histogram(直方图)
为什么需要 Histogram
先看一个问题。
假设你的 API 平均响应时间是 100ms。那系统是否健康?
不知道。因为平均值会掩盖问题:
shell
场景 1:所有请求都是 100ms
→ 平均值 100ms,系统很稳定
场景 2:
90% 的请求是 10ms
10% 的请求是 910ms
→ 平均值还是 100ms,但有严重的长尾问题!
这就是著名的"平均值谎言"。你需要知道分布:
- 有多少请求在 50ms 内?
- 有多少请求在 100ms 内?
- 有多少请求在 500ms 内?
- 有多少请求超过 1 秒?
Histogram 就是来解决这个问题的。
Histogram 的结构
一个 Histogram 指标会生成多条时间序列:
ini
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.005"} 0
http_request_duration_seconds_bucket{le="0.01"} 0
http_request_duration_seconds_bucket{le="0.025"} 0
http_request_duration_seconds_bucket{le="0.05"} 50
http_request_duration_seconds_bucket{le="0.1"} 150
http_request_duration_seconds_bucket{le="0.25"} 180
http_request_duration_seconds_bucket{le="0.5"} 195
http_request_duration_seconds_bucket{le="1"} 198
http_request_duration_seconds_bucket{le="2.5"} 200
http_request_duration_seconds_bucket{le="5"} 200
http_request_duration_seconds_bucket{le="+Inf"} 200
http_request_duration_seconds_sum 45.67
http_request_duration_seconds_count 200
分解一下:
- 多个
_bucket:每个 bucket 是一个 Counter,记录"≤ 某个值"的请求数 _sum:所有请求的耗时总和(Counter)_count:总请求数(Counter)
两个关键理解
理解 1:bucket 是累积的
ini
le="0.05" → 50 表示:≤ 0.05 秒的请求有 50 个
le="0.1" → 150 表示:≤ 0.1 秒的请求有 150 个(包含前面的 50 个)
le="0.5" → 195 表示:≤ 0.5 秒的请求有 195 个(包含前面的 150 个)
所以:
- 0.05 ~ 0.1 秒之间的请求数 = 150 - 50 = 100 个
- 0.1 ~ 0.5 秒之间的请求数 = 195 - 150 = 45 个
理解 2:不存原始数据,存区间统计
Histogram 不会存每个请求的准确耗时:
makefile
不是这样存:
request_1: 0.234 秒
request_2: 0.156 秒
request_3: 0.089 秒
...
而是这样存:
≤ 0.05 秒: 50 个
≤ 0.1 秒: 150 个
≤ 0.5 秒: 195 个
...
用两个类比理解
类比 1:存的是等第,不是分数
yaml
不存每个学生的分数:
学生 A: 95 分
学生 B: 88 分
学生 C: 76 分
... (1000 个学生)
而是存分数段统计:
≥ 90 分(A 等): 150 人
≥ 80 分(B 等): 400 人(包含 A 等)
≥ 70 分(C 等): 700 人(包含 A+B 等)
≥ 60 分(D 等): 950 人
所有人: 1000 人
这样:
- 存储量从 1000 条记录 → 5 条记录
- 牺牲了精确度(不知道具体分数)
- 保留了分布信息(知道有多少人在各个分数段)
类比 2:数据库的 GROUP BY 聚合
sql
-- 不是存每条订单明细
SELECT order_id, amount FROM orders; -- 100 万行
-- 而是存金额区间统计
SELECT
CASE
WHEN amount <= 100 THEN '0-100'
WHEN amount <= 500 THEN '100-500'
WHEN amount <= 1000 THEN '500-1000'
END AS range,
COUNT(*) AS count
FROM orders
GROUP BY range; -- 只有几行
Histogram 就像是提前做好的 GROUP BY 聚合。
常用函数
histogram_quantile() - 计算分位数
promql
# 计算 P95(95 分位数)
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
# 计算 P99
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# 计算 P50(中位数)
histogram_quantile(0.5, rate(http_request_duration_seconds_bucket[5m]))
分位数的含义:
- P95 = 0.8 秒 → 95% 的请求在 0.8 秒内完成
- P99 = 1.5 秒 → 99% 的请求在 1.5 秒内完成
为什么必须配合 rate()?
因为 _bucket 是 Counter(累积的),你需要转成速率:
promql
# 错误:直接用 bucket
histogram_quantile(0.95, http_request_duration_seconds_bucket)
# 这会基于"总累积值"计算,结果是错的
# 正确:先 rate() 再计算分位数
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
# 这是基于"过去 5 分钟的速率"计算,结果才对
计算平均响应时间
promql
# 用 sum 和 count 计算平均值
rate(http_request_duration_seconds_sum[5m])
/
rate(http_request_duration_seconds_count[5m])
实战案例
假设你的 API 监控显示:
promql
# 平均响应时间
avg = 150ms
# P95
histogram_quantile(0.95, ...) = 200ms
# P99
histogram_quantile(0.99, ...) = 2000ms ← 注意这里!
分析:
- 平均 150ms 看起来不错
- P95 200ms 也还行
- 但 P99 2000ms 说明有严重的长尾问题
可能的原因:
- 某些请求触发了慢查询
- 某些请求有 GC 停顿
- 某些请求等待外部服务超时
这就是 Histogram 的价值:让长尾问题无所遁形。
Histogram 的设计智慧
Histogram 是四种类型中最复杂的,但也是设计最精妙的:
-
牺牲精确,换取效率
- 不存每个请求的准确耗时(太贵)
- 只存区间统计(够用)
- 从 1000 条数据压缩到 10 条
-
用空间换时间
- 提前分好区间(bucket)
- 查询时只需要聚合,不需要重新计算
- histogram_quantile() 基于线性插值,速度快
-
可聚合是关键
- 多个实例的 bucket 可以相加
- 这是 Histogram 比 Summary 更通用的原因
- 牺牲了客户端计算成本,换来了服务端灵活性
这是"简单模型看透复杂问题"的巅峰之作:用几个累积的计数器(bucket),配合一个聚合函数(histogram_quantile),就能看透整个系统的性能分布。
2.4 Summary(摘要)
定义
Summary 和 Histogram 类似,都是用来统计分布,但有个关键区别:分位数在哪计算。
Histogram vs Summary
| 对比维度 | Histogram | Summary |
|---|---|---|
| 分位数计算位置 | Prometheus(服务端) | Exporter(客户端) |
| 存储内容 | bucket 统计 | 预计算的分位数 |
| 查询时计算 | 需要用 histogram_quantile() | 直接读 |
| 能否聚合 | 能(多实例) | 不能 |
| 灵活性 | 可以算任意分位数 | 只能看预设的 |
| 客户端开销 | 小(只统计 bucket) | 大(要计算分位数) |
Summary 的结构
ini
# TYPE http_request_duration_seconds summary
http_request_duration_seconds{quantile="0.5"} 0.232
http_request_duration_seconds{quantile="0.9"} 0.821
http_request_duration_seconds{quantile="0.99"} 1.523
http_request_duration_seconds_sum 45.67
http_request_duration_seconds_count 200
注意:
- 没有
_bucket - 直接给出了
quantile="0.5"等分位数的值 - 这些分位数是在 Exporter 里算好的
用类比理解
scss
Histogram 方案:
Java 代码统计 bucket → 发送给 Prometheus → 查询时用 histogram_quantile() 计算
Summary 方案:
Java 代码直接计算 P50/P90/P99 → 发送给 Prometheus → 查询时直接读
就像:
ini
Histogram = 给你食材,你自己做菜
Summary = 直接给你做好的菜
Summary 为什么不能聚合?
这是个关键限制。
假设有 2 个实例:
ini
实例 1: P90 = 0.8 秒
实例 2: P90 = 1.2 秒
问:整体的 P90 是多少?
答案:不知道。
你不能简单地:
- 求平均:(0.8 + 1.2) / 2 = 1.0 ? 错!
- 取最大:max(0.8, 1.2) = 1.2 ? 错!
因为分位数不能这样算。只有原始数据(或 bucket 统计)才能正确聚合。
promql
# Histogram 可以聚合
histogram_quantile(0.9,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
)
# 先把多个实例的 bucket 加起来,再计算 P90
# Summary 不能聚合
sum(http_request_duration_seconds{quantile="0.9"})
# 这样做是错的!把多个实例的 P90 加起来毫无意义
什么时候用 Summary?
适合 Summary 的场景:
- 单实例应用:不需要聚合
- 查询性能敏感:Summary 查询更快(不需要计算)
- 分位数固定:你确定只需要 P50/P90/P99
典型例子:Go 程序的 GC 时间监控(官方库默认用 Summary)
ini
go_gc_duration_seconds{quantile="0"} 0.000042
go_gc_duration_seconds{quantile="0.25"} 0.000065
go_gc_duration_seconds{quantile="0.5"} 0.000077
go_gc_duration_seconds{quantile="0.75"} 0.000091
go_gc_duration_seconds{quantile="1"} 0.001752
Histogram vs Summary 选择建议
markdown
你的场景 → 推荐类型
----------------------------------------------
多实例,需要聚合 → Histogram
单实例 → Summary 或 Histogram
不确定需要哪些分位数 → Histogram
需要预测未来的分位数 → Histogram
客户端资源受限 → Histogram
查询性能要求极高 → Summary
实战建议:优先用 Histogram,除非有特殊原因。
Summary 的设计哲学
Summary 体现了一个有趣的权衡:客户端计算 vs 服务端灵活性。
- Histogram:客户端简单,服务端强大(可以算任意分位数)
- Summary:客户端复杂,服务端简单(只能看预设的分位数)
Prometheus 最终选择让 Histogram 成为主力,Summary 作为特殊场景的补充。这个选择背后的思考是:
监控系统应该把复杂性留在服务端(Prometheus),让客户端(Exporter)尽可能简单。
这又回到了开头的哲学:简单优先。即使 Summary 查询更快,但它牺牲了灵活性,也增加了客户端的复杂度。相比之下,Histogram 虽然查询时需要计算,但客户端更简单,也更通用。
四种类型的本质
到这里,我们可以总结四种类型的本质了:
| 类型 | 本质 | 设计智慧 |
|---|---|---|
| Counter | 只增的累积器 | 牺牲绝对精确,专注速率和趋势 |
| Gauge | 可变的快照 | 不关心历史,只关心当前 |
| Histogram | 分布的压缩 | 牺牲精确度,换取存储和计算效率 |
| Summary | 预计算的结果 | 客户端复杂,服务端简单 |
四种类型,覆盖了监控的所有场景:
- 需要累积?Counter
- 需要当前状态?Gauge
- 需要看分布?Histogram
- 需要极致性能?Summary
这就是"用最少的类型描述最多的场景"的智慧。
3. Histogram:如何优雅地处理分布问题
3.1 高基数问题
这是使用 Histogram 时最容易踩的坑。
一个 Histogram 会生成多少时间序列?
假设你有 10 个 bucket,那么一个 Histogram 指标会生成:
ini
10 个 _bucket + 1 个 _sum + 1 个 _count = 12 条时间序列
现在看看高基数问题:
场景 1:低基数标签
promql
http_request_duration_seconds_bucket{service="api", path="/users", le="0.1"}
标签组合:
- service: 5 个服务
- path: 20 个接口
- le: 10 个 bucket
总时间序列数:
ini
5 × 20 × 10 = 1000 条(只是 bucket)
1000 + 5×20×2 = 1200 条(加上 sum 和 count)
这完全可以接受。
场景 2:高基数标签
promql
# 错误示例!
http_request_duration_seconds_bucket{user_id="123456", le="0.1"}
标签组合:
- user_id: 100 万用户
- le: 10 个 bucket
总时间序列数:
ini
1,000,000 × 10 = 10,000,000 条(只是 bucket)
10,000,000 + 1,000,000 × 2 = 12,000,000 条(加上 sum 和 count)
这会直接把 Prometheus 打爆!
高基数放大效应
对比 Gauge 和 Histogram:
| 标签组合数 | Gauge | Histogram (10 bucket) | 放大倍数 |
|---|---|---|---|
| 100 | 100 | 1,200 | 12x |
| 1,000 | 1,000 | 12,000 | 12x |
| 10,000 | 10,000 | 120,000 | 12x |
| 100,000 | 100,000 | 1,200,000 | 12x |
Histogram 把基数放大了 (bucket数 + 2) 倍!
如何避免?
- 不要在高基数标签上用 Histogram
promql
# 错误
http_duration{user_id="..."} # user_id 是高基数
http_duration{ip="..."} # IP 是高基数
http_duration{trace_id="..."} # trace_id 是高基数
# 正确
http_duration{service="...", path="..."} # 低基数
- 用路径模板代替具体路径
promql
# 错误
http_duration{path="/api/users/123456"} # 每个用户ID一个路径
# 正确
http_duration{path="/api/users/:id"} # 模板化路径
- 减少 bucket 数量
go
// 不要设置太多 bucket
buckets := []float64{
0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1,
0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0,
} // 15 个 bucket,太多了!
// 简化到必要的 bucket
buckets := []float64{0.1, 0.5, 1.0, 5.0} // 4 个 bucket 够用
3.2 bucket 边界如何设置?
这是个常见问题:bucket 的 le 值应该设多少?
原则 1:覆盖你关心的范围
比如你的 API:
- 正常情况下:50-200ms
- 可接受范围:< 500ms
- 超时设置:5 秒
那么 bucket 可以设置为:
go
buckets := []float64{0.05, 0.1, 0.2, 0.5, 1, 5}
// 覆盖了 50ms, 100ms, 200ms, 500ms, 1s, 5s
原则 2:分布要合理
go
// 不好:集中在小范围
buckets := []float64{0.1, 0.11, 0.12, 0.13, 10}
// 好:均匀分布
buckets := []float64{0.1, 0.5, 1, 5, 10}
原则 3:考虑计算精度
分位数计算是基于线性插值的,bucket 越密集,精度越高:
ini
bucket 设置: [0.1, 1.0, 10.0]
如果 P95 落在 1.0 - 10.0 之间
Prometheus 会用线性插值估算
精度较低(跨度太大)
bucket 设置: [0.1, 0.5, 1.0, 2.0, 5.0, 10.0]
如果 P95 落在 1.0 - 2.0 之间
精度会更高(跨度小)
Prometheus 默认的 bucket
Prometheus 官方库的默认 bucket 是:
go
[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
这个设置覆盖了从 5ms 到 10s 的范围,对大多数 API 来说够用。
3.3 _sum 和 _count 的作用
Histogram 的 _sum 和 _count 不是强制的,但强烈建议有。
作用 1:计算平均值
promql
rate(http_request_duration_seconds_sum[5m])
/
rate(http_request_duration_seconds_count[5m])
如果没有 _sum 和 _count,你算不出平均值。
作用 2:计算请求速率
promql
rate(http_request_duration_seconds_count[5m])
这等价于:
promql
rate(http_requests_total[5m])
所以 _count 其实就是个 Counter,记录请求总数。
作用 3:检查数据一致性
promql
# _count 应该等于最大的 bucket
http_request_duration_seconds_count
==
http_request_duration_seconds_bucket{le="+Inf"}
如果不相等,说明数据有问题。
官方客户端库会自动生成
用官方库(Go、Java、Python)不用操心,它们会自动维护 _sum 和 _count:
go
// Go 代码
histogram := prometheus.NewHistogram(...)
histogram.Observe(0.234) // 自动更新 _bucket、_sum、_count
只有手写 Exporter 时才需要注意。
Histogram 设计的深层思考
Histogram 的三个设计细节(bucket 累积、高基数问题、_sum/_count)都指向一个核心思想:
在性能、精度、灵活性之间找到最佳平衡点。
- bucket 累积:让聚合变简单(直接相加)
- 控制高基数:避免存储爆炸
- _sum 和 _count:用两个额外的 Counter 就能算平均值
没有一个设计是完美的,但 Histogram 找到了一个"足够好"的平衡点:
- 牺牲一点精确度(不知道准确耗时)
- 换取存储效率(压缩 1000 倍)
- 保留灵活性(可以算任意分位数)
- 支持聚合(多实例可以合并)
这就是"简单模型"的力量:用简单的规则(累积的 bucket),解决复杂的问题(性能分布)。
4. 易混淆概念:细节中的设计智慧
4.1 rate() vs irate()
这是最容易混淆的一对函数。
计算方式的区别
promql
rate(http_requests_total[5m]) # 用 5 分钟窗口内所有点计算平均速率
irate(http_requests_total[5m]) # 只用最近两个点计算瞬时速率
举个例子
假设过去 5 分钟的 Counter 值:
makefile
时间 值
10:00 1000
10:01 1100 (+100)
10:02 1200 (+100)
10:03 1300 (+100)
10:04 1400 (+100)
10:05 1600 (+200) ← 最后 1 分钟增加了 200
rate() 的计算:
ini
总增量 = 1600 - 1000 = 600
时间跨度 = 5 分钟 = 300 秒
rate = 600 / 300 = 2.0/s
irate() 的计算:
ini
最近两个点的增量 = 1600 - 1400 = 200
时间间隔 = 60 秒
irate = 200 / 60 = 3.33/s
曲线对比
makefile
rate(): 平滑曲线
时间 值
10:00 2.0
10:01 2.0
10:02 2.0
10:03 2.0
10:04 2.0
10:05 2.0 ← 仍然是平均值
irate(): 敏感曲线
时间 值
10:00 1.67
10:01 1.67
10:02 1.67
10:03 1.67
10:04 1.67
10:05 3.33 ← 立即反应了突增
何时用哪个?
| 场景 | 推荐 | 原因 |
|---|---|---|
| 告警规则 | rate() | 避免毛刺导致误报 |
| 容量规划 | rate() | 需要长期趋势 |
| 实时监控 Dashboard | irate() | 快速反应变化 |
| SLO 计算 | rate() | 需要平滑数据 |
| 调试问题 | irate() | 需要看瞬时情况 |
一个实战建议
在 Grafana Dashboard 上:
- 概览页面用
rate()(看整体趋势) - 详情页面用
irate()(看实时波动)
4.2 increase() vs rate()
定义
promql
increase(http_requests_total[1h]) # 返回 1 小时的增量(总数)
rate(http_requests_total[1h]) # 返回每秒的速率
换算关系
scss
increase(metric[1h]) = rate(metric[1h]) × 3600
因为 1 小时 = 3600 秒。
实际例子
promql
http_requests_total
时间 值
09:00 1000
10:00 2000
increase(http_requests_total[1h]) = 2000 - 1000 = 1000
→ 1 小时增加了 1000 个请求
rate(http_requests_total[1h]) = (2000 - 1000) / 3600 = 0.278
→ 平均每秒 0.278 个请求
什么时候用哪个?
promql
# 你想知道"1 小时内总共处理了多少请求"
increase(http_requests_total[1h])
# 你想知道"平均每秒处理多少请求"
rate(http_requests_total[1h])
# 告警:每秒请求超过 100
rate(http_requests_total[5m]) > 100
# 告警:1 小时内错误超过 1000 个
increase(error_total[1h]) > 1000
4.3 histogram_quantile() 的常见误区
误区 1:不用 rate()
promql
# 错误
histogram_quantile(0.95, http_request_duration_seconds_bucket)
# 正确
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
为什么?因为 bucket 是 Counter(累积值),你需要转成速率:
ini
bucket 的原始值:
le="0.1" 1000 ← 累积有 1000 个
le="0.5" 5000 ← 累积有 5000 个
rate() 后:
le="0.1" 2.5 ← 每秒 2.5 个
le="0.5" 10 ← 每秒 10 个
histogram_quantile 需要"每秒多少个"才能算对
误区 2:丢掉 le 标签
promql
# 错误:聚合时丢掉了 le
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m]))
)
# 正确:by (le) 保留 le 标签
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
)
为什么?因为 histogram_quantile() 需要 le 标签来知道每个 bucket 的边界。
误区 3:对非 bucket 指标使用
promql
# 错误:这不是 histogram
histogram_quantile(0.95, http_requests_total)
# histogram_quantile 只能用于 _bucket 指标
4.4 sum() vs avg() vs rate()
这三个函数经常一起用,容易混淆。
sum() - 求和
promql
# 把所有实例的请求数加起来
sum(rate(http_requests_total[5m]))
avg() - 求平均
promql
# 计算所有实例的平均请求速率
avg(rate(http_requests_total[5m]))
区别
bash
场景:3 个实例
实例 1: 100 req/s
实例 2: 200 req/s
实例 3: 300 req/s
sum() = 100 + 200 + 300 = 600 req/s ← 总请求速率
avg() = (100 + 200 + 300) / 3 = 200 req/s ← 平均每个实例的速率
什么时候用哪个?
promql
# 你想知道"整个集群每秒处理多少请求"
sum(rate(http_requests_total[5m]))
# 你想知道"平均每个实例每秒处理多少请求"
avg(rate(http_requests_total[5m]))
5. 生产环境的实战经验
5.1 指标命名规范
基本格式
xml
<namespace>_<name>_<unit>_<suffix>
示例:
bash
node_memory_MemAvailable_bytes # 服务器内存
http_requests_total # HTTP 请求
http_request_duration_seconds # HTTP 耗时
mysql_queries_total # MySQL 查询
redis_memory_used_bytes # Redis 内存
规范细节
| 部分 | 说明 | 示例 |
|---|---|---|
| namespace | 所属系统 | node, http, mysql, redis |
| name | 指标含义 | requests, queries, memory |
| unit | 单位 | bytes, seconds, ratio |
| suffix | 类型后缀 | _total, _bucket, _count |
单位规范
- 时间:
_seconds(不要用 ms 或 minutes) - 大小:
_bytes(不要用 KB 或 MB) - 比例:
_ratio(0-1)或_percent(0-100)
promql
# 好
http_request_duration_seconds 0.234
memory_usage_bytes 8589934592
cpu_usage_ratio 0.85
# 不好
http_request_duration_ms 234
memory_usage_mb 8192
cpu_usage_percent 85
为什么统一用基本单位?
- 避免单位换算错误
- PromQL 可以用
* 1000等转换显示
5.2 标签设计原则
原则 1:低基数
promql
# 好:低基数标签
http_requests{method="GET", status="200"}
→ method 有 5 个值(GET/POST/PUT/DELETE/PATCH)
→ status 有 10 个值(200/404/500...)
→ 总组合 5 × 10 = 50
# 坏:高基数标签
http_requests{user_id="123456"}
→ user_id 有百万个值
→ 百万条时间序列
原则 2:有意义
promql
# 好:标签有实际含义
http_requests{service="api", path="/users"}
# 坏:标签没什么用
http_requests{server_index="1", cluster_id="abc123"}
原则 3:路径模板化
promql
# 坏:每个用户一个路径
http_duration{path="/api/users/123456"}
http_duration{path="/api/users/789012"}
...
# 好:用模板
http_duration{path="/api/users/:id"}
原则 4:不要用标签存数值
promql
# 坏:把数值放标签里
http_requests{duration="0.234"}
# 好:用专门的 Histogram 指标
http_request_duration_seconds_bucket{le="0.5"}
5.3 常见的生产问题
问题 1:Counter 突然下降怎么办?
现象:
makefile
http_requests_total
10:00 1000
10:01 1500
10:02 500 ← 突然下降了!
可能原因:
- Pod/容器重启了(正常)
- Prometheus 抓取错误(问题)
- 应用 bug(问题)
排查:
promql
# 看 up 指标,确认是否抓取成功
up{job="my-app"}
# 看是否有重启
kube_pod_container_status_restarts_total
问题 2:Histogram 的 P99 突然飙升
现象:
makefile
P50: 100ms(正常)
P90: 200ms(正常)
P99: 5000ms(异常!)
这说明有 1% 的请求特别慢,可能原因:
- 数据库慢查询
- 缓存未命中
- GC 停顿
- 下游服务超时
排查:
promql
# 看慢的是哪个接口
topk(5,
histogram_quantile(0.99,
rate(http_duration_bucket[5m])
)
)
# 对比 P50 和 P99 的差异
histogram_quantile(0.99, ...)
/
histogram_quantile(0.5, ...)
# 如果比值 > 10,说明有严重长尾
问题 3:指标突然消失
现象:
ini
http_requests_total{service="api"}
10:00 有数据
10:05 没有数据了
可能原因:
- 服务挂了(
up == 0) - 标签变了(比如 service 名称改了)
- 指标被删除了
排查:
promql
# 看服务是否存活
up{job="my-app"}
# 看所有 http_requests_total 指标(不限制标签)
http_requests_total
# 看最近新出现的时间序列
time() - timestamp(http_requests_total) < 300
5.4 性能优化技巧
技巧 1:用 Recording Rules 预计算
如果一个查询很复杂,经常用到,可以用 Recording Rule 预先计算:
yaml
groups:
- name: api_rules
interval: 30s
rules:
- record: job:http_requests:rate5m
expr: sum(rate(http_requests_total[5m])) by (job)
- record: job:http_p95_seconds
expr: histogram_quantile(0.95,
sum(rate(http_duration_bucket[5m])) by (job, le)
)
然后查询时直接用:
promql
# 不用每次都算 rate() 和 sum()
job:http_requests:rate5m
# 不用每次都算 histogram_quantile()
job:http_p95_seconds
技巧 2:控制指标数量
promql
# 查看指标数量
count(http_requests_total)
# 查看哪个标签基数最高
count by (service) (http_requests_total)
count by (path) (http_requests_total)
如果某个标签基数太高,考虑:
- 去掉这个标签
- 合并标签值(比如把所有 5xx 归为一类)
- 用路径模板
技巧 3:合理设置抓取间隔
yaml
# prometheus.yml
scrape_configs:
- job_name: 'high-frequency'
scrape_interval: 15s # 变化快的指标
- job_name: 'low-frequency'
scrape_interval: 60s # 变化慢的指标
没必要所有指标都 15 秒抓一次。
生产实践的智慧
生产环境的经验告诉我们:理论再完美,也要经得起实战检验。
这一章的所有经验都指向一个原则:在理论和实际之间找到平衡。
- 命名规范:让人一眼看懂,而不是炫技
- 标签设计:低基数优先,宁简勿繁
- 问题排查:从简单到复杂,逐步缩小范围
- 性能优化:先用 Recording Rules 预计算,再考虑复杂方案
这些经验的背后,还是那个哲学:简单优先,够用就好。
6. 下一步:从理解到实践
通过这篇文章,我们深入探讨了 Prometheus 的设计哲学:
一种哲学:简单优先
- 牺牲一点精确性,换取简单和可靠
- 把复杂性留在服务端,让客户端简单
- 在性能、精度、灵活性之间找平衡
四个指标:用最少的类型描述最多的场景
- Counter:只增不减的累积器
- Gauge:可增可减的快照
- Histogram:分布的压缩
- Summary:预计算的结果
一个模型:如何看透复杂系统
Prometheus 用四种简单的指标类型,就能描述世间所有监控场景。这不是巧合,而是抓住了本质:
- 累积?用 Counter
- 状态?用 Gauge
- 分布?用 Histogram
- 性能?用 Summary
这就是"用简单模型看透复杂系统"的智慧。
但理解哲学只是第一步,接下来你需要知道:
如何设计一个 Exporter?
- node-exporter 是怎么设计的?
- 为什么 CPU 时间用 Counter,CPU 使用率用 Gauge?
- 为什么某些指标要预聚合?
如何在 Grafana 中构建复杂表达式?
- 如何计算 CPU 使用率?
- 如何计算内存可用率?
- 如何预测磁盘何时满?
如何开发自定义 Exporter?
- 监控数据库应该关注哪些指标?
- 如何选择合适的类型?
- 如何避免常见的坑?
让我们继续下一篇,以 node-exporter 为例,深入学习监控指标的设计艺术。
下一篇:《Prometheus Exporter 设计实战 ------ node-exporter 源码解析与 Grafana 集成》
附录:四种类型速查表
| 类型 | 用途 | 只增不减 | 典型场景 | 常用函数 |
|---|---|---|---|---|
| Counter | 累计次数 | 是 | 请求数、错误数、查询数 | rate(), increase() |
| Gauge | 当前状态 | 否 | CPU、内存、连接数、队列长度 | 直接读、delta() |
| Histogram | 分布统计 | 是(bucket) | API 响应时间、请求大小 | histogram_quantile() |
| Summary | 预计算分位数 | 是(count) | GC 时间(单实例) | 直接读 {quantile} |
选择决策树
css
你的问题是什么?
├─ 发生了多少次? → Counter
├─ 现在是多少? → Gauge
└─ 分布如何(有没有长尾)?
├─ 多实例需要聚合 → Histogram
└─ 单实例 → Summary 或 Histogram