← 中文资源

简体中文版本

指标基数

指标基数是唯一标签组合产生的序列数——用户 id 与完整路径把它乘到百万级——是指标系统最主要的成本与性能上限。

阅读英文原文

概念定义

一个指标不是一个数,而是“每个标签组合一个数”。基数就是你实际创建了多少个数。预期指标与实际序列数之间的差距,正是监控平台死掉的地方。

工作机制

被抓取目标按标签集暴露序列;服务器把每个不同组合都索引进内存并各自写入存储。两个各 100 取值的标签就是一万条序列,再加一个无界标签就乘上它的去重数。限制与重标记规则在摄取侧丢弃或分桶越界标签。

使用场景

持续管理:按指标序列数告警、像评审 schema 一样评审新标签;真正的高基数问题路由到 trace 与日志——它们按事件存上下文,无需预聚合。

常见误区

哈希标签值既保住基数又毁掉可查询性——两头全输。“标签越多监控越好”的真相是每个分区都按序列计费。基数爆炸常经依赖升级抵达:库版本标签遇上一个舰队的 pod。要持续对每指标序列数告警;分桶标签——路径模板代替原始 URL、状态类代替完整消息——要趁早刻意归一,否则原始标签会从某个端点渗入并蔓延。exemplar 提供桥梁:指标保持有界同时链接到逐请求的 trace。

代码示例

import promClient from 'prom-client';
import express from 'express';

const registry = new promClient.Registry();
promClient.collectDefaultMetrics({ register: registry, prefix: 'svc_' });

const ordersTotal = new promClient.Counter({
  name: 'orders_created_total',
  help: 'Total orders created',
  labelNames: ['tenant', 'channel'],
  registers: [registry],
});

const requestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Request latency distribution',
  buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5],
  labelNames: ['route', 'method'],
  registers: [registry],
});

async function handleCheckout(req, res) {
  const end = requestDuration.startTimer({ route: '/checkout', method: 'POST' });
  try {
    ordersTotal.inc({ tenant: 'acme', channel: 'web' });
    res.json({ ok: true });
  } finally {
    end();
  }
}

const app = express();
app.post('/checkout', handleCheckout);
app.get('/metrics', async (req, res) => {
  res.set('content-type', registry.contentType);
  res.end(await registry.metrics());
});

app.listen(3000);
from collections.abc import Iterable
from dataclasses import dataclass, field


@dataclass
class EmaTracker:
    period: int
    value: float | None = None
    alpha: float = field(init=False, default=0.0)

    def __post_init__(self) -> None:
        if self.period < 1:
            raise ValueError("period must be at least 1")
        self.alpha = 2.0 / (self.period + 1)

    def update(self, sample: float) -> float:
        if self.value is None:
            self.value = sample
        else:
            self.value = self.alpha * sample + (1 - self.alpha) * self.value
        return self.value

    def reset(self) -> None:
        self.value = None


def exponential_moving_average(values: Iterable[float], period: int) -> list[float]:
    tracker = EmaTracker(period)
    return [tracker.update(value) for value in values]


def simple_moving_average(values: list[float], window: int) -> list[float | None]:
    if window < 1:
        raise ValueError("window must be at least 1")
    smoothed: list[float | None] = []
    for index in range(len(values)):
        if index + 1 < window:
            smoothed.append(None)
        else:
            chunk = values[index + 1 - window : index + 1]
            smoothed.append(sum(chunk) / window)
    return smoothed


def main() -> None:
    samples = [10.0, 12.0, 11.0, 15.0, 14.0, 16.0]
    print("ema:", [round(value, 3) for value in exponential_moving_average(samples, 3)])
    print("sma:", simple_moving_average(samples, 3))


if __name__ == "__main__":
    main()