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_idsession_id作为标签。你想想看,每个用户每次请求都会产生一个新的时间序列。如果系统有10万用户,这个指标就会产生10万个时间序列。Prometheus的存储压力会急剧上升。

正确的做法是:

http_requests_total{method="GET", endpoint="/api/v1/users"}

user_idsession_id作为日志内容,而不是标签。

4.3.1 Labels设计原则

我总结了几条原则,供大家参考:

原则 说明 示例
基数控制 每个标签的取值数量不要超过1000 status_code(200, 404, 500)✅ vs user_id(10万种)❌
可枚举性 标签值应该是可枚举的,而不是无限的 method(GET, POST, PUT)✅ vs request_body(无限)❌
查询导向 你将来想怎么查,就怎么设计标签 regionserviceinstance分组查询
避免冗余 不要包含可以从其他标签推导出的信息 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 知识体系总览

为了让大家更直观地理解本章的知识结构,我画了一张图:

Prometheus核心概念知识体系 数据模型 Metric Name + Labels + Sample 时间序列 = 指标 + 标签组合 Metric类型 Counter(只增不减) Gauge(可增可减) Histogram(分布统计) Summary(预计算分位数) Labels设计 基数控制 可枚举性 查询导向 避免冗余 核心总结 数据模型是骨架,Metric类型是工具,Labels设计是灵魂 三者结合,才能构建出高效、可扩展的监控体系

这张图把本章的三个核心概念串联起来了。数据模型定义了“数据长什么样”,Metric类型决定了“数据怎么用”,Labels设计则影响了“数据好不好查”。三者缺一不可。

4.5 小结

好了,今天的内容就到这里。我最后再啰嗦一句:监控系统的设计,80%的工作在数据建模阶段。花时间把数据模型、Metric类型和Labels设计好,后面运维会轻松很多。

如果你在项目中遇到监控数据爆炸、查询缓慢的问题,不妨回头看看Labels设计是否合理。我曾经就因为一个高基数的标签,把整个Prometheus集群搞垮过。嗯,那种教训,一次就够了。


专注资料整理