Chapter 03

数字,聚合,时间序列

Metrics 是「每分钟有多少请求、p99 延迟多少、当前连接数多少」。它用便宜的空间保留了系统的宏观脉搏——告警、容量规划、SLA 报告全靠它。OTel 的 Metrics API 设计比 Prometheus 更规整,还兼容它。

六种 Instrument

类型同步/异步方向用途
Counter同步单调递增请求数、字节数、error 数
UpDownCounter同步可增可减活跃连接数、队列长度
Histogram同步分布延迟、payload 大小
ObservableCounter异步(回调)单调进程累计 CPU time
ObservableUpDownCounter异步可增可减goroutine 数、堆大小
ObservableGauge异步瞬时值CPU 使用率、温度

记住一条原则:能算增量的用 Counter,只能读快照的用 Observable

Counter:最常用的第一个

import { metrics } from "@opentelemetry/api";

const meter = metrics.getMeter("order-service");

const requestCounter = meter.createCounter("http.server.requests", {
  description: "HTTP requests count",
  unit: "{request}",   // UCUM 单位
});

app.use((req, res, next) => {
  res.on("finish", () => {
    requestCounter.add(1, {
      "http.method": req.method,
      "http.route": req.route?.path ?? "unknown",
      "http.status_code": res.statusCode,
    });
  });
  next();
});

参数里那个对象叫 attributes(在 Prometheus 语境里等于 labels)——不同组合会生成不同的时间序列。

高基数警告
不要把 user.idsession.idrequest.id 放进 metric 的 attributes。百万用户 = 百万条时间序列,存储会爆炸。高基数标识符只放在 trace 的 attributes 里。

Histogram:p50/p95/p99 的源头

const latency = meter.createHistogram("http.server.duration", {
  description: "HTTP request duration",
  unit: "ms",
});

const start = Date.now();
await handle(req);
latency.record(Date.now() - start, {
  "http.route": req.route.path,
});

Histogram 不是返回原始值,而是把值分到里(如 [0-10ms, 10-50ms, 50-200ms ...])。后端从桶计算分位数。

Explicit Buckets vs Exponential Histogram

Explicit buckets(经典)
你预先定义桶边界 [10, 50, 100, 500, 1000]。简单,和 Prometheus 兼容,但需要预测数据范围。
Exponential Histogram(推荐)
OTel 独有,桶按指数缩放自动调整,分位数精度更高、存储更省。需要后端支持(Prometheus 3.0+ / Grafana Mimir / Datadog)。
// 通过 View 把某个 metric 配成指数直方图
new MeterProvider({
  views: [
    new View({
      instrumentName: "http.server.duration",
      aggregation: new ExponentialHistogramAggregation(),
    }),
  ],
});

UpDownCounter:可增可减

const activeConnections = meter.createUpDownCounter(
  "db.client.connections.active",
  { unit: "{connection}" }
);

pool.on("acquire", () => activeConnections.add(1));
pool.on("release", () => activeConnections.add(-1));

和 Counter 的区别只在可以减——Prometheus 里对应 Gauge 类型。

异步 Observable*

有些值不是"事件触发"而是"每隔一段时间读一次"——比如当前的堆内存、goroutine 数、温度。用 Observable,由 SDK 周期性调你的回调:

meter.createObservableGauge(
  "process.memory.heap_used",
  { unit: "By" },
  (observer) => {
    const used = process.memoryUsage().heapUsed;
    observer.observe(used, { "process.pid": process.pid });
  }
);

// 每个 metric 读取周期(默认 60s),SDK 调一次回调

View:重塑指标

View 是 SDK 层的"规则"——在数据离开前可以改名、改聚合方式、加/减 attributes

