作为一名在云原生领域摸爬滚打的开发者,我们深知监控系统的健康状态对于保障服务稳定性至关重要。在众多监控工具中,Prometheus 凭借其强大的数据模型和灵活的查询语言,成为了 Kubernetes 生态中事实上的标准。你是否曾经在仪表盘前盯着不断跳动的数字,却不确定该如何选择最合适的指标类型?或者,你是否因为使用了错误的指标类型而导致告警失灵?
在这篇文章中,我们将摒弃枯燥的理论定义,像老朋友聊天一样,深入探讨 Prometheus 的四大核心指标类型:Counter(计数器)、Gauge(仪表盘)、Histogram(直方图)和 Summary(摘要)。我们将不仅解释它们是什么,更重要的是,通过实际的代码示例和避坑指南,帮助你掌握在何种场景下使用何种指标,从而构建出更加健壮的监控体系。
目录
Prometheus 指标机制概览
在深入细节之前,让我们先统一一下对 Prometheus 工作原理的认知。Prometheus 采用的是一种“拉取”模型。这意味着我们的应用程序并不需要主动把数据推送给监控系统,而是暴露一个 HTTP 端点。Prometheus 服务器会定期到这里来“抓取”数据。
这种机制的好处显而易见:即使网络出现抖动,只要 Prometheus 能恢复连接,它就能获取到最新的状态。而在代码层面,我们通常使用官方提供的客户端库(如 Java, Go, Python 客户端库)来暴露这些指标。为了让 Prometheus 能正确解析这些数据,我们通常遵循 OpenMetrics 格式。
场景先行:我们需要关注什么?
在谈论具体类型之前,让我们先看看在实际运维中,我们最关心的几个关键场景,这将帮助我们理解为何需要不同的指标类型:
- 内存使用率:这是一个典型的“状态”指标。我们想知道当前用了多少内存,它是可以升也可以降的。这决定了我们需要使用 Gauge 类型。
- CPU 消耗时间:CPU 是一直在运行的,其消耗的时间是单向递增的。这决定了我们需要使用 Counter 类型。
- 请求响应延迟:我们要知道大部分请求的响应时间是多少,P99 延迟是多少。这涉及到数据分布,通常需要 Histogram 或 Summary。
1. Gauge(仪表盘):捕捉瞬时的状态
Gauge 是最直观的指标类型。想象一下你汽车里的油量表或速度表,它们反映的是当前的瞬时值。
什么是 Gauge?
Gauge 是一个可以任意上升或下降的数值。它用于衡量那些“状态类”的数据,比如当前的内存使用量、队列中的任务数量、当前的温度等。
为什么不能使用 rate()?
这是一个新手常犯的错误:对 Gauge 类型的指标使用 INLINECODE60cadec8 函数。INLINECODEe2d3953a 是专门为那些只增不减的 Counter 设计的,用来计算增长速度。如果你对 Gauge(比如内存)使用 INLINECODEe59e70ba,计算出来的“内存增长速率”通常没有实际意义(内存回收时甚至会出现负值)。对于 Gauge,我们通常直接查看其绝对值,或者使用 INLINECODE96542996 函数来看一段时间内的变化量。
代码示例:监控活跃连接数
让我们看一个 Python 的例子,模拟监控当前服务器的活跃连接数。
from prometheus_client import start_http_server, Gauge
import random
import time
# 定义一个 Gauge 类型的指标
# 第一个参数是指标名称,第二个是描述,第三个是标签(用于区分不同的实例)
active_connections = Gauge(‘app_active_connections‘, ‘Current number of active connections‘, [‘endpoint‘])
def simulate_traffic():
# 模拟不同端点的连接数波动
while True:
# 这里我们模拟数据:连接数在 10 到 100 之间随机波动
conn_count_api = random.randint(10, 100)
conn_count_db = random.randint(5, 50)
# 使用 .labels() 指定标签,然后 .set() 设置当前值
active_connections.labels(endpoint=‘/api‘).set(conn_count_api)
active_connections.labels(endpoint=‘/db‘).set(conn_count_db)
print(f"Updated metrics: API={conn_count_api}, DB={conn_count_db}")
time.sleep(2)
if __name__ == ‘__main__‘:
# 启动一个 HTTP 服务,让 Prometheus 可以抓取数据
start_http_server(8000)
simulate_traffic()
代码解析:
- 定义:我们使用 INLINECODEe74741a3 创建了一个名为 INLINECODEe9f67b22 的指标。
- 标签:
[‘endpoint‘]允许我们区分不同的端点(如 API 和数据库连接)。在 Prometheus 中,这会生成多个时间序列。 - 设置值:
.set()方法是 Gauge 的核心,它直接更新当前值。无论之前的值是多少,现在就是它。
最佳实践
当你在仪表盘上展示 Gauge 时,直接展示其原始数值。在告警规则中,通常使用类似 memory_usage_bytes > 1024 * 1024 * 1024 这样的表达式,表示“如果当前内存超过 1GB 则告警”,而不是关注它的变化率。
2. Histogram(直方图):分而治之的分布统计
如果说 Gauge 是为了“现状”,那么 Histogram 就是为了“历史分布”。它是监控中最强大但也最容易被误解的类型。
什么是 Histogram?
Histogram(直方图)并不会存储每个具体的数值,而是将数值按照预设的“桶”进行分桶计数。比如,请求延迟 0.1秒 的有多少个,0.5秒 的有多少个。
默认情况下,Prometheus 的客户端库会提供一组默认桶(例如 Go 客户端默认为 .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10 秒)。这意味着我们可以非常灵活地计算出 P95、P99 等分位数值。
为什么 Histogram 比 Summary 更好用?
这是一个关键点。Histogram 只是记录“有多少个请求落在这个区间内”,这些数据可以在查询时由 histogram_quantile() 函数聚合计算。这意味着我们可以查看“整个集群过去 5 分钟的 P95 延迟”。而 Summary 通常在客户端计算好分位数,服务端只能做简单的聚合,无法准确计算“多集群的加权平均 P99”。
代码示例:测量 API 请求延迟
让我们用 Go 语言来演示如何记录一个 HTTP 请求的持续时间。
package main
import (
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
// 定义一个 Histogram 指标
// http_request_duration_seconds 是指标名
// []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} 是我们自定义的桶
httpDurations = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request latencies in seconds",
Buckets: prometheus.LinearBuckets(0.1, 0.5, 10), // 从 0.1 开始,步长 0.5,共 10 个桶
},
[]string{"method", "path"}, // 标签
)
)
func init() {
// 注册指标
prometheus.MustRegister(httpDurations)
}
func main() {
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 模拟业务处理
time.Sleep(time.Duration(100) * time.Millisecond)
// 记录持续时间
// Observe 方法会自动将当前时间与 start 时间差记录到 Histogram 中
duration := time.Since(start).Seconds()
httpDurations.WithLabelValues("GET", "/api").Observe(duration)
w.Write([]byte("Hello World"))
})
http.ListenAndServe(:8080", nil)
}
代码解析:
- NewHistogramVec:我们创建了一个支持标签的 Histogram。
- Buckets:这里我们演示了自定义桶。
prometheus.LinearBuckets(0.1, 0.5, 10)意味着我们关注 0.1 秒到 5.6 秒之间的延迟分布。如果你的服务响应非常快(比如微秒级),你需要调整这个区间,否则所有请求都会落入第一个桶,导致数据无效。 - Observe:这是 Histogram 的核心方法。它接收一个浮点数,并自动增加对应桶的计数器,同时更新 INLINECODE353d8005(总和)和 INLINECODE348cf276(次数)。
性能优化建议
Histogram 会生成大量的时间序列。假设你有 INLINECODE7f02157a 个标签值的组合,以及 INLINECODE88edc124 个桶,你就会产生 INLINECODE1b8b6ab3 个时间序列(加2是因为还有 sum 和 _count)。如果你的标签基数很大(比如用户ID),千万不要把它作为 Histogram 的标签,否则可能会打爆你的 Prometheus 内存。
3. Summary(摘要):客户端预聚合的分位数
什么是 Summary?
Summary 的作用也是为了监控“延迟”类的数据分布。它和 Histogram 最大的区别在于:Summary 在客户端(应用程序)侧计算分位数。也就是说,你的应用程序代码在运行时会计算出 P50、P90、P95 等值,然后直接暴露给 Prometheus。
什么时候使用 Summary?
虽然 Histogram 通常是首选,但在某些特殊情况下,Summary 更有用。比如,当你的数据分布极其不规则,或者你需要在客户端直接控制分位数的计算精度,或者你不想在 Prometheus 服务器上进行复杂的查询计算时。
代码示例:模拟任务处理队列长度
以下示例展示了如何在 Python 中定义一个 Summary 来跟踪任务处理的耗时。
from prometheus_client import start_http_server, Summary
import random
import time
# 定义一个 Summary
# 这里的 quantiles 参数定义了我们要计算哪些分位数:0.5(中位数), 0.9, 0.99
request_time = Summary(‘request_processing_seconds‘, ‘Time spent processing request‘,
quantiles=[0.5, 0.9, 0.99])
def process_request(t):
"""一个模拟的请求处理函数"""
time.sleep(t)
if __name__ == ‘__main__‘:
start_http_server(8000)
# 模拟流量
for _ in range(1000):
start_time = time.time()
# 随机处理时间
process_request(random.random())
# 使用 .observe() 记录时间
request_time.observe(time.time() - start_time)
重要提示:
Summary 暴露的 request_processing_seconds{quantile="0.99"} 是一个具体的值。这意味着你不能对它求平均值。如果你有两个相同的实例,一个延迟 10ms,一个 100ms,它们的 P99 可能分别是 10ms 和 100ms。当 Prometheus 抓取这两个指标时,你无法得到全局的 P99,只能看到两个独立的 P99。
4. Counters(计数器):永不回头的事件记录
Counter 是最简单的指标类型:它只能增加,不能减少(除非服务重启)。它是用来计数“事件”发生的次数。
使用场景
- HTTP 请求总次数:
http_requests_total - CPU 消耗时间:
node_cpu_seconds_total(这看起来像时间,但其实是个计数器,记录 CPU 累计跑了多少秒) - 销售订单数量
代码示例:API 请求计数器
让我们看看如何在 Java (Spring Boot) 中使用 Counter 记录 API 调用次数。
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ApiController {
private final Counter apiCallCounter;
// 注入 MeterRegistry 并构建 Counter
public ApiController(MeterRegistry registry) {
this.apiCallCounter = Counter.builder("api.calls.total")
.description("Total number of API calls")
.tag("service", "payment") // 打上标签
.register(registry);
}
@GetMapping("/pay")
public String pay() {
// 每次请求时,增加计数器
apiCallCounter.increment();
return "Payment processed";
}
}
深入理解:rate() 函数
既然 Counter 是一直累加的,我们通常不关心“累计总数”(那个数字可能大得没意义),我们关心的是“每秒增加了多少”。这就是 rate() 函数的用武之地。
在 Prometheus 查询语言 (PromQL) 中:
rate(api_calls_total[5m])
这个表达式会计算过去 5 分钟内,该 Counter 每秒平均增长了多少。这直接反映了当前的 QPS(每秒查询率)。
避坑指南:Counter 的重置问题
有时候,服务重启会导致 Counter 归零。如果你直接计算差值,可能会看到一个巨大的尖刺。Prometheus 的 INLINECODEc081cc95 或 INLINECODE594b9183 函数会自动处理这种“计数器重置”的情况,所以请始终使用这两个函数来查询 Counter 的变化率,而不是手动去算差值。
总结与最佳实践
我们已经深入探讨了 Prometheus 的四种核心指标。让我们来总结一下当你面对一个监控需求时,该如何做出选择:
- 数值是会上下波动的吗?(如内存、温度、队列长度)
* 选择:Gauge。
操作*:直接查询当前值。如果看变化,用 delta()。
- 数值是只增不减的“事件”吗?(如请求数、错误数、销售量)
* 选择:Counter。
操作*:使用 INLINECODEcb7c0939 或 INLINECODEd4888514 来查看变化速率(QPS, EPS)。
- 你需要监控数值的分布(延迟、响应大小)吗?
* 首选:Histogram。它开销小,聚合灵活(可以在多个服务实例间聚合)。
操作*:使用 INLINECODEc6e3624f 计算分位数,或者直接查询 INLINECODEe14b5717。
* 备选:Summary。仅在分布极不规则或需要在客户端精确控制分位数时使用。
性能与成本优化清单
为了保持你的监控系统高效运行,这里有一些实战建议:
- 控制标签基数:这一点至关重要。永远不要把用户 ID、Session ID 或 Trace ID 作为标签的值。这会导致“高基数”问题,瞬间耗尽 Prometheus 的内存。只使用那些拥有有限枚举值的标签,比如 INLINECODE7205c43e (GET/POST) 或 INLINECODEd2494e00 (200/500)。
- Histogram 桶的优化:如果你发现 90% 的请求都落在了第一个桶里,或者所有的请求都散落在不同的桶里没有任何集中,说明你的桶配置得不好。请根据实际的业务数据范围调整 Bucket 的区间。
通过掌握这些指标类型的特性与用法,你不仅能更好地观察系统的运行状态,更能从数据中发现潜在的性能瓶颈。现在,不妨试着在你的下一个项目中应用这些知识,为你的关键业务指标配置上最合适的 Prometheus 类型吧。
希望这篇文章能帮助你更好地理解 Prometheus 的指标世界。如果你在实践过程中遇到任何问题,欢迎随时交流探讨。