土法炼钢兴趣小组的算法知识备份

【可观测性工程】指标体系设计:USE、RED、Golden Signals 与业务 KPI

文章导航

分类入口
architectureobservability
标签入口
#metrics#use-method#red-method#golden-signals#prometheus#cardinality#histogram#summary#slo#kpi

目录

指标体系设计:USE、RED、Golden Signals 与业务 KPI

指标(Metrics)是可观测性的三大支柱之一。但”要监控什么指标”这个问题,从来没有一个团队能仅凭直觉给出正确答案。

2012 年 Brendan Gregg 提出 USE、2015 年 Tom Wilkie 提出 RED、2016 年 Google SRE Book 总结 Four Golden Signals——这三套方法论并不是互相取代的关系,而是各自针对不同对象:USE 看资源、RED 看请求、Golden Signals 看服务健康、业务 KPI 看价值

本文尝试把这四个层次串起来,并给出一份可以直接抄到生产环境的指标清单。

USE / RED / Golden Signals / KPI 矩阵

一、为什么需要方法论,而不是”想到什么监控什么”

1.1 没有方法论的指标体系,长什么样

绝大多数团队在起步阶段的监控,都是按”出过什么事故就加什么指标”的方式建设的。这种”事故驱动”的指标体系会在半年内演化成三种典型病态:

第一种:监控盲区。 你监控了 CPU 使用率、内存使用率、HTTP 5xx,但没有监控磁盘 I/O 饱和度、没有监控 TCP 重传率、没有监控文件描述符耗尽。直到某天凌晨三点 MySQL 因为 Too many open files 挂掉,你才发现从来没有 fd 相关告警。

第二种:告警疲劳(Alert Fatigue)。 因为不知道哪些指标重要,运维同学把所有指标都设了告警阈值,一个凌晨收到 200 条告警,值班同学直接静音所有通道,真正的故障告警被淹没。Google SRE Book 第六章给出了一个经验数字:如果一个 on-call 工程师一天收到超过两条需要人工介入的告警,这个告警系统就已经失败了。

第三种:关键指标缺失。 你有 CPU、内存、网络流量、HTTP QPS,但当老板问”为什么昨晚 GMV 掉了 30%“的时候,你只能回答”技术指标一切正常”。因为你从来没有把”支付成功率”这个业务指标接入监控。

这三类问题的共同根因是:没有一个覆盖框架(coverage framework)告诉你”监控应该覆盖哪些维度”

1.2 方法论的作用:系统性覆盖

方法论的本质是”完备性保证”。当你用 USE 方法论去审视一台服务器的时候,你会被迫为每一个资源(CPU、内存、磁盘、网络、进程、文件描述符、中断……)填满三列:Utilization、Saturation、Errors。如果某一格填不出来,那就是一个盲区,要么是数据没采集,要么是这个维度不存在。

这就像软件测试领域的”等价类划分 + 边界值分析”——方法论不能保证你一定找到 bug,但能保证你不会因为遗漏某一类维度而错过 bug

1.3 三套方法论的历史脉络

年份 方法论 提出者 背景
2012 USE Brendan Gregg(当时在 Joyent) 为 Solaris/Linux 系统性能分析总结
2015 RED Tom Wilkie(Weaveworks) 在 GrafanaCon 2015 演讲中提出,面向微服务
2016 Four Golden Signals Google SRE Team Site Reliability Engineering 一书正式化

时间顺序并非巧合:2012 年的业务形态仍以单机/小集群为主,关注的是资源瓶颈;2015 年容器化和微服务兴起,关注的是请求链路;2016 年 Google 把分布式系统运维的最佳实践系统化,关注的是整个服务健康。

1.4 从阿里双十一看方法论的演进

阿里巴巴在公开的技术分享中描述过双十一压测监控体系的演进(参见 InfoQ、阿里技术公众号等公开资料):早期(2009—2011)主要关注机器资源,仪表盘上全是 CPU、内存曲线;2012 年后引入类似 RED 的接口指标,开始关注服务层面的成功率和 RT;2015 年之后,“业务全景大盘”成为主角,订单创建率、支付成功率、库存扣减成功率被作为一等公民监控。这条演进路径几乎和 USE → RED → Golden Signals → KPI 的层次完全吻合——不是巧合,而是分布式系统复杂度上升的必然结果。


二、USE 方法论

2.1 定义与来源

USE 方法论由 Brendan Gregg 在 2012 年 6 月的博文 The USE Method 中正式提出,后续在 Systems Performance: Enterprise and the Cloud(第一版 2013,第二版 2020)一书中系统展开。其核心主张:

For every resource, check utilization, saturation, and errors.

三字母展开:

三者的关系:Utilization 告诉你”资源有多忙”,Saturation 告诉你”资源忙到什么程度开始积压工作”,Errors 告诉你”资源是否出现了物理或协议层面的异常”。

2.2 一个容易混淆的概念:Utilization 100% 不等于 Saturation > 0

这是 USE 方法论最容易被误解的点。考虑一个单核 CPU:当它持续 100% 运行某个单线程程序时,Utilization = 100%,但如果此时 run queue 长度为 1(只有当前一个任务),Saturation = 0。此时 CPU 并未”过载”,只是恰好跑满。

只有当 run queue 长度大于 CPU 核心数,即多个任务在排队等 CPU,才算 Saturation > 0。

这个区分很重要,因为单独看 Utilization 会做出错误的扩容决策。Brendan Gregg 原文中举过一个经典例子:一个批处理任务把 CPU 打到 100%,但系统没有任何延迟感知问题——因为没有其他任务在等;此时盲目扩容毫无意义。

2.3 USE 的适用对象

USE 方法论只适用于资源。Brendan Gregg 在原文中给出的 checklist 包括:

