01 为什么需要指标

摘要:

指标(Metrics)是对系统状态的数值量化——它将”系统健不健康”这个模糊的问题转化为可计算、可比较、可告警的数字。与日志记录离散事件、链路追踪记录请求调用链不同,指标记录的是连续的数值时间序列:CPU 使用率每秒一个点、请求延迟 P99 每 15 秒一个点、错误率每分钟一个点。这种连续性使得指标天然适合趋势分析、异常检测和容量规划。本文从指标的定义出发,深入解析 Prometheus 定义的四种指标类型(Counter、Gauge、Histogram、Summary)的精确语义和适用场景,然后介绍 USE 和 RED 两种经典的指标方法论,帮助工程师在面对一个新系统时知道”该监控什么”。


第 1 章 指标的本质

1.1 什么是指标

指标(Metric) 是在一段时间内对系统某个维度的数值度量。每个指标由三部分组成:

  • 名称(Name):描述度量的对象(如 http_requests_totalcpu_usage_percent
  • 标签(Labels):描述度量的维度(如 {service="order-service", method="POST", status="200"}
  • 数据点(Data Points):时间戳 + 数值的序列(如 (14:00:00, 150), (14:00:15, 163), (14:00:30, 171)

名称 + 标签的唯一组合构成一条时间序列(Time Series)。例如:

http_requests_total{service="order-service", method="POST", status="200"}
  → 一条时间序列,记录 order-service 的 POST 请求中状态码为 200 的请求总数

http_requests_total{service="order-service", method="POST", status="500"}
  → 另一条时间序列,记录状态码为 500 的请求总数

1.2 指标与其他信号的本质区别

指标与日志、链路追踪的根本差异在于数据形态

日志记录的是离散事件——每个事件是独立的,事件之间没有数学关系。你无法对”订单创建成功”和”支付超时”这两条日志做加减运算。

链路追踪记录的是请求级调用树——每条 Trace 描述一个具体请求的完整生命周期。Trace 是结构化的(有父子 Span 关系),但仍然是离散的。

指标记录的是连续数值——数据点之间有数学关系(可以计算变化率、求平均值、算分位数)。这种数学可操作性是指标的核心价值。

能力日志链路追踪指标
回答”发生了什么”✅ 详细的事件上下文✅ 请求的调用链❌ 只有数值
回答”有多少/多快/多高”❌ 需要手动统计❌ 需要聚合✅ 天然支持
趋势分析✅ 时间序列图
告警⚠️ 基于日志量✅ 基于阈值或变化率
容量规划✅ 线性回归预测
存储成本最高最低

1.3 为什么指标是告警的基础

告警的本质是”当某个条件持续满足时,通知工程师”。这个”条件”几乎总是基于指标的:

  • “当错误率 > 1% 持续 5 分钟” → 基于 error_rate 指标
  • “当 P99 延迟 > 2 秒持续 3 分钟” → 基于 http_request_duration_seconds 指标
  • “当磁盘使用率 > 85%” → 基于 disk_usage_percent 指标
  • “当消息队列积压 > 10,000” → 基于 kafka_consumer_lag 指标

日志也能触发告警(如”当 ERROR 日志每分钟 > 100 条”),但这本质上仍然是将日志转化为指标(计数)后再告警。链路追踪几乎无法直接用于告警——你不能对每条 Trace 设置告警规则。

指标的低存储成本也使其成为长期趋势分析的唯一可行选项。保留 1 年的 Prometheus 指标数据可能只需要几十 GB,而保留 1 年的日志数据可能需要数十 TB。


第 2 章 四种指标类型

Prometheus 定义了四种指标类型,覆盖了几乎所有的监控需求。理解它们的精确语义是正确使用指标的前提。

2.1 Counter(计数器)

Counter 是一个只增不减的累计计数器。它的值从 0 开始,只能递增(或在进程重启时归零)。

为什么需要 Counter?

假设你想监控”每秒处理的请求数”。最直觉的做法是维护一个变量 requests_per_second,每秒重置一次。但这种做法有严重的问题:如果 Prometheus 的 scrape 间隔是 15 秒,它可能错过中间的 14 次重置,得到的只是最后 1 秒的值——数据不准确。

Counter 的设计哲学是只记录累计值,将”速率计算”交给查询时处理。Prometheus 每次 scrape 只需要读取当前的累计值,然后通过 rate() 函数在查询时计算速率:

rate(http_requests_total[5m])
= (当前值 - 5分钟前的值) / 300秒
= 过去 5 分钟的平均每秒请求数

这种设计的好处是抗丢失——即使 Prometheus 错过了几次 scrape,只要有任意两个时间点的值,就能计算出这段时间的平均速率。

Counter 的典型用法

# 请求总数
http_requests_total{method="POST", status="200"} = 15839

# 错误总数
http_errors_total{service="order-service", type="timeout"} = 42

# 发送的字节总数
network_transmit_bytes_total{interface="eth0"} = 1073741824

Counter 的使用陷阱

陷阱一:不要直接展示 Counter 的原始值。http_requests_total = 15839 没有任何意义——你需要的是 rate(http_requests_total[5m]) 得到的每秒请求数。 陷阱二:Counter 在进程重启时会归零。rate() 函数会自动处理这种”归零跳变”(检测到值减小时视为重启),但如果你手动计算差值,需要自己处理。

2.2 Gauge(仪表盘)

Gauge 是一个可增可减的瞬时值。它记录的是”此刻”的状态,不是累计值。

Gauge 与 Counter 的区别

Counter 回答”到目前为止总共发生了多少次”,Gauge 回答”现在是多少”。Counter 像汽车的里程表(只增),Gauge 像温度计(可增可减)。

Gauge 的典型用法

# 当前 CPU 使用率
cpu_usage_percent{core="0"} = 73.5

# 当前内存使用量
memory_used_bytes{instance="worker-01"} = 8589934592

# 当前活跃连接数
active_connections{service="order-service"} = 42

# 当前消息队列积压
kafka_consumer_lag{topic="orders", group="order-processor"} = 1500

# 当前温度
temperature_celsius{location="server-room-A"} = 24.3

Gauge 可以直接展示原始值(不需要 rate()),也可以用于计算变化趋势:

# 内存使用量的变化率(每秒增加多少字节)
deriv(memory_used_bytes[1h])

# 如果按当前速率增长,还有多久磁盘会满
predict_linear(disk_used_bytes[1h], 3600 * 24)

2.3 Histogram(直方图)

Histogram 将观测值分布到预定义的桶(Bucket)中,用于计算分位数(如 P50、P90、P99)和平均值。

为什么需要 Histogram?

假设你想监控 HTTP 请求的延迟。最简单的做法是记录平均延迟——但平均值会隐藏极端情况。如果 99% 的请求 50ms 完成,1% 的请求 10 秒完成,平均延迟只有 150ms,看起来”还好”——但那 1% 的用户体验是灾难性的。

你需要的是分位数(Percentile):P99 表示”99% 的请求在这个延迟以内完成”。上面的例子中 P99 = 10s,远比平均值 150ms 更能反映真实的用户体验。

Histogram 通过将每个请求的延迟”投”到对应的桶中来近似计算分位数。每个桶定义了一个上界(upper bound),记录延迟 ≤ 该上界的请求累计数:

# Histogram 指标的实际存储:
http_request_duration_seconds_bucket{le="0.01"}  = 500    # ≤ 10ms 的请求有 500 个
http_request_duration_seconds_bucket{le="0.05"}  = 4800   # ≤ 50ms 的请求有 4800 个
http_request_duration_seconds_bucket{le="0.1"}   = 4950   # ≤ 100ms 的请求有 4950 个
http_request_duration_seconds_bucket{le="0.5"}   = 4990   # ≤ 500ms 的请求有 4990 个
http_request_duration_seconds_bucket{le="1.0"}   = 4995   # ≤ 1s 的请求有 4995 个
http_request_duration_seconds_bucket{le="5.0"}   = 4999   # ≤ 5s 的请求有 4999 个
http_request_duration_seconds_bucket{le="+Inf"}  = 5000   # 所有请求有 5000 个
http_request_duration_seconds_sum                = 250.5  # 所有请求的延迟总和
http_request_duration_seconds_count              = 5000   # 请求总数

通过 PromQL 的 histogram_quantile() 函数计算分位数:

# 计算 P99
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

桶的选择至关重要。如果桶的边界设置不合理(如只有 0.1s 和 10s 两个桶),分位数的估算精度会很差。桶边界应该覆盖预期的延迟范围,且在关键区域(如 SLO 阈值附近)设置更密的桶:

// Java Prometheus 客户端:自定义桶边界
Histogram requestDuration = Histogram.build()
    .name("http_request_duration_seconds")
    .help("HTTP request duration in seconds")
    .buckets(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10)
    .register();

Histogram 的桶是累积的

Histogram 的桶是累积桶(Cumulative Bucket)——le="0.1" 的值包含了 le="0.05" 的值。这种设计使得计算分位数只需要在桶之间做线性插值,不需要知道原始的每个观测值。代价是存储开销——每个桶对应一条时间序列,10 个桶 = 10 条时间序列(加上 _sum_count 共 12 条)。

2.4 Summary(摘要)

Summary 在客户端直接计算分位数,然后将计算结果暴露给 Prometheus。

# Summary 指标的实际存储:
http_request_duration_seconds{quantile="0.5"}   = 0.048   # P50 = 48ms
http_request_duration_seconds{quantile="0.9"}   = 0.092   # P90 = 92ms
http_request_duration_seconds{quantile="0.99"}  = 4.8     # P99 = 4.8s
http_request_duration_seconds_sum               = 250.5
http_request_duration_seconds_count             = 5000

Histogram 与 Summary 的核心区别

维度HistogramSummary
分位数计算位置服务端(PromQL 查询时)客户端(应用进程内)
跨实例聚合✅ 可以聚合多个实例的桶❌ 不能聚合分位数
精度取决于桶边界的粒度精确(基于流式算法)
CPU 开销低(只做桶计数)高(客户端计算分位数)
可动态调整分位数✅ 查询时指定任意分位数❌ 分位数在代码中固定

为什么 Histogram 几乎总是更好的选择?

Summary 最大的问题是不可聚合。假设 order-service 有 10 个实例,你想知道整个服务的 P99 延迟。如果使用 Histogram,你可以将 10 个实例的桶数据聚合后再计算分位数:

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

但如果使用 Summary,10 个实例各自报告自己的 P99——你无法将 10 个 P99 数学地合并为整体的 P99(P99 不满足可加性)。你只能看到 10 个独立的 P99 值,无法得到全局视图。

在微服务架构中,一个服务通常有多个实例,因此Histogram 是绝大多数场景的正确选择。Summary 只在以下极少数场景有优势:

  • 单实例服务,不需要跨实例聚合
  • 对分位数精度有极高要求(Histogram 的桶估算不够精确)
  • 客户端已有高效的分位数计算库

2.5 指标类型选型速查

监控对象推荐类型示例
请求总数、错误总数Counterhttp_requests_total
字节传输量Counternetwork_transmit_bytes_total
当前连接数Gaugeactive_connections
队列积压Gaugekafka_consumer_lag
CPU/内存使用率Gaugecpu_usage_percent
请求延迟分布Histogramhttp_request_duration_seconds
响应体大小分布Histogramhttp_response_size_bytes
批处理耗时分布Histogrambatch_job_duration_seconds

第 3 章 指标方法论:该监控什么

面对一个新系统,工程师最常见的困惑是”该监控什么指标”。两种经典的方法论提供了系统化的答案。

3.1 USE 方法论:面向基础设施

USE 方法论由 Brendan Gregg(《Systems Performance》作者)提出,适用于基础设施资源(CPU、内存、磁盘、网络)的监控。

USE 代表三个维度:

  • U(Utilization,利用率):资源被使用的比例(如 CPU 使用率 80%)
  • S(Saturation,饱和度):资源的排队/等待程度(如 CPU 运行队列长度 > 核心数)
  • E(Errors,错误):资源的错误事件数(如磁盘 I/O 错误、网络丢包)

USE 矩阵

资源Utilization(利用率)Saturation(饱和度)Errors(错误)
CPUcpu_usage_percentnode_load1(1 分钟负载)机器校验错误
内存memory_used_bytes / totalSwap 使用量、OOM 次数ECC 错误
磁盘 I/Odisk_io_util(iostat %util)I/O 队列深度 avgqu-szdisk_io_errors
网络带宽使用率TCP Retransmits、丢包率NIC 错误计数
连接池active / max等待获取连接的线程数连接超时次数

为什么”饱和度”比”利用率”更重要?

利用率告诉你资源”用了多少”,但不告诉你”够不够用”。CPU 利用率 90% 在批处理场景中可能完全正常(充分利用资源),但在在线服务场景中可能意味着延迟即将飙升——因为新请求需要排队等待 CPU 时间片。

饱和度直接衡量”排队程度”——当饱和度开始上升时,意味着资源已经成为瓶颈,用户可感知的延迟即将增加。在告警策略中,饱和度告警通常比利用率告警更有价值

3.2 RED 方法论:面向服务

RED 方法论由 Tom Wilkie(Grafana Labs 联合创始人)提出,适用于面向用户的服务(如 HTTP API、gRPC 服务)的监控。

RED 代表三个维度:

  • R(Rate,速率):每秒请求数(QPS/RPS)
  • E(Errors,错误):每秒错误请求数(或错误率)
  • D(Duration,耗时):请求延迟分布(P50、P90、P99)

RED 指标示例

# Rate:每秒请求数
rate(http_requests_total[5m])

# Errors:错误率
rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m])

# Duration:P99 延迟
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

为什么 RED 如此简洁有效?

RED 的三个维度直接对应用户体验的三个方面:

  • Rate:系统在处理多少请求?(是否有流量异常——突增或跌零?)
  • Errors:有多少请求失败了?(用户是否遇到错误?)
  • Duration:请求有多快?(用户是否感到”慢”?)

如果一个服务的 Rate、Errors、Duration 三个指标都正常,那么从用户视角看,这个服务就是健康的。这三个指标构成了服务健康度的最小充分集

3.3 USE + RED 的组合

USE 和 RED 并非互斥——它们覆盖不同的层次:


graph TD
    subgraph "用户视角(RED)"
        R["Rate</br>每秒请求数"]
        E["Errors</br>错误率"]
        D["Duration</br>延迟分位数"]
    end

    subgraph "基础设施视角(USE)"
        CPU["CPU</br>利用率/饱和度/错误"]
        MEM["内存</br>利用率/饱和度/错误"]
        DISK["磁盘</br>利用率/饱和度/错误"]
        NET["网络</br>利用率/饱和度/错误"]
    end

    R --> CPU
    R --> NET
    E --> MEM
    D --> DISK
    D --> CPU

    classDef red fill:#44475a,stroke:#ff79c6,color:#f8f8f2
    classDef use fill:#44475a,stroke:#8be9fd,color:#f8f8f2

    class R,E,D red
    class CPU,MEM,DISK,NET use

排查逻辑:先用 RED 发现服务层面的异常(如 P99 延迟上升),然后用 USE 定位底层资源瓶颈(如 CPU 饱和度上升 → CPU 是瓶颈)。


第 4 章 指标的命名规范

4.1 Prometheus 的命名约定

良好的指标命名是可维护性的基础。Prometheus 社区推荐以下命名约定:

格式<namespace>_<subsystem>_<name>_<unit>

# ✅ 好的命名:
http_requests_total                        # 总请求数(Counter)
http_request_duration_seconds              # 请求延迟,单位秒(Histogram)
process_resident_memory_bytes              # 进程常驻内存,单位字节(Gauge)
node_disk_io_time_seconds_total            # 磁盘 I/O 时间,单位秒(Counter)

# ❌ 差的命名:
requests                                    # 缺少 namespace 和单位
http_request_latency_milliseconds          # 不应用毫秒(Prometheus 约定用基本单位:秒、字节)
order_service_http_req_cnt                 # 缩写影响可读性

命名规则

  • Counter 类型以 _total 结尾
  • 使用基本单位:秒(不是毫秒)、字节(不是 KB/MB)
  • 使用蛇形命名法(snake_case)
  • 避免缩写(requests 而非 reqduration 而非 dur

4.2 标签的设计原则

标签的基数(Cardinality)直接影响 Prometheus 的性能和存储成本——每个唯一的标签组合对应一条时间序列。

http_requests_total{service="order", method="POST", status="200", instance="10.0.0.1:8080"}
http_requests_total{service="order", method="POST", status="500", instance="10.0.0.1:8080"}
http_requests_total{service="order", method="GET",  status="200", instance="10.0.0.1:8080"}
...

每增加一个标签值,时间序列数量可能成倍增加。

高基数标签是 Prometheus 的头号杀手

Grafana Loki 的标签基数问题类似(参见 04 Loki 日志系统深度解析),Prometheus 同样对高基数标签极度敏感。

  • ✅ 适合做标签:method(~5 个值)、status(~10 个值)、service(~50 个值)
  • ❌ 不适合做标签:user_idorder_idrequest_path(含路径参数)、ip_address

如果 request_path 包含路径参数(如 /api/users/12345),应该将路径参数归一化为模板(/api/users/{id}),否则每个不同的用户 ID 都会产生一条新的时间序列。


第 5 章 指标的局限性

5.1 指标不能回答”为什么”

指标告诉你”P99 延迟从 200ms 涨到了 5 秒”,但不能告诉你”为什么涨了”。你需要结合链路追踪(定位慢在哪个服务/哪个 Span)和日志(查看具体的错误信息/SQL 语句)才能找到根因。

5.2 指标的聚合会丢失细节

指标本质上是聚合数据——rate(http_requests_total[5m]) 给出的是 5 分钟内的平均 QPS,掩盖了这 5 分钟内的瞬时波动。如果在第 1 分钟有一个 10 秒的流量尖峰,5 分钟的平均 QPS 可能看起来完全正常。

同理,P99 延迟是一个统计量——它告诉你”99% 的请求在 X 毫秒内完成”,但不告诉你那 1% 的慢请求具体是哪些、慢在哪里。

5.3 指标不适合记录个体事件

如果你需要知道”用户 A 在 14:00:05 的请求耗时是多少”,指标无法回答——它只有聚合后的分位数,没有单个请求的数据。这种需求需要链路追踪或日志来满足。

指标、日志、链路追踪的协作模型

05 日志与链路追踪的联动 中我们讨论了日志与 Trace 的联动。指标在这个协作模型中的角色是第一发现者——通过指标告警发现异常,然后通过 Exemplar 跳转到链路追踪定位瓶颈,再跳转到日志查看细节。三者的协作构成了完整的可观测性排查闭环。


参考资料

  1. Prometheus Metric Types:https://prometheus.io/docs/concepts/metric_types/
  2. Brendan Gregg (2013). The USE Methodhttps://www.brendangregg.com/usemethod.html
  3. Tom Wilkie (2018). The RED Methodhttps://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/
  4. Prometheus Naming Best Practices:https://prometheus.io/docs/practices/naming/
  5. Google SRE Book, Chapter 6: Monitoring Distributed Systems:https://sre.google/sre-book/monitoring-distributed-systems/

思考题

  1. Prometheus 使用 Pull 模型——定期从目标拉取指标。Pull 模型的优势是 Prometheus 控制采集节奏、容易发现目标不可用。但 Pull 模型对短生命周期的 Job(如批处理任务)不友好——Job 可能在 Prometheus 拉取前就结束了。Pushgateway 解决了这个问题——Job 将指标推送到 Pushgateway,Prometheus 从 Pushgateway 拉取。Pushgateway 的局限是什么(如不支持实例级别的 UP 检测、可能成为单点)?
  2. Prometheus 的本地存储(TSDB)使用 WAL + Chunk 文件存储时间序列。默认保留 15 天。在什么场景下 15 天不够(如容量规划需要月度/季度趋势)?Remote Write 到长期存储(Thanos、Mimir、VictoriaMetrics)如何实现?Remote Write 的性能开销和可靠性如何?
  3. Prometheus 的服务发现(Service Discovery)自动发现监控目标——在 Kubernetes 中通过 API 发现 Pod/Service/Node。relabel_configs 允许在采集前修改标签——如从 Pod 的 Annotation 中提取 metrics 端口。你如何设计 relabel_configs 以实现’只监控有特定 Annotation 的 Pod’?