new MeterProvider({
  views: [
    // 1. 丢弃某些高基数 attribute
    new View({
      instrumentName: "http.server.duration",
      attributeKeys: ["http.method", "http.status_code"],  // 只保留这俩
    }),
    // 2. 重命名
    new View({
      instrumentName: "old.name",
      name: "new.name",
    }),
    // 3. 换桶
    new View({
      instrumentName: "http.server.duration",
      aggregation: new ExplicitBucketHistogramAggregation(
        [10, 50, 100, 500, 1000, 5000]
      ),
    }),
  ],
});

View 的威力在于运维和应用代码解耦——应用写 metric,运维调 view,不用改业务代码。

Delta vs Cumulative

Cumulative(累积)
每次上报都是"从启动到现在的总和"。Prometheus 默认。查询时要用 rate() 做差。
Delta(增量)
每次上报只是"从上次到现在的增量"。某些后端(Datadog、AWS)偏好。OTel SDK 配置一行即可切换。
new PeriodicExportingMetricReader({
  exporter: new OTLPMetricExporter({
    temporalityPreference: AggregationTemporality.DELTA,
  }),
  exportIntervalMillis: 60000,
});

选择原则:接 Prometheus 用 Cumulative,接 Datadog/AWS 用 Delta。Collector 也能在中间做 cumulativetodelta 转换。

单位:UCUM

OTel 用 UCUM(Unified Code for Units of Measure)标准单位:

含义UCUM 写法
毫秒ms
s
字节By
千字节KiBy
请求数(无量纲){request}(花括号包裹)
百分比(0-1)1(乘法因子)

和 Prometheus 兼容

Prometheus 有两种方式接 OTel:

方式一:Prom → OTel
Collector 用 prometheusreceiver 直接 scrape,原生 Prom 指标进入 OTel pipeline。适合逐步迁移。
方式二:OTel → Prom
app 用 OTLP 上报到 Collector,Collector 用 prometheusexporter 暴露 /metrics 端点给 Prometheus scrape。
方式三:Prom Remote Write
Collector 的 prometheusremotewriteexporter 直接把 OTel metrics 推给 Mimir/Thanos/Cortex。
命名差异
OTel 用 .(如 http.server.duration),Prometheus 用 _(如 http_server_duration_seconds)。Collector 会自动转换,你不用操心。

RED / USE 方法

埋点"埋什么"是门艺术,社区有两套经典方法论:

RED(针对服务)
Rate(QPS)· Errors(错误率)· Duration(延迟分布)。 对外提供的每个服务都应该有这三个 metric——用 Counter + Counter + Histogram 三个 instrument 就够。
USE(针对资源)
Utilization(使用率)· Saturation(饱和度)· Errors(错误)。 对每个资源(CPU、内存、磁盘、网络、DB 连接池)都应该有这三个维度。

一段完整的 RED 埋点

const requests = meter.createCounter("http.server.requests");
const errors = meter.createCounter("http.server.errors");
const duration = meter.createHistogram("http.server.duration", {
  unit: "ms",
});

app.use((req, res, next) => {
  const start = performance.now();
  res.on("finish", () => {
    const attrs = {
      "http.method": req.method,
      "http.route": req.route?.path ?? "unknown",
      "http.status_code": res.statusCode,
    };
    requests.add(1, attrs);
    if (res.statusCode >= 500) errors.add(1, attrs);
    duration.record(performance.now() - start, attrs);
  });
  next();
});

这 20 行代码 + Grafana 一张图,就是 RED 全部——QPS、错误率、p99 一网打尽。

Exemplars:Metrics ↔ Traces 桥梁

Exemplar 是一种"指标里嵌 trace_id"的机制——你在 Grafana 看 p99 突刺,点一下刺尖的 exemplar,就直接跳到那一条慢 trace。

# Collector 配置
processors:
  metricsgeneration:
    exemplars: true

# Prometheus 也要开 exemplar 支持
global:
  scrape_interval: 15s
  external_labels:
    send_exemplars: "true"

SDK 层只要同时开了 trace 和 metrics——当 metric 在 span 内被记录时,exemplar 会自动附带当前的 trace_id/span_id。

本章小结