# 资源类型 Utilization Saturation Errors
1 CPU %busy run queue length / scheduler latency machine check exceptions
2 Memory(容量) used / total 页面换出速率、OOM 事件 ECC 错误计数
3 Memory(带宽) 内存总线占用率 停顿周期
4 磁盘 I/O %util(iostat) avgqu-sz、await 磁盘错误(SMART)
5 磁盘容量 used / total 文件系统损坏事件
6 网络接口 带宽占用率 TX/RX queue drops CRC / frame errors
7 文件描述符 used / limit EMFILE 错误频次
8 Kernel: tasks 进程数 / pid_max fork 失败率
9 Kernel: inode used / total
10 GPU compute utilization memory pressure ECC errors

注意这张表的覆盖粒度——每个资源都要填满三列,填不出来就要问自己:是这一维度不存在(比如磁盘容量的 Saturation 没有明确定义),还是只是没采集到?

2.4 USE 的 Prometheus 实现

基于 node_exporter(Prometheus 官方的 Linux 主机 exporter),USE 矩阵的绝大部分格子都能被填满。

CPU Utilization:

# 整机 CPU 使用率(1 - idle 比例)
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance)

CPU Saturation(run queue 与核数比值):

# 负载归一化:load1 / logical_cpu_count
avg(node_load1) by (instance)
  /
count(count(node_cpu_seconds_total{mode="idle"}) by (instance, cpu)) by (instance)

这个查询比直接用 node_load1 更有意义:在 8 核机器上 load1 = 4 属于健康,在 2 核机器上 load1 = 4 就是饱和。除以核数之后可以统一阈值(通常 > 1 视为饱和)。

Memory Utilization:

# 已使用内存占比(排除 buffer / cache 可回收部分)
1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes

注意:不要用 MemFree。Linux 倾向于把空闲内存用作 page cache,MemFree 常年接近 0 毫无意义。MemAvailable(内核 3.14+ 提供)才是真正”可用”的内存。

Memory Saturation(swap 活动):

# 每秒换出页数;持续 > 0 即饱和
rate(node_vmstat_pswpout[5m])

在禁用 swap 的现代容器环境中,Memory Saturation 表现为 OOM Kill:

# OOM Kill 事件速率
rate(node_vmstat_oom_kill[5m])

Disk I/O Utilization:

# 磁盘繁忙百分比
rate(node_disk_io_time_seconds_total[5m])

Disk I/O Saturation:

# 平均队列深度
rate(node_disk_io_time_weighted_seconds_total[5m])

io_time_weighted 是队列深度对时间的积分,除以采样时间窗口就得到平均队列深度。持续 > 1 表示磁盘有积压。

Network Errors:

# 接收错误速率
rate(node_network_receive_errs_total[5m])
# 丢包速率
rate(node_network_receive_drop_total[5m])

File Descriptor Utilization:

# 整机 fd 使用率
node_filefd_allocated / node_filefd_maximum

进程级 fd 需要单独的 exporter(如 process-exporter)。

2.5 USE 的局限

USE 方法论的威力在于”资源视角”,但它也仅限于资源视角。以下问题 USE 回答不了:

这正是需要 RED 与 Golden Signals 的原因。


三、RED 方法论

3.1 定义与来源

RED 方法论由 Tom Wilkie 在 GrafanaCon 2015 演讲 Monitoring Microservices, the RED Method 中提出。彼时他在 Weaveworks 工作,主持 Cortex 项目(后来演化为 Grafana Mimir)。RED 三字母:

Tom Wilkie 在演讲中明确指出:RED 的提出动机是让微服务监控”足够简单以至于每个服务都能做到一致”——一家公司可能有几百个微服务,不可能每个服务都用不同的监控方案。RED 是一种跨服务的标准化接口

3.2 RED 的对象:请求

RED 只关心”请求/响应”这一范式。它的标准适用对象:

如果一个组件没有清晰的”请求”概念——比如一个常驻后台 worker 按流式处理数据——那么 RED 就不直接适用,可能需要用”处理的消息数 + 处理延迟 + 错误数”的类 RED 形式。

3.3 RED 的 Prometheus 实现

标准的 HTTP RED 依赖三个指标:

http_requests_total              Counter   tags: service, handler, method, status
http_request_duration_seconds    Histogram tags: service, handler, method

Rate:

sum(rate(http_requests_total[5m])) by (service, handler)

Error Rate(失败率):

# 5xx 占比
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
  /
sum(rate(http_requests_total[5m])) by (service)

注意分母不能加 status 标签过滤,否则就不是”失败率”而是”5xx 内部分布”了。另外在告警中通常要给分母设置”至少 N QPS”的门槛,否则低流量服务一条错误就会触发 100% 错误率:

# 仅在 QPS > 1 时评估错误率
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
  /
sum(rate(http_requests_total[5m])) by (service)
> 0.01
and
sum(rate(http_requests_total[5m])) by (service) > 1

Duration(p99 延迟):

histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (service, handler, le)
)

这里有一个微妙之处:sum by (..., le) 是必须的——必须保留 le 标签,因为 histogram_quantile 就是靠 le 推算分位数。忘记保留 le 是 PromQL 初学者最常见的错误之一。

3.4 RED 与 SLO 的直接映射

Google SRE 把可用性 SLO 拆成两个维度:

对照 RED:

典型的 SLO 定义(伪代码):

slo:
  name: checkout-service-availability
  objective: 99.9%
  window: 30d
  sli:
    numerator: sum(rate(http_requests_total{service="checkout",status!~"5.."}[1m]))
    denominator: sum(rate(http_requests_total{service="checkout"}[1m]))

slo:
  name: checkout-service-latency
  objective: 99%  # 99% 请求 <= 500ms
  window: 30d
  sli: |
    sum(rate(http_request_duration_seconds_bucket{service="checkout",le="0.5"}[1m]))
      /
    sum(rate(http_request_duration_seconds_count{service="checkout"}[1m]))

可以看到:RED 指标是 SLI(Service Level Indicator)的直接原材料。这也是为什么每个微服务都应该有 RED 的根本原因——没有 RED 就没有 SLO。

3.5 RED 的不足

RED 看得见请求,看不见资源。考虑以下场景:

所以 RED 必须和 USE 搭配,并且都上层再叠加业务 KPI。


