基数是指标的预算
一个很短的指标名,背后可能对应大量存储与查询成本。Prometheus 使用指标名及完整标签集合标识一条时间序列,每出现一种新的实际标签组合,就形成另一条需要采集、建立索引、保留和查询的序列。基数预算关注的是,哪些区别值得付出这笔成本,以及服务增长时,这些区别的数量是否仍有明确上限。它不是单纯要求标签越少越好,而是要求每个维度都有可解释的用途。 统计真正出现的组合 标签维度会扩大可能的组合数,但只有每种组合都实际出现时,各维度取值数的乘积才等于真实数量。两条路由、两种方法、两类状态最多可以形成八种组合;如果每条路由只允许一种方法,导出的组合就可能更少。因此既要计算理论上界,也要核对实际输出,并把实例等采集目标标签纳入服务端最终接收的序列估计。 取值集合比标签名字是否好听更重要。标准化路由可以把同一类订单操作归在一起,而包含每个订单编号的原始路径会持续增加取值。请求编号、时间戳、用户标识以及任意查询字符串,都可能随着事件量增长。开发者把标签叫作路由,并不意味着它真的有界;还要检查真实流量中的值,尤其是错误路径、未匹配路径和很少出现的边界请求,避免正常测试掩盖高基数来源。 把指标表示方式一起算进去 对于每组实际标签,普通计数器通常产生一条主要序列,另外可能存在伴随指标。经典直方图则会展开成多个桶、总和与计数组件。如果有六条有限桶边界,再加正无穷桶、总和及计数,每组标签就对应九条核心序列。有些客户端还会导出创建时间序列,真实清单必须把它算上,不能将用于理解原理的简化公式当成完整埋点总量。 多个副本还会增加实例维度。采集间隔决定每条序列多久写入一个样本,保留期限决定历史数据保存多久,它们与不同序列的数量不是同一概念。不断变化的标识还会制造时序更替:生产端已经消失的短命组合,历史样本仍可能继续留在存储里。因此应分别观察活跃序列、新建速度、写入量、内存和查询表现,不能拿一个固定的“每条序列占多少字节”覆盖所有配置与工作负载。 坏掉的样子 看似方便排障的标签,可能把聚合指标变成逐请求事件仓库。只在仪表盘查询中去掉该标签,无法撤销已经发生的采集和保留成本。对每个标识计算哈希,也不会消除其唯一性;截断随机标识通常只是推迟增长问题,同时引入碰撞。如果业务真正需要的是分类统计,就应选择有明确上限的类别,而不是给无限集合换一种编码方式。 也不能为了降低成本,把识别用户影响所必需的区别全部压平。保留有意义且有界的结果或服务维度,把单次请求调查交给具有相应访问控制的日志和链路记录。如果在采集过程中删除标签,还要检查一次采集内是否出现重复的最终序列,以及诊断信息是否丢失。能够修改生产端时,应优先在那里修正,再验证新标签集合,并明确旧历史序列如何随保留策略逐步退出。 快速验证 保存为 cardinality.py 并使用 Python 3 执行。人为构造的事件把路由、方法和状态的每一种组合重复五十次。一种表示只保留这些有界维度,另一种再增加唯一的请求标识。计算假定二十个副本都导出相同的组合,并使用六条有限边界的经典直方图;这个前提在代码旁明确列出,便于读者核对乘法来源。 from itertools import product routes = ["/orders/{order_id}", "/products/{product_id}"] methods = ["GET", "POST"] statuses = ["200", "500"] events = list(product(routes, methods, statuses, range(50))) bounded = {(route, method, status) for route, method, status, _ in events} with_request_id = {(route, method, status, f"request-{i}") for i, (route, method, status, _) in enumerate(events)} finite_buckets = 6 # Core classic-histogram series: finite buckets, +Inf, _sum, _count. # Optional _created series are deliberately excluded from this calculation. series_per_combination = finite_buckets + 3 replicas = 20 bounded_series = len(bounded) * series_per_combination * replicas request_id_series = len(with_request_id) * series_per_combination * replicas assert len(events) == 400 and len(bounded) == 8 assert len(with_request_id) == 400 assert bounded_series == 1440 and request_id_series == 72000 print(f"observed label sets: bounded={len(bounded)}, request_id={len(with_request_id)}") print(f"core classic-histogram series: bounded={bounded_series}, request_id={request_id_series}") print(f"multiplier={request_id_series // bounded_series}x") 四百条事件在有界表示下被聚合为八种组合,带请求标识时则保留四百种。扩展到二十个副本后,两者分别产生一千四百四十条与七万二千条核心直方图序列,相差五十倍。示例明确排除了可选创建时间序列和其他指标族,因此输出是给定假设下的序列数量模型,不是对 Prometheus 内存或磁盘占用进行过真实压测的结果。 ...