概念定义
一个指标不是一个数,而是“每个标签组合一个数”。基数就是你实际创建了多少个数。预期指标与实际序列数之间的差距,正是监控平台死掉的地方。
工作机制
被抓取目标按标签集暴露序列;服务器把每个不同组合都索引进内存并各自写入存储。两个各 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()