四、Google Four Golden Signals

4.1 定义与来源

Google SRE Book 第六章 Monitoring Distributed Systems 给出了 Four Golden Signals:

4.2 Golden Signals 和 RED 的关系

对比会发现:

RED Golden Signals 差异
Rate Traffic 几乎等价,Traffic 更强调业务语义
Errors Errors Golden Signals 额外强调”隐式错误”
Duration Latency Golden Signals 要求拆分成功/失败
Saturation RED 没有,Golden Signals 显式要求

Golden Signals = RED + Saturation + 区分成功/失败的延迟。它是 RED 的超集,但视角更偏 SRE——不只要看请求成功率,还要看”未来会不会出事”。

4.3 Saturation 的 Prometheus 实现

Saturation 并没有统一的 PromQL 表达式,因为每种资源的”多满”定义不同。实践中常见做法:

CPU:

# load per core,建议阈值:> 1
avg(node_load1) by (instance)
  /
count(count(node_cpu_seconds_total{mode="idle"}) by (instance, cpu)) by (instance)

应用层连接池:

# 连接池使用率,建议阈值:> 0.8
sum(db_pool_in_use_connections) by (service)
  /
sum(db_pool_max_connections) by (service)

消息队列积压(典型的 Saturation):

kafka_consumergroup_lag{group="order-consumer"}

磁盘容量的预测性告警(Golden Signals 强调的”N 天后会满”):

# predict_linear 根据过去 6 小时趋势预测 4 天后的值
predict_linear(node_filesystem_avail_bytes[6h], 4 * 24 * 3600) < 0

这是 Prometheus predict_linear 函数的经典用法——给线性增长的指标做简单外推。对于容量类指标(磁盘、证书到期天数、内存缓慢泄漏)非常有效。

4.4 区分成功 / 失败的延迟

Google SRE Book 强调”快速失败的请求会拉低延迟指标”。标准做法是分开两个 Histogram:

# 成功请求 p99
histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket{status!~"5.."}[5m])) by (le))

# 失败请求 p99
histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket{status=~"5.."}[5m])) by (le))

或者直接在埋点阶段就把 status 作为 label——但要注意基数(见第九节)。


五、三者适用对象对比

5.1 分层架构示意

从下往上:

┌─────────────────────────────────────────┐
│  Business KPI    业务价值指标             │  ← 业务埋点
│    订单成功率、GMV、DAU、转化率            │
├─────────────────────────────────────────┤
│  Service Layer   服务层                  │  ← RED / Golden Signals
│    HTTP 请求、gRPC 调用、DB 查询          │
├─────────────────────────────────────────┤
│  Infrastructure  基础设施层               │  ← USE
│    CPU、内存、磁盘、网络、FD、内核        │
└─────────────────────────────────────────┘

5.2 覆盖矩阵

组件 方法论 关键指标 Prometheus 实现示例
Linux 主机 CPU USE 使用率 1 - rate(node_cpu_seconds_total{mode="idle"}[5m])
Linux 主机 CPU USE 饱和度 node_load1 / cpu_count
Linux 主机 内存 USE 使用率 1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
Linux 主机 磁盘 USE I/O util rate(node_disk_io_time_seconds_total[5m])
Linux 主机 网络 USE 错误 rate(node_network_receive_errs_total[5m])
Kubernetes Node USE Pod 密度 kube_node_status_allocatable{resource="pods"} - sum(kube_pod_info) by (node)
HTTP 服务 RED Rate sum(rate(http_requests_total[5m])) by (service)
HTTP 服务 RED Errors ... status=~"5.." ... / ...
HTTP 服务 RED Duration histogram_quantile(0.99, ...)
gRPC 服务 RED Rate sum(rate(grpc_server_handled_total[5m]))
gRPC 服务 RED Errors ... grpc_code!="OK" ...
MySQL USE+RED 连接饱和度 mysql_global_status_threads_running / mysql_global_variables_max_connections
MySQL RED 查询 QPS rate(mysql_global_status_queries[5m])
Redis USE 内存使用率 redis_memory_used_bytes / redis_memory_max_bytes
Redis RED 命令 QPS rate(redis_commands_processed_total[5m])
Kafka Golden Lag(饱和度) kafka_consumergroup_lag
Kafka RED 生产 QPS rate(kafka_server_brokertopicmetrics_messagesin_total[5m])
JVM 应用 USE 堆使用率 jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}
JVM 应用 USE GC 耗时 rate(jvm_gc_pause_seconds_sum[5m])
订单服务 KPI 订单成功率 sum(rate(orders_created_total{status="success"}[5m])) / sum(rate(orders_created_total[5m]))
支付服务 KPI 支付失败率 sum(rate(payment_total{result="failure"}[5m])) / sum(rate(payment_total[5m]))

5.3 怎么判断”该用哪一个”

实用决策树:

  1. 这是一个”资源”吗?(CPU、内存、磁盘……)→ USE
  2. 这是一个”请求响应”组件吗?→ RED
  3. 你需要预测性指标或从 SRE 视角建 SLO?→ Golden Signals
  4. 老板或 PM 会看这个指标吗?→ Business KPI

大多数生产服务同时需要前三者,关键业务服务还必须叠加第四类。


六、业务 KPI 与技术指标的映射

6.1 为什么纯技术指标不够

2020 年某头部电商平台曾发生过这样一起故障:购物车服务的 RED 指标完全正常(QPS 正常、5xx 为零、p99 延迟正常),但加购接口因为一次配置变更,错误地把所有 SKU 的库存判定为 0,于是每次加购都返回 HTTP 200 + {"success":false,"msg":"库存不足"}。整整一个多小时,技术监控全绿,业务同学从 GMV 曲线上才发现异常。

这就是”系统健康 ≠ 业务健康”的典型案例。HTTP 200 可以掩盖一切业务错误;如果不把业务状态埋点到监控系统,永远发现不了。

6.2 典型映射

