4. Prometheus核心概念:数据模型、Metric类型与Labels设计
大家好,我是你们的老朋友。今天我们来聊聊Prometheus最核心的几个概念。说实话,我见过不少团队,Prometheus装上了,Grafana也配好了,但监控数据就是不对劲——要么指标爆炸,要么查不出问题。说白了,就是没搞懂数据模型和Metric类型的设计哲学。
我个人习惯,在讲任何监控系统之前,先让大家理解它的数据长什么样。Prometheus的数据模型,是它所有能力的基石。
4.1 数据模型:时间序列的骨架
Prometheus存储的每一个监控数据点,本质上都是一个时间序列。什么叫时间序列?就是同一个指标,在不同时间点上的取值序列。
一个完整的时间序列由三部分组成:
- Metric Name(指标名):比如
http_requests_total,描述“是什么”。 - Labels(标签):一组键值对,比如
{method="GET", endpoint="/api/v1/users"},描述“是谁的”。 - Sample(样本):一个具体的时间戳 + 一个值。
举个例子:
http_requests_total{method="POST", endpoint="/api/v1/orders", status="200"} 1024 @ 1700000000
这条数据的意思是:在时间戳1700000000这一刻,POST请求到/api/v1/orders接口,返回200状态码的请求总数是1024次。
核心要点:Prometheus的存储引擎,就是围绕“指标名+标签组合”来建立索引的。你查询时,其实是在筛选这些标签组合。
我记得有一次,团队里新来的同事把所有请求都写到一个指标里,没有加任何标签。结果想查某个接口的延迟,完全没法区分。嗯,这就是典型的“数据模型没设计好,后面全白干”。
4.2 Metric类型:四种武器
Prometheus提供了四种核心的Metric类型。每种类型解决一类特定的监控问题。我按使用频率来介绍。
4.2.1 Counter(计数器)
Counter只增不减。它用来记录累计值,比如请求总数、错误总数、CPU时间片总数。
你想想看,什么场景下数据是只增不减的?
- HTTP请求总数
- 数据库查询次数
- 任务执行失败次数
代码示例:
// 定义一个Counter指标
var requestCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests.",
},
[]string{"method", "endpoint"},
)
// 使用
requestCounter.WithLabelValues("GET", "/api/v1/users").Inc()
避坑指南:我曾经见过有人用Counter来记录当前在线人数。结果重启服务后,Counter归零了,在线人数突然变成0。这明显用错了类型。在线人数应该用Gauge。
4.2.2 Gauge(仪表盘)
Gauge可增可减。它用来记录瞬时值,比如当前内存使用量、当前CPU温度、当前活跃连接数。
说白了,Gauge就是“现在是多少”的问题。
// 定义一个Gauge指标
var memoryUsage = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "memory_usage_bytes",
Help: "Current memory usage in bytes.",
},
)
// 使用
memoryUsage.Set(1024 * 1024 * 256) // 设置为256MB
memoryUsage.Add(1024) // 增加1KB
memoryUsage.Sub(512) // 减少512B
我个人习惯,Gauge指标通常配合avg_over_time这类函数使用,来观察一段时间内的波动情况。
4.2.3 Histogram(直方图)
Histogram用来统计数据的分布情况。它把数据分到不同的桶(Bucket)里,然后统计每个桶里的样本数量。
最常见的场景:请求延迟分布。
比如,你想知道:
- 有多少请求在10ms以内完成?
- 有多少请求在10ms~50ms之间?
- 有多少请求超过了100ms?
代码示例:
// 定义一个Histogram指标,桶边界为:5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1000ms
var requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Histogram of HTTP request durations.",
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0},
},
[]string{"method", "endpoint"},
)
// 使用
requestDuration.WithLabelValues("GET", "/api/v1/users").Observe(0.032) // 记录一个32ms的请求
重要:Histogram会暴露三个时间序列:_bucket(桶计数)、_sum(总和)、_count(总数)。你可以用histogram_quantile函数计算百分位数,比如P99延迟。
我在项目中遇到过一个问题:桶的边界设置得太稀疏,导致大部分请求都落在同一个桶里,完全看不出分布特征。后来我调整了桶的分布,才真正看清了延迟的“长尾效应”。
4.2.4 Summary(摘要)
Summary和Histogram类似,也是用来计算百分位数的。但区别在于:Summary在客户端直接计算百分位数,而Histogram把原始数据发给服务端再计算。
什么时候用Summary?
- 你明确知道需要哪些百分位数(比如P50、P90、P99)
- 你不想在服务端做额外的计算
代码示例:
// 定义一个Summary指标,计算P50和P99
var requestDurationSummary = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "http_request_duration_seconds_summary",
Help: "Summary of HTTP request durations.",
Objectives: map[float64]float64{
0.5: 0.05, // P50,允许误差5%
0.99: 0.001, // P99,允许误差0.1%
},
},
[]string{"method", "endpoint"},
)
// 使用
requestDurationSummary.WithLabelValues("GET", "/api/v1/users").Observe(0.032)
注意:Summary的百分位数是固定的,你无法在查询时动态改变。而Histogram可以灵活计算任意百分位数。所以,除非你有明确的性能要求,否则我建议优先使用Histogram。
4.3 Labels设计:监控系统的灵魂
Labels是Prometheus最强大的特性,也是最容易踩坑的地方。我见过太多因为Labels设计不合理,导致存储爆炸、查询缓慢的案例。
先看一个反面教材:
http_requests_total{method="GET", endpoint="/api/v1/users", user_id="12345", session_id="abc-def-ghi"}
这里把user_id和session_id作为标签。你想想看,每个用户每次请求都会产生一个新的时间序列。如果系统有10万用户,这个指标就会产生10万个时间序列。Prometheus的存储压力会急剧上升。
正确的做法是:
http_requests_total{method="GET", endpoint="/api/v1/users"}
把user_id和session_id作为日志内容,而不是标签。
4.3.1 Labels设计原则
我总结了几条原则,供大家参考:
| 原则 | 说明 | 示例 |
|---|---|---|
| 基数控制 | 每个标签的取值数量不要超过1000 | status_code(200, 404, 500)✅ vs user_id(10万种)❌ |
| 可枚举性 | 标签值应该是可枚举的,而不是无限的 | method(GET, POST, PUT)✅ vs request_body(无限)❌ |
| 查询导向 | 你将来想怎么查,就怎么设计标签 | 按region、service、instance分组查询 |
| 避免冗余 | 不要包含可以从其他标签推导出的信息 | 有instance标签,就不需要host标签 |
我的经验:我曾经接手过一个系统,一个指标上有15个标签,每个标签都有几百种取值。结果这个指标产生了上亿个时间序列,Prometheus直接OOM了。后来我们砍掉了8个不必要的标签,系统才稳定下来。
4.3.2 常见的Labels设计模式
这里给出几个我常用的标签设计模板:
# 服务级别
service_requests_total{service="user-service", instance="10.0.1.1:8080", method="GET", endpoint="/api/v1/users", status="200"}
# 基础设施级别
node_cpu_seconds_total{instance="10.0.1.1:9100", cpu="0", mode="user"}
# 业务级别
order_total{region="cn-east", payment_method="wechat", status="paid"}
你想想看,这些标签设计有什么共同点?
- 每个标签的取值都是有限的(服务名、实例IP、HTTP方法、状态码)
- 标签之间是正交的,没有冗余
- 查询时可以通过标签组合快速定位问题
4.4 知识体系总览
为了让大家更直观地理解本章的知识结构,我画了一张图:
这张图把本章的三个核心概念串联起来了。数据模型定义了“数据长什么样”,Metric类型决定了“数据怎么用”,Labels设计则影响了“数据好不好查”。三者缺一不可。
4.5 小结
好了,今天的内容就到这里。我最后再啰嗦一句:监控系统的设计,80%的工作在数据建模阶段。花时间把数据模型、Metric类型和Labels设计好,后面运维会轻松很多。
如果你在项目中遇到监控数据爆炸、查询缓慢的问题,不妨回头看看Labels设计是否合理。我曾经就因为一个高基数的标签,把整个Prometheus集群搞垮过。嗯,那种教训,一次就够了。