指标是可观测性中成本最低、查询最快的信号。一个设计良好的指标体系可以让你在秒级回答”系统现在健康吗?”、“P99 延迟是多少?”、“错误率趋势如何?“——而无需搜索 TB 级日志或扫描百万条追踪。
但指标也是最容易出问题的信号:基数爆炸导致存储膨胀、Histogram 分桶不当导致 P99 失真、指标命名不一致导致查询困难。本章将深入 Prometheus 的四大度量类型,讨论 Histogram 的分桶原理、基数爆炸的治理策略,以及如何设计一个可持续演进的指标体系。
一、指标的本质:聚合的权衡
1.1 为什么需要指标?
指标的本质是预聚合——在存储之前将原始事件聚合为时间序列,用少量的存储空间保留足够的信息量。
| 维度 | 原始事件(日志/追踪) | 聚合指标 |
|---|---|---|
| 存储成本 | 高(每事件一条记录) | 低(预聚合时间序列) |
| 查询速度 | 慢(需要扫描) | 快(直接查询序列) |
| 信息保真度 | 高(完整事件) | 低(聚合后丢失细节) |
| 适用场景 | 排查具体问题 | 发现趋势异常 |
1.2 指标的结构
一个 Prometheus 指标由三部分组成:
http_request_duration_seconds{method="GET", status="200", endpoint="/api/v1/orders"} 0.150│ │ ││ │ └─ 数值│ └─ 标签(Labels / 维度)└─ 指标名称(Metric Name)指标名称描述”度量什么”,标签描述”度量的维度”。同一个指标名称 + 不同标签值 = 不同的时间序列。
二、四大度量类型
2.1 Counter:只增计数器
Counter 是最简单的度量类型——它只能递增(或重置为零)。适用于累计计数场景:
// Go: 使用 OTel SDK 定义 Counterimport ( "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel")
var requestCounter metric.Int64Counter
func init() { meter := otel.Meter("order-service") requestCounter, _ = meter.Int64Counter( "http.server.request.total", metric.WithDescription("Total number of HTTP requests"), )}
func handleRequest(w http.ResponseWriter, r *http.Request) { // 递增 Counter requestCounter.Add(r.Context(), 1, metric.WithAttributes( semconv.HTTPMethodKey.String(r.Method), semconv.HTTPStatusCodeKey.Int(200), semconv.HTTPRouteKey.String("/api/v1/orders"), ), )}Counter 的关键规则:
| 规则 | 说明 | 反模式 |
|---|---|---|
| 只能递增 | Counter 不能递减 | 用 Counter 记录当前并发数 |
| 用 rate() 查询 | Counter 的绝对值无意义,变化率才有意义 | 直接查询 Counter 值 |
| 重置处理 | 服务重启时 Counter 归零,rate() 自动处理 | 忽略重置导致尖峰 |
# 正确:查询请求速率rate(http_server_request_total{service="order-service"}[5m])
# 错误:查询 Counter 绝对值http_server_request_total{service="order-service"}2.2 Gauge:可增可减仪表
Gauge 可以任意增减,适用于”当前值”场景:
var activeConnections metric.Int64UpDownCounter
func handleConnection(conn net.Conn) { activeConnections.Add(context.Background(), 1) defer activeConnections.Add(context.Background(), -1)
// 处理连接...}| 场景 | 示例 | 说明 |
|---|---|---|
| 当前并发数 | http.server.active_connections | 每个连接 +1,断开 -1 |
| 内存使用量 | process.runtime.go.memory.heap_inuse | Go 运行时自动采集 |
| 温度/CPU | system.cpu.utilization | 系统指标 |
| 队列深度 | queue.depth | 消息队列当前深度 |
Gauge 适合”当前状态”,Counter 适合”累计事件”。一个常见的错误是用 Gauge 记录请求总数——这会导致重启后数据丢失,且无法使用 rate() 计算速率。
2.3 Histogram:延迟分析之王
Histogram 是可观测性中最重要的度量类型——它是 P50/P90/P99 延迟计算的基础。
Histogram 的工作原理:
Histogram 将观测值分配到预定义的桶(bucket)中,每个桶记录”≤该桶上界的观测次数”:
// 定义 Histogramvar requestDuration metric.Float64Histogram
func init() { meter := otel.Meter("order-service") requestDuration, _ = meter.Float64Histogram( "http.server.request.duration", metric.WithUnit("s"), metric.WithDescription("HTTP request duration in seconds"), // 自定义分桶(默认分桶通常不够用) metric.WithExplicitBucketBoundaries( 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, ), )}
func handleRequest(w http.ResponseWriter, r *http.Request) { start := time.Now() // 处理请求... duration := time.Since(start).Seconds()
requestDuration.Record(r.Context(), duration, metric.WithAttributes( semconv.HTTPMethodKey.String(r.Method), semconv.HTTPRouteKey.String("/api/v1/orders"), ), )}Histogram 在 Prometheus 中的存储:
# 一个 Histogram 产生 N+2 个时间序列(N = 桶数)http_server_request_duration_bucket{le="0.005"} 10 # ≤5ms: 10 个请求http_server_request_duration_bucket{le="0.01"} 25 # ≤10ms: 25 个请求http_server_request_duration_bucket{le="0.025"} 50 # ≤25ms: 50 个请求http_server_request_duration_bucket{le="0.05"} 80 # ≤50ms: 80 个请求http_server_request_duration_bucket{le="0.1"} 95 # ≤100ms: 95 个请求http_server_request_duration_bucket{le="0.25"} 99 # ≤250ms: 99 个请求http_server_request_duration_bucket{le="0.5"} 100 # ≤500ms: 100 个请求http_server_request_duration_bucket{le="+Inf"} 100 # 总请求数http_server_request_duration_sum 3.5 # 总耗时http_server_request_duration_count 100 # 总请求数P99 计算:
# 使用 histogram_quantile 计算 P99histogram_quantile(0.99, sum(rate(http_server_request_duration_bucket[5m])) by (le, service, endpoint))2.4 Histogram 分桶设计
分桶设计直接影响 P99 的准确性。Prometheus 默认分桶为:
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, +Inf这些默认分桶是为”通用 Web 服务”设计的。如果你的服务延迟特征不同,需要自定义分桶:
| 场景 | 推荐分桶 | 说明 |
|---|---|---|
| 通用 Web 服务 | 默认分桶 | 覆盖 5ms ~ 10s |
| 低延迟 API | 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5 | 侧重 1ms ~ 500ms |
| 批处理任务 | 1, 5, 10, 30, 60, 120, 300, 600 | 侧重秒级 ~ 分钟级 |
| 数据库查询 | 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5 | 侧重毫秒级 |
Histogram 的 P99 是估算值,不是精确值。误差取决于 P99 落在哪个桶中。如果 P99 恰好落在两个桶之间,估算值会是桶的上界——这意味着 P99 会被高估。例如,如果真实 P99 = 180ms,而桶上界是 250ms,则 histogram_quantile 返回 250ms。
2.5 Summary vs Histogram
| 维度 | Histogram | Summary |
|---|---|---|
| 计算位置 | 服务端(Prometheus) | 客户端(应用) |
| 聚合性 | 可跨实例聚合 | 不可跨实例聚合 |
| 分桶配置 | 可动态调整 | 固定 |
| 存储成本 | N+2 个序列/指标 | 3+N 个序列/指标 |
| 准确性 | 估算(受分桶影响) | 精确 |
| 推荐 | 推荐 | 仅特殊场景 |
结论:永远优先使用 Histogram。Summary 的不可聚合性是致命缺陷——当你有 10 个实例时,Summary 无法计算全局 P99。
三、基数爆炸:指标的阿喀琉斯之踵
3.1 什么是基数爆炸?
基数(Cardinality)是指一个指标的所有标签组合产生的唯一时间序列数量。当基数过高时,存储和查询性能急剧下降——这就是基数爆炸。
3.2 基数爆炸的数学
总序列数 = 指标数 × ∏(每个标签的唯一值数)| 标签组合 | 唯一值数 | 总序列数 | 评估 |
|---|---|---|---|
| service × endpoint × status | 10 × 50 × 5 | 2,500 | 可控 |
| service × user_id | 10 × 1,000,000 | 10,000,000 | 爆炸 |
| service × endpoint × status × pod | 10 × 50 × 5 × 100 | 250,000 | 偏高 |
| service × request_id | 10 × ∞ | ∞ | 致命 |
3.3 常见高基数标签
| 标签 | 唯一值数 | 问题 | 解决方案 |
|---|---|---|---|
user_id | 百万级 | 每个用户一条序列 | 移到日志/追踪,指标中不包含 |
request_id | 无限 | 每个请求一条序列 | 绝对不能出现在指标中 |
ip_address | 百万级 | 每个客户端一条序列 | 聚合为地区/ASN |
session_id | 百万级 | 每个会话一条序列 | 移到日志 |
pod_name | 百级 | 每次部署产生新序列 | 用 deployment 替代 |
error_message | 千级 | 每个错误消息一条序列 | 用 error_code 替代 |
3.4 基数治理策略
策略 1:标签白名单
# OTel Collector: 只允许低基数标签通过processors: filter: error_mode: ignore metrics: metric: - 'name == "http.server.request.duration" and attributes["user_id"] != nil' # 移除高基数标签 transform: error_mode: ignore metric_statements: - context: datapoint statements: - delete_key(attributes, "user_id") - delete_key(attributes, "request_id") - delete_key(attributes, "ip_address")策略 2:高基数标签降维
// 将 IP 地址降维为地区func ipToRegion(ip string) string { // 使用 GeoIP 数据库将 IP 映射为地区 region := geoip.Lookup(ip).Region return region // "us-east-1", "eu-west-1" 等}
// 使用降维后的标签requestDuration.Record(ctx, duration, metric.WithAttributes( semconv.HTTPMethodKey.String(method), attribute.String("client_region", ipToRegion(clientIP)), ),)策略 3:高基数指标分离
每个时间序列大约占用 3-5 KB 内存(Prometheus)。100 万序列 = 3-5 GB 内存。如果你的 Prometheus 内存使用率持续上升,很可能是基数爆炸。使用 prometheus_cardinality_exporter 监控基数。
四、指标命名约定
4.1 Prometheus 命名规范
<namespace>_<subsystem>_<name>_<unit>| 规则 | 正确 | 错误 |
|---|---|---|
| 小写下划线 | http_server_request_duration_seconds | httpServerRequestDuration |
| 单数形式 | http_server_request_duration | http_server_request_durations |
| 带单位后缀 | http_server_request_duration_seconds | http_server_request_duration |
| 不带类型后缀 | http_server_request_duration | http_server_request_duration_histogram |
4.2 OTel 语义约定
OpenTelemetry 定义了标准化的指标名称和属性:
| OTel 指标 | 说明 | 对应 Prometheus |
|---|---|---|
http.server.request.duration | HTTP 请求耗时 | http_server_request_duration_seconds |
http.server.request.total | HTTP 请求总数 | http_server_request_total |
http.server.active_requests | 当前活跃请求 | http_server_active_requests |
db.client.operation.duration | 数据库操作耗时 | db_client_operation_duration_seconds |
process.runtime.go.memory.heap.inuse | Go 堆内存使用 | go_memory_heap_inuse_bytes |
五、PromQL 查询实战
5.1 常用查询模式
# 1. 请求速率(QPS)sum(rate(http_server_request_total[5m])) by (service)
# 2. 错误率sum(rate(http_server_request_total{status=~"5.."}[5m])) by (service)/sum(rate(http_server_request_total[5m])) by (service)
# 3. P50 / P90 / P99 延迟histogram_quantile(0.50, sum(rate(http_server_request_duration_bucket[5m])) by (le, service))histogram_quantile(0.90, sum(rate(http_server_request_duration_bucket[5m])) by (le, service))histogram_quantile(0.99, sum(rate(http_server_request_duration_bucket[5m])) by (le, service))
# 4. 延迟 SLO 达成率(P99 < 500ms)sum(rate(http_server_request_duration_bucket{le="0.5"}[5m])) by (service)/sum(rate(http_server_request_duration_count[5m])) by (service)
# 5. RED 方法(Rate-Errors-Duration)# Ratesum(rate(http_server_request_total[5m])) by (service, endpoint)# Errorssum(rate(http_server_request_total{status=~"5.."}[5m])) by (service, endpoint)# Durationhistogram_quantile(0.99, sum(rate(http_server_request_duration_bucket[5m])) by (le, service, endpoint))
# 6. USE 方法(Utilization-Saturation-Errors)— 适用于资源指标# Utilization: CPU 使用率1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]))# Saturation: 运行队列长度avg(node_load5)# Errors: 设备错误rate(node_disk_io_time_seconds_total[5m])5.2 RED vs USE 方法
| 方法 | 适用对象 | 三个指标 | 说明 |
|---|---|---|---|
| RED | 请求/服务 | Rate、Errors、Duration | 微服务排障首选 |
| USE | 资源/硬件 | Utilization、Saturation、Errors | 基础设施排障首选 |
六、指标体系设计
6.1 分层指标体系
| 层级 | 指标类型 | 消费者 | 更新频率 |
|---|---|---|---|
| 业务指标 | 业务 KPI | 产品经理、业务方 | 分钟级 |
| 服务指标 | RED | 开发者、SRE | 秒级 |
| 基础指标 | USE | 运维 | 秒级 |
6.2 指标与 SLO 的关系
指标是 SLI(Service Level Indicator)的实现,SLI 是 SLO 的度量:
SLO: 99.9% 的请求在 500ms 内完成 └─ SLI: 请求延迟 < 500ms 的比例 └─ 指标: histogram_quantile(0.999, rate(http_server_request_duration_bucket[5m]))在第 11 章:SLO 与告警中深入讨论 SLO 方法论。
七、动手实践:搭建指标体系
7.1 部署 Prometheus
# 使用 Docker Compose 启动 Prometheusdocker compose up -d prometheus
# 验证 Prometheus 运行curl http://localhost:9090/api/v1/query?query=up7.2 Prometheus 配置
global: scrape_interval: 15s evaluation_interval: 15s
scrape_configs: - job_name: 'otel-collector' static_configs: - targets: ['otel-collector:8889'] metric_relabel_configs: # 移除高基数标签 - source_labels: [__name__] regex: '.*_bucket' action: keep - source_labels: [user_id] regex: '.+' action: drop # 丢弃包含 user_id 的样本
rule_files: - 'recording_rules.yaml' - 'alerting_rules.yaml'7.3 Recording Rules
# recording_rules.yaml — 预计算常用查询groups: - name: service:red interval: 30s rules: # 请求速率 - record: service:http_requests:rate5m expr: sum(rate(http_server_request_total[5m])) by (service, endpoint)
# 错误率 - record: service:http_errors:rate5m expr: sum(rate(http_server_request_total{status=~"5.."}[5m])) by (service, endpoint)
# P99 延迟 - record: service:http_request_duration:p99 expr: histogram_quantile(0.99, sum(rate(http_server_request_duration_bucket[5m])) by (le, service, endpoint))7.4 验证查询
# 在 Prometheus UI 中验证open http://localhost:9090
# 查询示例# 1. 所有服务的请求速率curl 'http://localhost:9090/api/v1/query?query=sum(rate(http_server_request_total[5m]))%20by%20(service)'
# 2. P99 延迟curl 'http://localhost:9090/api/v1/query?query=histogram_quantile(0.99,sum(rate(http_server_request_duration_bucket[5m]))%20by%20(le,service))'
# 3. 基数检查curl 'http://localhost:9090/api/v1/query?query=count(http_server_request_duration_bucket)'7.5 验证清单
| 检查项 | 验证方式 | 预期结果 |
|---|---|---|
| Prometheus 采集数据 | up 查询 | targets 全部为 1 |
| Histogram 数据正确 | histogram_quantile 查询 | P99 值合理 |
| Recording Rules 生效 | 查询 recording rule 名称 | 返回预计算结果 |
| 基数可控 | count(metric) 查询 | 序列数 < 预期阈值 |
八、本章小结
上一章剖析了结构化日志与关联 ID。
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| 四大度量类型 | Counter(只增计数)、Gauge(可增可减)、Histogram(延迟分析之王)、Summary(避免使用)。 | 四大度量类型 |
| Histogram 分桶 | 分桶设计直接影响 P99 准确性,需要根据服务延迟特征自定义分桶。 | Histogram 分桶 |
| 基数爆炸 | 高基数标签(user_id、request_id、IP)是指标的阿喀琉斯之踵,必须治理。 | 基数爆炸 |
| 命名约定 | 遵循 OTel 语义约定,确保跨服务一致性。 | 命名约定 |
| RED/USE 方法 | 服务用 RED(Rate-Errors-Duration),资源用 USE(Utilization-Saturation-Errors)。 | RED/USE 方法 |
| Recording Rules | 预计算常用查询,降低 Dashboard 和告警的查询延迟。 | Recording Rules |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