技术指标(RED / USE) 业务 KPI 联动关系
订单服务 HTTP 200 成功率 订单创建成功率 近似一致,但需要排除”业务失败的 200”
支付网关 5xx 错误率 支付失败率 5xx 是支付失败的子集
搜索服务 p99 搜索结果呈现时间 延迟越长点击率越低
Redis 缓存命中率 商品详情页加载耗时 命中率低 → 降级到 DB → 延迟上升
Kafka lag 订单履约时效 lag 大 → 订单状态更新慢 → 用户感知变慢

6.3 美团公开的联动案例

美团技术团队在《美团监控平台的高可用架构实践》(2021)一文中提到,其监控平台”CAT + 天网 + Mt-Falcon”不仅采集技术指标,还把关键业务指标(订单、优惠券核销、骑手接单成功率)与技术指标关联展示。当某个 IDC 的订单创建率跌幅超过阈值时,平台会自动关联该 IDC 的基础设施指标和核心服务 RED 指标进行根因分析。这一设计本质上就是”Business KPI 作为告警入口,技术指标作为根因证据”的典型实现。

6.4 SLI 的精确定义

Google SRE Workbook 对 SLI(Service Level Indicator)的定义值得抄一遍:

SLI 是一个精心挑选的、能准确反映用户体验的技术指标。它必须满足:可量化、可比较、随服务质量单调变化。

不是所有技术指标都是 SLI。CPU 使用率是技术指标但不是 SLI——因为”CPU 使用率高”和”用户体验差”没有单调关系。HTTP 成功率是典型 SLI——成功率降低直接意味着用户体验变差。

业务 KPI 往往就是最佳的 SLI 候选:用户不关心你的 CPU,但关心能不能下单、能不能支付。

6.5 业务 KPI 的 Prometheus 实现

业务指标通常需要显式埋点。以 Go 语言订单服务为例:

package orders

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    ordersCreatedTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Namespace: "myshop",
            Subsystem: "orders",
            Name:      "created_total",
            Help:      "Total number of order creation attempts.",
        },
        []string{"channel", "result"}, // channel: app/web/h5; result: success/business_fail/system_fail
    )

    ordersAmountTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Namespace: "myshop",
            Subsystem: "orders",
            Name:      "amount_cents_total",
            Help:      "GMV in cents (only counted on success).",
        },
        []string{"channel"},
    )
)

func CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
    order, err := doCreateOrder(ctx, req)
    if err != nil {
        if isBusinessError(err) {
            ordersCreatedTotal.WithLabelValues(req.Channel, "business_fail").Inc()
        } else {
            ordersCreatedTotal.WithLabelValues(req.Channel, "system_fail").Inc()
        }
        return nil, err
    }
    ordersCreatedTotal.WithLabelValues(req.Channel, "success").Inc()
    ordersAmountTotal.WithLabelValues(req.Channel).Add(float64(order.AmountCents))
    return order, nil
}

对应的 Prometheus 查询:

# 订单成功率(1 分钟窗口)
sum(rate(myshop_orders_created_total{result="success"}[1m]))
  /
sum(rate(myshop_orders_created_total[1m]))

# 实时 GMV 速率(元/秒)
sum(rate(myshop_orders_amount_cents_total[1m])) / 100

6.6 业务指标的特殊性

相比 RED / USE,业务 KPI 有几个需要特别注意的特性:


七、Prometheus 指标命名规范

7.1 标准格式

Prometheus 官方文档 Best Practices for Naming 推荐的指标名格式:

<namespace>_<subsystem>_<name>_<unit>

7.2 Label(标签)的命名

Label 名使用 snake_case,不放入指标名中:

为什么?因为 Prometheus 查询依赖 label 做聚合。如果你把 method 写进指标名,就无法用一个查询同时统计所有 method。

7.3 单位后缀规范

类型 后缀 示例
Counter _total http_requests_total
_seconds http_request_duration_seconds
字节 _bytes process_resident_memory_bytes
比例 _ratio cpu_usage_ratio
信息 metric _info kubernetes_pod_info

关键约定:时间一律用秒(_seconds),不用毫秒。字节一律用字节(_bytes),不用 KB / MB。这个约定是为了避免聚合时单位转换错误。

7.4 四种指标类型的命名

类型 命名约定 适用场景
Counter 必须以 _total 结尾 单调递增的累计值(请求数、错误数、字节数)
Gauge 无特殊后缀,常用名词 瞬时值(队列长度、连接数、温度)
Histogram _seconds / _bytes 结尾,自动生成 _bucket_sum_count 分布类,用于延迟、包大小
Summary 同 Histogram 同 Histogram,但客户端计算分位数

7.5 好的命名 vs 坏的命名对比

坏的命名 好的命名 问题
api_latency http_request_duration_seconds 没有单位;缺少子系统前缀
errors http_requests_total{status=~"5.."} 不应该为错误单独建指标,用 label
userCount users_active 驼峰命名,应用 snake_case
requests_per_second http_requests_total Counter 应该记录总量,rate 在查询时计算
cpu_usage_percentage cpu_usage_ratio (0-1) 比例建议用 0-1 而非 0-100
memory_mb process_resident_memory_bytes 统一用字节
http_GET_requests_total http_requests_total{method="GET"} method 应该是 label

7.6 标准 HTTP 指标命名

这是 OpenMetrics 和 Prometheus 社区事实上的标准:

http_requests_total                   Counter  labels: method, status, handler
http_request_duration_seconds_bucket  Histogram labels: method, handler, le
http_request_duration_seconds_sum     Counter  labels: method, handler
http_request_duration_seconds_count   Counter  labels: method, handler
http_request_size_bytes_bucket        Histogram labels: method, handler, le
http_response_size_bytes_bucket       Histogram labels: method, handler, le

大多数框架(Spring Boot Actuator、Go 的 promhttp、Python 的 prometheus_client)都遵循这套命名,跨服务聚合才成为可能。


八、Histogram vs Summary 的选择

8.1 为什么需要分布类指标

延迟只给平均值会骗人:1000 次请求,999 次 10ms,1 次 10 秒,平均 20ms——看起来不错,但那 1 次 10 秒的用户体验是灾难。这就是为什么需要分位数。

8.2 Histogram:客户端分桶,服务端估算

Histogram 在客户端把观测值分到预定义的 bucket 中,每个 bucket 对应一个 Counter。例如定义 bucket [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10](单位:秒),每次观测都给”小于等于某个 bucket 上界”的所有 Counter +1。

服务端的 histogram_quantile 通过插值估算分位数。

关键优势:Histogram 可以跨实例聚合。因为 bucket 都是线性可加的 Counter,所以:

# 把 10 个实例的延迟合起来计算 p99
histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le))

这在微服务环境下至关重要——一个服务跑在 50 个 Pod 上,你想要的是”整个服务的 p99”,而不是每个 Pod 单独的 p99。

8.3 Histogram Bucket 设计原则

Bucket 设计是 Histogram 最容易做错的环节。经验法则:

  1. 围绕 SLO 设置:如果 SLO 是 p99 < 500ms,bucket 必须在 500ms 附近细粒度覆盖(例如 300ms、400ms、500ms、600ms、800ms)。
  2. 指数增长:在感兴趣的区间之外,bucket 以大约 2x 间距扩展。
  3. 覆盖极端值:最大 bucket 要覆盖到可能出现的极慢请求(10s、30s),否则超出最大 bucket 的请求会被计入 +Inf,分位数计算会高估。
  4. 数量控制:每个 Histogram 的 bucket 数量直接决定时间序列数,典型值 10—15 个 bucket 即可。

一个好的 HTTP 延迟 bucket 示例:

var DefaultHTTPLatencyBuckets = []float64{
    0.005, 0.01, 0.025, 0.05, 0.1,
    0.25, 0.5, 1, 2.5, 5, 10,
}

8.4 Summary:客户端分位数

Summary 在客户端直接计算分位数(通常用 t-digest 或 CKMS 算法),服务端直接读取数值。

http_request_duration_seconds{quantile="0.5"}  0.032
http_request_duration_seconds{quantile="0.9"}  0.125
http_request_duration_seconds{quantile="0.99"} 0.850

关键限制:Summary 不能跨实例聚合。分位数数学上不可加,avg(p99) 不是”整体 p99”——这是第十节会专门讨论的工程坑。

8.5 选型决策

维度 Histogram Summary
跨实例聚合 ✅ 支持 ❌ 不支持
分位数精度 取决于 bucket 设计,近似值 客户端精确计算
客户端开销 低(只是 counter 递增) 较高(滑动窗口 + 估算算法)
服务端开销 较高(每个 bucket 一条时间序列)
可以改分位数 ✅ 查询时决定 ❌ 埋点时决定
99% 的微服务场景 ✅ 首选
单实例精确分位数

结论:绝大多数微服务场景选 Histogram,不要犹豫。Summary 只在”单进程精确分位数”这个小众场景有优势。

8.6 Native Histogram(Prometheus 2.40+)

Prometheus 2.40(2022 年 11 月)引入了 Native Histogram(也叫 Sparse Histogram)。它解决了经典 Histogram 的核心痛点:bucket 需要预先设计。

Native Histogram 使用指数 bucket 方案(schema),bucket 数量和边界自动按观测值范围调整,可以做到”精度和空间都可配置”。在 Grafana Mimir 和高基数场景下,Native Histogram 显著降低了 series 数量。

不过截至 2024 年,Native Histogram 在 Grafana / Alertmanager 生态中的工具链支持仍在完善,生产环境采用前建议先小范围验证。

8.7 三语言埋点示例

Go(官方 client_golang):

import "github.com/prometheus/client_golang/prometheus/promauto"

var httpDuration = promauto.NewHistogramVec(
    prometheus.HistogramOpts{
        Namespace: "myshop",
        Subsystem: "http",
        Name:      "request_duration_seconds",
        Help:      "HTTP request latencies.",
        Buckets:   []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
    },
    []string{"method", "handler", "status"},
)

func Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriter{ResponseWriter: w, status: 200}
        next.ServeHTTP(rw, r)
        httpDuration.WithLabelValues(
            r.Method,
            normalizeHandler(r.URL.Path),
            strconv.Itoa(rw.status),
        ).Observe(time.Since(start).Seconds())
    })
}

Java(Micrometer):

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;

@Component
public class HttpTimingAspect {
    private final MeterRegistry registry;

    public HttpTimingAspect(MeterRegistry registry) {
        this.registry = registry;
    }

    public Object around(ProceedingJoinPoint pjp, String handler) throws Throwable {
        Timer.Sample sample = Timer.start(registry);
        String status = "200";
        try {
            return pjp.proceed();
        } catch (Exception e) {
            status = "500";
            throw e;
        } finally {
            sample.stop(Timer.builder("myshop.http.request.duration")
                .tag("handler", handler)
                .tag("status", status)
                .publishPercentileHistogram()  // 生成 Histogram 而不是 Summary
                .serviceLevelObjectives(
                    Duration.ofMillis(100),
                    Duration.ofMillis(500),
                    Duration.ofSeconds(1))
                .register(registry));
        }
    }
}

Micrometer 默认使用 Summary(它叫 “client-side percentile”),一定要显式调用 publishPercentileHistogram() 才会导出成 Prometheus Histogram,否则跨实例聚合会失效。这是 Spring Boot + Prometheus 最常见的陷阱之一。

Python(prometheus_client):

from prometheus_client import Histogram, CollectorRegistry
import time

HTTP_DURATION = Histogram(
    'myshop_http_request_duration_seconds',
    'HTTP request latency',
    ['method', 'handler', 'status'],
    buckets=(0.005, 0.01, 0.025, 0.05, 0.1,
             0.25, 0.5, 1, 2.5, 5, 10),
)

def middleware(app):
    def wrapper(environ, start_response):
        start = time.perf_counter()
        status_code = {'value': '200'}

        def custom_start_response(status, headers, exc_info=None):
            status_code['value'] = status.split(' ')[0]
            return start_response(status, headers, exc_info)

        try:
            return app(environ, custom_start_response)
        finally:
            HTTP_DURATION.labels(
                method=environ['REQUEST_METHOD'],
                handler=normalize_path(environ['PATH_INFO']),
                status=status_code['value'],
            ).observe(time.perf_counter() - start)
    return wrapper

九、基数(Cardinality)治理

9.1 基数的定义

一个 metric 的时间序列数量 = 所有 label 值组合的笛卡尔积。

举例:

http_requests_total{method, status, handler, service}

总时间序列数 = 5 × 20 × 50 × 10 = 50000

这是一个 metric,如果有 50 个 metric,就是 250 万 series。Prometheus 单机建议 series 总量控制在 1000 万以内。

9.2 高基数的典型错误

凡是把”值域无限或接近无限”的字段做 label,都是基数灾难:

label 字段 值域规模 结果
user_id 百万级 灾难
request_id / trace_id 每次请求一个 秒级爆炸
session_id 在线用户数 灾难
原始 URL path(含 ID) 接口数 × ID 数 灾难
错误堆栈 hash 理论无限 灾难
时间戳字符串 无限 灾难

正确的做法是把这些信息作为”结构化日志”或”Trace span attribute”,而不是作为 metric label。

9.3 线上事故案例

2020 年国内某大厂的一次公开事后复盘(脱敏整理):

这个案例在业内流传甚广,核心教训只有一句话:任何进入 label 的字段,都要先证明它的值域有限且可控

9.4 防御策略

策略 1:label 值审查

埋点时问三个问题:

  1. 这个 label 的可能取值集合能列出来吗?
  2. 取值数量是否随业务增长?
  3. 如果新增一个取值是否需要修改代码?

全部”是 / 否 / 是”才能作为 label。典型正例:status(HTTP 状态码枚举)、method(HTTP 方法枚举)、region(机房枚举)。典型反例:user_idurlerr_msg

策略 2:路径规范化(path normalization)

HTTP handler 要把动态部分替换成模板:

func normalizeHandler(path string) string {
    // /product/12345/detail → /product/:id/detail
    re := regexp.MustCompile(`/\d+(/|$)`)
    return re.ReplaceAllString(path, "/:id$1")
}

现代 Web 框架(Gin、Spring、FastAPI)都可以拿到路由模板,优先使用框架提供的模板而不是原始 URL。

策略 3:Recording rules 预聚合

对高基数的原始指标预聚合成低基数的中间指标:

# prometheus.rules.yml
groups:
  - name: http_aggregations
    interval: 30s
    rules:
      - record: service:http_requests:rate5m
        expr: sum(rate(http_requests_total[5m])) by (service, status)

      - record: service:http_request_duration_seconds:p99
        expr: histogram_quantile(0.99,
          sum(rate(http_request_duration_seconds_bucket[5m])) by (service, le))

Dashboard 和告警应该优先消费 service:* 命名空间的聚合指标,而不是原始指标。这既能降低查询压力,也能让上层用户免于接触高基数。

策略 4:series 数量硬限制

Prometheus 启动参数:

prometheus \
  --storage.tsdb.retention.time=15d \
  --query.max-samples=50000000 \
  --web.enable-admin-api

自 Prometheus 2.24 起,可以通过 limits 配置块为单个 scrape target 设置 series 限制:

scrape_configs:
  - job_name: 'app'
    sample_limit: 10000         # 每次 scrape 最多 10000 个 sample
    label_limit: 30             # 单个 metric 最多 30 个 label
    label_name_length_limit: 200
    label_value_length_limit: 200

策略 5:基数巡检

定期检查各 metric 的 series 数:

# 查询所有 metric 名
curl -s http://prometheus:9090/api/v1/label/__name__/values | jq '.data | length'

# 查询单个 metric 的 series 数(需要 Prometheus 2.20+)
curl -sG http://prometheus:9090/api/v1/query \
  --data-urlencode 'query=count({__name__="http_requests_total"})'

# 列出 series 数最高的 top 10 metric
curl -s http://prometheus:9090/api/v1/status/tsdb | \
  jq '.data.seriesCountByMetricName[:10]'

Prometheus 自带的 /api/v1/status/tsdb 接口直接返回 top 10 高基数 metric,应该把它接入日常巡检。

策略 6:Mimirtool cardinality check

Grafana Labs 的 mimirtool 提供专门的基数分析命令:

mimirtool analyze prometheus \
  --address=http://prometheus:9090 \
  --id=tenant1 \
  --output=cardinality.json

mimirtool analyze dashboard \
  --output-dir=./report \
  dashboards/*.json

它不仅能找出高基数 metric,还能分析 dashboard 和 rule 文件里未使用的 metric——未使用的指标是纯粹的成本浪费。

9.5 基数预算管理

成熟团队会把 series 配额作为”监控成本”分配给各业务团队:

团队 series 配额 当前使用 使用率
订单 1,000,000 820,000 82%
支付 500,000 510,000 102% ← 告警
风控 300,000 150,000 50%

配合告警规则:

groups:
  - name: cardinality_budget
    rules:
      - alert: TeamCardinalityBudgetExceeded
        expr: |
          sum by (team) (
            count by (team, __name__) ({__name__=~".+"})
          )
          >
          on(team) group_left team_cardinality_budget
        for: 1h
        annotations:
          summary: "Team {{ $labels.team }} exceeded cardinality budget"

十、工程坑点

真实踩过的坑(脱敏整理):

10.1 把 userID 写成 label

某电商 SaaS 的用户行为埋点:

// 错误
userActionCounter.WithLabelValues(userID, action).Inc()

教训:metric label 只放枚举值,高基数数据用日志或 APM。

10.2 直方图桶设错

SLO 要求:订单创建 p99 < 500ms。

错误的 bucket 定义:

Buckets: []float64{0.1, 1, 10}  // 100ms, 1s, 10s

当 p99 处于 100ms 和 1s 之间时,histogram_quantile 插值得到的值在 100ms 到 1000ms 之间线性分布——这意味着无论真实 p99 是 200ms 还是 800ms,计算结果的误差都可能达到数百毫秒。SLO 告警要么一直触发要么一直不触发,完全失去意义。

修复:重新设计 bucket,在 SLO 目标附近细粒度覆盖:

Buckets: []float64{0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.75, 1, 2, 5, 10}

教训:bucket 必须覆盖 SLO 目标且前后有细粒度梯度。

10.3 Counter Reset 没处理

某 Python 服务用 gunicorn 启动多 worker,每次热重启 Counter 归零。监控系统直接对原始值做差分:

# 错误:直接用当前值 - 上次值
delta = current_value - last_value

current_value < last_value(因为重启)时,delta 变成负数,错误率”突增”触发告警。

修复方式(二选一): - 使用 Prometheus rate()increase(),它们内部会自动处理 counter reset - 自己实现差分时,判断 current < last 则把 current 当作从 0 开始计算

if current < last:
    delta = current           # counter reset
else:
    delta = current - last

教训:永远用 rate() / increase(),不要自己做 Counter 差分。

10.4 Label 命名不一致

同一个微服务,Java SDK 上报的 label 是 serviceName,Go SDK 上报的 label 是 service_name,Python SDK 上报的 label 是 service。PromQL 聚合:

sum(rate(http_requests_total[5m])) by (service)

只聚合了 Python 服务的数据,Java 和 Go 的请求全部被当作”空 service”——Grafana 图上有一条巨大的”未知”曲线,但没人注意到。

修复:制定公司级监控 SDK 规范,强制 label 命名约定;CI/CD 中加入静态检查(promtool check metrics + 自定义规则)。

教训:label 命名必须企业级统一,一次规范,全员遵守。

10.5 Recording rules 滞后建设

一个业务 dashboard 有 30 个 panel,每个 panel 查询的都是原始高基数指标:

histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (service, handler, le))

每次打开 dashboard 都要扫描 5 分钟窗口 × 数十万 series × 30 个 panel。Prometheus CPU 打满,其他查询 timeout。

修复:把每个 panel 的聚合结果固化为 recording rule,interval 30s:

- record: service:http_request_duration_seconds:p99:5m
  expr: histogram_quantile(0.99,
        sum(rate(http_request_duration_seconds_bucket[5m])) by (service, handler, le))

Dashboard 改为消费 service:http_request_duration_seconds:p99:5m,查询瞬间由”扫描数十万 series”降为”读取数千条预聚合序列”,CPU 消耗降到原来的 1/50。

教训:任何在 dashboard / 告警中出现 2 次以上的复杂查询,都应该有 recording rule。

10.6 Summary 跨实例聚合

某 Java 服务用 Micrometer 默认配置(Summary)暴露 p99,Grafana 面板里:

avg(http_server_requests_seconds{quantile="0.99"}) by (service)

这个查询在数学上毫无意义。分位数不可平均:10 个实例各自 p99 = 500ms,不代表整体 p99 = 500ms。极端情况下 10 个实例 p99 = 500ms,但整体 p99 可能是 2000ms(因为最慢那 1% 请求集中在某个实例上)。

修复:切换到 Histogram(Micrometer 调用 publishPercentileHistogram()),然后:

histogram_quantile(0.99,
  sum(rate(http_server_requests_seconds_bucket[5m])) by (service, le))

教训: - 跨实例聚合一律用 Histogram - Micrometer 默认是 Summary,务必显式开启 Histogram - 代码评审重点检查 quantile="x" 外层是否有 avg / sum 等聚合——这是红旗信号


十一、选型建议与可抄的指标清单

11.1 不同规模团队的指标体系建议

团队规模 阶段建议 重点方法论 工具栈建议
< 10 人,< 5 服务 MVP:USE + 核心 RED USE 为主,关键服务加 RED 单 Prometheus + Grafana,15 天保留
10—50 人,10—30 服务 全量 RED + 业务 KPI 埋点 USE + RED + 核心业务 KPI Prometheus HA + Thanos / VictoriaMetrics,30 天保留
50—200 人,30—200 服务 引入 Golden Signals 与 SLO 全部四层 Mimir / VictoriaMetrics cluster,90 天保留
200+ 人,200+ 服务 基数治理、多租户、成本优化 全部四层 + 基数预算 Mimir + cardinality 巡检 + recording rules 自动化

11.2 基础设施层(USE)指标清单

# Node 层(node_exporter)
- node_cpu_usage_ratio                 # U: CPU 使用率
- node_cpu_saturation                  # S: load1 / cores
- node_memory_usage_ratio              # U: 内存使用率
- node_memory_swap_rate                # S: 换页速率
- node_disk_io_util_ratio              # U: 磁盘繁忙率
- node_disk_io_queue_depth             # S: I/O 队列
- node_disk_error_rate                 # E: 磁盘错误
- node_network_bandwidth_usage_ratio   # U: 带宽占用
- node_network_drop_rate               # S: 丢包
- node_network_error_rate              # E: 网卡错误
- node_filefd_usage_ratio              # U: fd 使用率

# Kubernetes 层
- kube_pod_status_phase                # 状态
- kube_pod_container_status_restarts   # 重启次数
- kube_node_status_allocatable_pods    # 容量

11.3 服务层(RED)必备指标

每个服务至少要有这 5 个指标(可以由 SDK / middleware 自动生成):

指标名 类型 必备 label 告警参考
http_requests_total Counter service, method, handler, status QPS 跌幅 > 50%
http_request_duration_seconds Histogram service, method, handler p99 > SLO 目标
http_request_size_bytes Histogram service, handler p99 > 10MB
http_response_size_bytes Histogram service, handler p99 > 10MB
http_in_flight_requests Gauge service > 实例最大并发

11.4 中间件指标清单

MySQL(mysqld_exporter):

# USE
- mysql_global_status_threads_running / max_connections  # 连接饱和度
- rate(mysql_global_status_slow_queries[5m])             # 慢查询 E
- mysql_global_status_innodb_row_lock_time_avg           # 锁等待 S

# RED
- rate(mysql_global_status_queries[5m])                  # QPS
- rate(mysql_global_status_com_select[5m])               # SELECT 速率
- rate(mysql_global_status_handlers_error[5m])           # Handler 错误

Redis(redis_exporter):

- redis_memory_used_bytes / redis_memory_max_bytes       # U: 内存使用率
- rate(redis_commands_processed_total[5m])               # R: 命令 QPS
- rate(redis_rejected_connections_total[5m])             # E: 拒绝连接
- redis_blocked_clients                                  # S: 阻塞客户端
- rate(redis_keyspace_hits_total[5m])
  / (rate(redis_keyspace_hits_total[5m])
     + rate(redis_keyspace_misses_total[5m]))            # 命中率

Kafka(kafka_exporter):

- kafka_consumergroup_lag                                # S: 消费滞后(核心)
- rate(kafka_topic_partition_current_offset[5m])         # R: 生产速率
- kafka_topic_partition_under_replicated_partition       # E: 副本未同步
- kafka_brokers                                          # 可用 broker 数

11.5 JVM 应用指标

- jvm_memory_used_bytes / jvm_memory_max_bytes          # 堆使用率
- rate(jvm_gc_pause_seconds_sum[5m])                    # GC 耗时速率
- rate(jvm_gc_pause_seconds_count[5m])                  # GC 次数
- jvm_threads_live                                       # 活跃线程
- jvm_threads_deadlocked                                 # 死锁线程
- jvm_classes_loaded                                     # 已加载类

11.6 业务 KPI 通用模板

每个业务域至少埋这几类:

# 交易
- business_orders_created_total{channel, result}
- business_orders_amount_cents_total{channel}
- business_payment_total{method, result}
- business_payment_amount_cents_total{method}

# 用户
- business_user_login_total{channel, result}
- business_user_register_total{channel, result}

# 风控
- business_risk_rules_triggered_total{rule, action}
- business_risk_reject_total{reason}

对应的核心 SLO:

slo:
  - name: order-creation-success
    sli: |
      sum(rate(business_orders_created_total{result="success"}[5m]))
        /
      sum(rate(business_orders_created_total[5m]))
    objective: 99.95%
    window: 30d

  - name: payment-success
    sli: |
      sum(rate(business_payment_total{result="success"}[5m]))
        /
      sum(rate(business_payment_total[5m]))
    objective: 99.99%
    window: 30d

11.7 推荐的 recording rules 基线

groups:
  - name: red_aggregations
    interval: 30s
    rules:
      - record: service:http_requests:rate5m
        expr: sum(rate(http_requests_total[5m])) by (service)
      - record: service:http_requests:error_rate5m
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
            /
          sum(rate(http_requests_total[5m])) by (service)
      - record: service:http_request_duration_seconds:p50:5m
        expr: histogram_quantile(0.5,
              sum(rate(http_request_duration_seconds_bucket[5m])) by (service, le))
      - record: service:http_request_duration_seconds:p99:5m
        expr: histogram_quantile(0.99,
              sum(rate(http_request_duration_seconds_bucket[5m])) by (service, le))

  - name: use_aggregations
    interval: 30s
    rules:
      - record: instance:node_cpu:usage_ratio5m
        expr: 1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance)
      - record: instance:node_memory:usage_ratio
        expr: 1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
      - record: instance:node_disk:io_util_ratio5m
        expr: rate(node_disk_io_time_seconds_total[5m])

11.8 最终 checklist

建设指标体系时,对着这份清单自查:

能把这份清单过全的团队,指标体系就已经超过 80% 的同行了。


十二、总结

四套方法论覆盖了从资源到业务的完整观测面:

构建指标体系不是”想到什么监控什么”,而是:沿着层次结构,逐层用方法论做完备性检查,再叠加基数治理与规范化,最后沉淀成可跨服务复用的标准 SDK + 告警规则。

下一步自然是埋点:谁来埋、埋在哪、埋多细、成本多高。这是下一篇《埋点哲学:粒度、采样、基数爆炸与成本模型》要讨论的内容。


参考资料

  1. Brendan Gregg, “The USE Method”, https://www.brendangregg.com/usemethod.html, 2012
  2. Tom Wilkie, “The RED Method: How to Instrument Your Services”, GrafanaCon 2015 演讲
  3. Niall Murphy, Betsy Beyer, Chris Jones, Jennifer Petoff, Site Reliability Engineering, O’Reilly / Google, 2016, Chapter 6 “Monitoring Distributed Systems”
  4. Prometheus 官方文档 - “Metric and Label Naming”, https://prometheus.io/docs/practices/naming/
  5. Prometheus 官方文档 - “Histograms and Summaries”, https://prometheus.io/docs/practices/histograms/
  6. Charity Majors, Liz Fong-Jones, George Miranda, Observability Engineering, O’Reilly, 2022
  7. 美团技术团队,《美团监控平台的高可用架构实践》,美团技术博客,2021
  8. Brendan Gregg, Systems Performance: Enterprise and the Cloud, 2nd ed., Addison-Wesley, 2020
  9. Rob Skillington, “Taming High Cardinality TSDB Behavior”, Chronosphere Engineering Blog, 2021
  10. Grafana Labs, “Analyzing metric cardinality with mimirtool”, Grafana Mimir 官方文档

上一篇可观测性 vs 监控:从 Zabbix/Nagios 到 OpenTelemetry 的二十年

下一篇埋点哲学:粒度、采样、基数爆炸与成本模型

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / observability

可观测性工程

从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。

2026-04-22 · architecture / observability

【可观测性工程】时序数据库内核:TSM、TSI、倒排索引与 Gorilla 压缩

深入时序数据库的存储内核:Prometheus TSDB 的 WAL 与块管理、InfluxDB 的 TSM 引擎与 TSI 倒排索引、Gorilla 压缩算法的数学原理、VictoriaMetrics mergeset 架构、ClickHouse MergeTree 作为 metrics 后端,以及国内大厂在 series churn 和 compaction 风暴上踩过的坑。


By .