计数器速率估计主题封面,展示观察、判断与行动三个步骤。

Prometheus rate 估计计数器每秒的增长速度

请求计数器回答进程累计记录了多少事件,速率则回答这些事件到来得有多快。Prometheus 的 rate 估计指定范围内每秒平均增长量,修正能够观测到的计数器重置,并向范围边界进行外推。结果具有明确的时间单位,适合观察负载变化;但它不是两次抓取之间全部事件的精确还原,更不能凭空补回没有采到的重启历史。 采样决定能够推断多少事实 普通计数器在重置前持续增长,进程重启是常见的重置原因。相邻已观测样本出现下降时,计数器函数会按重置处理。不过,如果两次抓取之间已经重启并重新累计,而且新值超过旧样本,重置可能完全不可见。提高抓取频率可以提升观测分辨率,但任何查询都不能确定地恢复一段没有被记录的历史。 应先对每条计数器序列求 rate,然后聚合结果。如果先求和,一台实例的增长可能掩盖另一台实例的重置。标签决定序列身份,替换实例使用新标签时,是开始另一条序列,不是自动接续旧实例。队列长度等仪表值本来就允许正常下降,把它们交给计数器速率函数,会把真实下降错误解释为重置。 选择的范围也是指标含义的一部分 范围选择器左开右闭。在第五分钟求 [5m] 时,恰好位于第零分钟的样本不在范围内。对普通浮点计数器求速率至少需要两个可用样本;只有一个样本时得到空结果,并不表示业务流量为零。因此,范围不仅要覆盖正常抓取周期,还需要考虑偶发漏抓后的样本数量,不能仅凭希望曲线更灵敏就无限缩短。 较长窗口通常会平滑短促突发,也让突发离开平均值的时间更晚;较短窗口响应更快,但观测更少,而且可能变成空结果。应依据事件持续时间与检测目标选择范围,并在面板上明确标注。irate 使用最后两个样本,rate 使用所选范围,两者可以针对同一指标回答不同问题,只要面板清楚说明各自含义,不把数值当成同一种口径比较。 规则评估周期决定何时提出查询问题,不决定目标已经被抓取了多少次。如果目标每分钟才采集一次,即使面板每秒刷新,也不会凭空产生新的观测。分析告警响应速度时,需要分别检查采集与评估这两个周期,不能只缩短页面刷新时间。 坏掉的样子 刚创建的序列可能要等第二个样本到来后才出现在速率面板。把所有空值都补成零,可能掩盖采集故障。遇到空白,应先检查原始序列、时间戳和抓取状态,再判断业务是否空闲。另一种问题是窗口太长:短时间过载已经影响用户,但平均线被长窗口稀释,看起来始终平稳,告警因而没有达到阈值。 重置修正不代表缺口对准确性没有影响。漏抓可能隐藏突发或多次重启,范围边界的外推本身也属于估计。increase 以事件数量单位表达相应的范围估计,即使原始计数器只有整数,也可能返回小数。如果场景需要精确账务或计费总数,应依靠业务记账记录,不能把监控面板描述成可以完整重建交易流水的工具。 快速验证 把程序保存为 rate.py,执行 python3 rate.py。实验需要 Docker 与 Prometheus 3.5.1 镜像,镜像未缓存时可由 Docker 下载。程序调用真实 promtool 查询引擎,输入每分钟一份的合成样本。一次性容器没有外部网络,也不挂载宿主机目录,结束时会自动删除;运行结果由引擎断言决定,不是程序自己模拟查询公式。 import subprocess import uuid # Requires Docker and the public Prometheus 3.5.1 image. tests = r"""evaluation_interval: 1m tests: - interval: 1m input_series: - series: "demo_requests_total{instance=\"steady\"}" values: "0+60x5" - series: "demo_requests_total{instance=\"restart\"}" values: "100 160 220 20 80 140" - series: "demo_requests_total{instance=\"gaps\"}" values: "0 60 _ 180 _ 300" - series: "demo_requests_total{instance=\"one\"}" values: "_ _ _ _ _ 7" promql_expr_test: - expr: "rate(demo_requests_total{instance=\"steady\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"steady\"}" value: 1 - expr: "abs(rate(demo_requests_total{instance=\"restart\"}[5m]) - 5/6) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 1 - expr: "increase(demo_requests_total{instance=\"restart\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 250 - expr: "irate(demo_requests_total{instance=\"restart\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 1 - expr: "rate(demo_requests_total{instance=\"gaps\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"gaps\"}" value: 1 - expr: "rate(demo_requests_total{instance=\"one\"}[5m])" eval_time: 5m exp_samples: [] """ rules = r"""""" script = "set -eu\ncat > /tmp/tests.yml <<'TEST_DATA'\n" + tests + "\nTEST_DATA\n" if rules: script += "cat > /tmp/rules.yml <<'RULE_DATA'\n" + rules + "\nRULE_DATA\n" script += "exec /bin/promtool test rules /tmp/tests.yml\n" name = "xbcatm-prom-unit-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-s"], input=script, text=True, capture_output=True, timeout=45, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 断言全部通过时输出 SUCCESS。稳定序列每秒增加一个请求。对于 100 160 220 20 80 140,五分钟 rate 为 5/6,increase 为二百五十,irate 为一。这些结果不同,是因为范围排除了最初样本,随后修正可见重置,再把所得到的平均增长外推到请求时长;二百五十不能被描述为原始样本直接观测到的增量。 ...

十二月 24, 2027 · 2 分钟 · XB Field Notes
样本过滤与标签删除主题封面,展示观察、判断与行动三个步骤。

样本重新标记在入库前过滤指标

Prometheus 存在多个重新标记阶段,相似语法容易遮住不同作用。目标重新标记在采集之前处理发现目标,样本重新标记则在抓取之后、入库之前处理采集样本。前一个阶段的 drop 可以让某个端点不被抓取,后一个阶段的相同动作则从已经获取的响应中丢弃匹配样本。首先确定阶段,才能判断配置到底改变了哪一部分观测。 先决定删除数据,还是删除标签 在 metric_relabel_configs 中,action: drop 匹配组合后的源标签值,并丢弃匹配样本。使用 __name__ 作为输入时,可以拒绝指定指标族。它不是删除这个源标签却保留样本的意思。如果对一个广泛存在的值使用过宽条件,可能丢掉远超预期的数据,所以不能仅凭动作名称中有“删除”就认为它只影响元数据。 action: labeldrop 则把正则表达式用于标签名称,从保留样本中移除匹配标签;labelkeep 只保留名称匹配的标签。两者都不会求和或生成汇总。前一类操作删除观测,后一类操作改变身份,应该按照预期数据契约选择,而不是把名称相近的动作当成可互换参数。留下数值但删掉维度,也可能比直接过滤一部分样本带来更隐蔽的问题。 重新标记表达式默认在两端锚定。替换或过滤规则需要在对应阶段使用正确的源标签与模式。发现元数据通常属于目标阶段,到样本处理阶段时,已不能继续期待使用 __meta_ 输入。需要保留的上下文应提前复制到普通目标标签,而不是假定每个导出指标都会携带内部发现字段。配置语法相同,不代表输入集合也相同。 删除维度之后仍须保持唯一身份 时间序列由指标名和完整标签集合识别。移除区分两条观测的标签,可能让它们身份重叠,产生重复样本冲突或丢失区别。Prometheus 不会自动推断这些数值该求和、取平均还是任选其一。如果真正需要服务总量,应优先采用受控维度的仪表化以及明确查询聚合,而不是把标签删除当成自动汇总机制。 本例只在剩余路由身份本来就唯一的情况下删除构建标签,这是受控前提,不表示可以从任意导出器移除任何昂贵标签。上线前应使用有代表性的输出检查碰撞,也应核对引用被删标签的面板和告警。抓取成功只能说明采集过程完成,不能保证这些消费者仍会选择正确数据,更不能保证历史与新数据可以直接比较。 坏掉的样子 把样本过滤规则放进目标重新标记中,可能移除整个端点,而不是一类昂贵指标。反过来,样本过滤也不会阻止导出器生成响应,或阻止响应字节经过网络。它减少的是入库保留的样本。如果昂贵部分在采集本身,还需要处理仪表化或导出器行为,不能把存储侧过滤描述为同时消除了全部上游成本。 up 等自动生成的抓取序列不经过样本重新标记,因此在这个阶段尝试删除 up,不会隐藏系统生成的指标。健康的 up 可以与被主动丢弃的业务指标同时存在,解释数据缺失时应结合过滤配置。修改规则影响未来入库,不会重写已经保存的历史标签,所以查询跨越变更时刻时也可能看到两种身份。 快速验证 保存为 metric-relabel.py 后执行 python3 metric-relabel.py。需要 Docker 与 Prometheus 3.5.1,必要时由 Docker 获取镜像。同一个静态文本端点分别被原始任务与过滤任务抓取。路径使用 .txt 以提供文本内容类型,启动过程等待真正出现样本,不假定服务刚启动时目标列表已经填充完成。 ...

八月 4, 2027 · 2 分钟 · XB Field Notes
有界标签与请求标识形成的高基数标签,产生不同规模的直方图序列。

基数是指标的预算

一个很短的指标名,背后可能对应大量存储与查询成本。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 内存或磁盘占用进行过真实压测的结果。 ...

七月 21, 2027 · 1 分钟 · XB Field Notes
Pushgateway 指标生命周期主题封面,展示观察、判断与行动三个步骤。

Pushgateway 保留批任务指标,直到生命周期被主动处理

短生命周期任务可能在 Prometheus 下一次抓取之前就结束。Pushgateway 提供中间层,保留任务推送的指标以供后续抓取,但它不是每个短进程的默认方案。Prometheus 主要建议把它用于适当的服务级批任务结果。保留数据的生命周期与任务自身独立,因此抓取成功不能证明任务最近运行过,也不能证明业务工作已经成功完成。 为稳定的批任务结果建立身份 夜间对账这样的服务级任务,可以把最近结果发布到稳定分组键下。如果把新的运行编号、时间戳或机器身份放进键中,每次运行都可能创建额外分组,而不是更新同一份结果。Pushgateway 没有自动清理旧分组的过期时间机制,负责人需要明确替换或删除相关数据,不能假定退出任务后数据也会跟着消失。 与具体机器相关的批任务,往往可以使用节点导出器的文本文件采集器。共享网关本身也增加了可用性依赖。它的 up 表示 Prometheus 能否抓取网关,不表示每个原始推送任务都健康。应根据观测事件与清理责任选择采集方式,而不是把推送当成绕过服务发现或网络策略的通用办法。任务语义没有确定时,先增加网关并不会自动解决监控定义。 替换范围由 HTTP 方法决定 URL 路径定义分组键,包括任务名称与其他标签对。HTTP PUT 替换这个精确分组中的指标;POST 则替换本次请求包含的同名指标族,同时保留组内其他指标族。POST 不表示给原值递增,也不是向持久事件日志追加一条记录。如果一次报告把数值从七写成九,结果是九,而不是自动累加成十六。 DELETE 删除 URL 标识的精确分组。仅删除任务名构成的分组键,不会删除所有同时包含该任务名和额外标签的分组。网关文档规定的方法顺序允许后续成功推送排在先前已入队的删除之后,这可以用于验证清理结果。不能只看到删除请求被接受,就认定所有名称相似的序列都已经被处理,尤其不能把分组键误当成标签子集查询。 抓取时间不等于最近成功时间 网关通过 push_time_seconds 导出某组最近成功推送的时间。读取指标端点不会推进这个值。Prometheus 为采集样本赋予抓取时间戳,所以刚刚抓到的样本仍可能描述很久以前的任务结果。如果真正关心任务何时成功完成,应把最近成功时间作为独立的仪表值导出,并按任务计划检查其新鲜度。 不要给推送的指标样本附带显式样本时间戳,Pushgateway 会拒绝这种输入。把 Unix 时间戳作为仪表指标的数值是另一回事,可以正常使用。推送成功也不能证明业务成功或已经立即持久化:任务可以成功报告自己的失败,而没有配置持久化的网关在重启后可能丢失保留数据。各项完成语义应分别核对。 坏掉的样子 任务失败或被停用之后,旧的成功指标可能一直存在。只检查序列是否存在的告警,就会漏掉没有发生的新执行。应把最近成功值与预期调度比较,并让任务移除与分组清理同步。若分组标签不断改变,新旧运行还可能永久并列显示,给面板制造重复任务或持续健康的错觉,而实际已经没有新的成功结果。 本来想发布完整快照,却使用 POST,可能保留本次任务已不再输出的指标;只准备更新一个指标,却使用 PUT,又可能删除其他面板仍需要的结果。应明确哪个组件拥有整个分组,以及它使用哪种方法。不存在能够代替这些 HTTP 语义的 --replace 选项,不能把一个未经核实的命令参数写进操作流程。 ...

五月 13, 2027 · 2 分钟 · XB Field Notes
直方图聚合与分位数主题封面,展示观察、判断与行动三个步骤。

Prometheus 直方图保留分桶后的分布信息

延迟平均值可能隐藏一部分特别慢的请求。直方图通过记录落入不同桶的观测数量,保留更多分布信息,但仍然丢弃桶内部的细节。Prometheus 可以从这些计数估计分位数,无需保存每次请求的原始耗时。估计结果取决于桶边界与聚合方式,不能称为完整恢复了原始延迟分布,也不能因为输出包含小数就认为测量具有同样精度。 经典直方图由多条相关序列组成 经典直方图导出累计的 _bucket 计数器,以及 _count 与 _sum。le 标签表示每个桶包含的上界。一点五秒的观测会增加两秒桶和所有更大的桶,所以这些桶不是互不重叠的区间,不能直接相加得到总请求数。正无穷桶包含全部观测,与总计数对应。一个经典直方图因此不是一条可以单独理解的普通序列。 对于非负延迟观测,分别对总和与总计数应用 rate,可以得到每秒累计的耗时和每秒观测数量;两者相除就是以秒为单位的平均延迟。桶速率则描述选定时间范围内的分布。应该先对各条计数器求速率,再合并实例,这样各序列已经观测到的重置能够独立处理,不会在求和时被另一实例的增长掩盖。 先合并桶,再估计总体分位数 需要整个服务的一个分位数时,应在保留 le 的前提下合并桶速率,再调用 histogram_quantile。查询需要区分服务等维度时,也应保留相应标签。参与合并的桶边界要兼容,事件定义也要一致。如果先把 le 消掉,就丢失了经典分位数函数所需的桶身份,留下的数值无法继续表达累计分布。 不要平均各实例的分位数。分位数是分布中的位置,不是可相加的计数,即使各实例请求量完全相同,平均分位数也未必等于总体分位数。合并桶曲线可以具有与任何单个实例都不同的形状。如果问题是两秒以内完成的请求比例,恰好以两秒为边界的桶除以总计数,就可以直接回答这个阈值问题,不必绕到分位数再反推。 摘要指标可以导出客户端预先计算的分位数,这些数值也不能通过求和或平均合成总体分位数。不过,只要事件定义一致,它的总计数与总和仍可用于计算合并平均值。选择指标类型时,应从所需聚合能力出发,而不是仅看面板希望直接显示什么字段。 坏掉的样子 关注的分位数落在很宽的桶内时,输出容易显得比实际测量更精确。经典分位数计算会在选中的桶内插值,却不知道每个观测在桶内的真实位置。应围绕服务目标设置有用的边界,并在升级前审阅桶定义,避免滚动发布期间不同实例混用不兼容方案。事后加密桶边界也不会恢复旧观测已经丢失的细节。 桶或导出器缺失,与没有观测不同。面板上看似改善的分位数,也可能只是最慢实例的指标丢了。解释改善之前,应检查抓取完整性、参与计算的序列和样本数量。观测太稀疏或完全缺失时,分位数可能没有可用含义;把这种结果替换成零延迟,会制造虚假的成功信号,而不是补全测量。 快速验证 把程序保存为 histogram-query.py,执行 python3 histogram-query.py。实验需要 Docker 与 Prometheus 3.5.1 镜像,首次使用可能下载镜像。真实 promtool 对每分钟一份的合成计数器执行经典直方图查询。两个实例各自每分钟贡献一百份观测,但桶形状和总耗时明显不同,可以直接检验平均分位数为何不成立。 import subprocess import uuid # Requires Docker and the public Prometheus 3.5.1 image. tests = r"""evaluation_interval: 1m tests: - interval: 1m input_series: - series: "api_duration_seconds_bucket{instance=\"fast\",le=\"1\"}" values: "0+95x5" - series: "api_duration_seconds_bucket{instance=\"fast\",le=\"2\"}" values: "0+100x5" - series: "api_duration_seconds_bucket{instance=\"fast\",le=\"5\"}" values: "0+100x5" - series: "api_duration_seconds_bucket{instance=\"fast\",le=\"+Inf\"}" values: "0+100x5" - series: "api_duration_seconds_count{instance=\"fast\"}" values: "0+100x5" - series: "api_duration_seconds_sum{instance=\"fast\"}" values: "0+17x5" - series: "api_duration_seconds_bucket{instance=\"slow\",le=\"1\"}" values: "0+0x5" - series: "api_duration_seconds_bucket{instance=\"slow\",le=\"2\"}" values: "0+0x5" - series: "api_duration_seconds_bucket{instance=\"slow\",le=\"5\"}" values: "0+100x5" - series: "api_duration_seconds_bucket{instance=\"slow\",le=\"+Inf\"}" values: "0+100x5" - series: "api_duration_seconds_count{instance=\"slow\"}" values: "0+100x5" - series: "api_duration_seconds_sum{instance=\"slow\"}" values: "0+400x5" promql_expr_test: - expr: "abs(histogram_quantile(0.99, sum by (le) (rate(api_duration_seconds_bucket[2m]))) - 4.94) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{}" value: 1 - expr: "abs(avg(histogram_quantile(0.99, rate(api_duration_seconds_bucket[2m]))) - 3.385) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{}" value: 1 - expr: "sum(rate(api_duration_seconds_bucket{le=\"2\"}[2m])) / sum(rate(api_duration_seconds_count[2m]))" eval_time: 5m exp_samples: - labels: "{}" value: 0.5 - expr: "abs(sum(rate(api_duration_seconds_sum[2m])) / sum(rate(api_duration_seconds_count[2m])) - 2.085) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{}" value: 1 """ rules = r"""""" script = "set -eu\ncat > /tmp/tests.yml <<'TEST_DATA'\n" + tests + "\nTEST_DATA\n" if rules: script += "cat > /tmp/rules.yml <<'RULE_DATA'\n" + rules + "\nRULE_DATA\n" script += "exec /bin/promtool test rules /tmp/tests.yml\n" name = "xbcatm-prom-unit-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-s"], input=script, text=True, capture_output=True, timeout=45, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 成功时输出 SUCCESS。正确合并后的百分之九十九分位数估计是四点九四秒;分别计算两个实例后再平均,得到三点三八五秒,并不是总体结果。满足两秒阈值的观测占一半,合并平均延迟为二点零八五秒。断言对浮点结果采用很小的数值容差,避免要求十进制小数必须具有完全相同的二进制表示。 ...

一月 6, 2027 · 2 分钟 · XB Field Notes
毫秒先转换成秒,再以含明确单位的指标名导出。

把单位写进指标名里

名为 request_duration 的指标会让读者猜测,二百五十究竟表示秒、毫秒还是别的量。面向 Prometheus 的名称,例如 http_request_duration_seconds,能够把单位约定带入查询、记录规则与告警配置。但这个约定必须由正确实现支撑:给毫秒数据的名字加上秒后缀,不会自动改变数值大小,反而可能让监控产生一千倍的误差。名称提供线索,转换逻辑才决定数据是否可信。 在明确的边界完成换算 每个指标族应只表示一种量和一种单位。Prometheus 建议使用秒、字节等基础单位,并在适用时把单位写为后缀。持续时间与绝对时间戳都可以用秒表示,却不是同一种量,因此不能因为名字都含秒,就把上次成功的时间戳与一次请求的耗时直接相加。帮助文本应写明测量对象、开始结束边界以及含义,让简短名称背后仍有明确的契约。 如果底层库输出毫秒,应在观测或导出边界转换一次,并保留足够精度,避免把很短的耗时截断为零。仪表盘可以按基础单位配置显示,让零点二五秒自动显示为二百五十毫秒,这只改变呈现方式,不改变存储样本。不能仅因为坐标轴显示毫秒,就在记录规则中再做一次换算,否则查询和图表会在不同环节重复缩放。 单位与类型回答不同问题 累计传输字节数通常使用字节加总量后缀,当前内存占用仪表则只使用字节后缀。两者单位相同,但只有前者表示不断累积的工作量,适合计算计数器速率。请求次数按惯例使用总量后缀,无须编造一个物理单位。直方图的桶、总和与计数,还会在耗时指标族名称后加入各自的组件后缀,因此阅读名称时也要识别指标族与导出组件之间的关系。 比例通常表示零到一之间的分数,而百分数使用另一种数值尺度,面板应明确自己期待哪种输入。计数器可以在进程重启时归零,Prometheus 的计数器速率函数会根据观测序列中的下降处理重置,并不要求额外存在创建时间序列。应先分别计算独立计数器的速率,再执行聚合,让各自的重置仍能被识别;不要先把多个实例的累计值混成一条看似连续的曲线。 坏掉的样子 名字正确的指标仍然可能承载错误数值。如果同一个标识下混入秒和毫秒,历史数据已经被污染,修改帮助文字或补一个说明标签不能修复旧样本。单位迁移应被视为契约变化,生产端、查询、告警和仪表盘需要一起核对,历史区间的比较也必须说明边界。旧序列含义不兼容时,可以采用新的指标标识,避免新旧数值被误当成同一种量连续使用。 名称后缀也不会强制执行计数器语义。一个普通运行过程中可以升降的仪表,即使名字带总量后缀,也不适合按累计计数器解释。反过来,已经得到每秒速率后再除以时间窗口,会制造另一种量。这些是维度和测量语义的问题,不能只靠显示格式解决。其他遥测体系可能把单位放在元数据中,应遵循已定义的导出映射,而不是机械重命名所有上游仪器。 快速验证 把代码保存为 units.py,使用 Python 3 执行。程序转换三条人为构造的延迟观测,并输出带帮助文字和类型声明的 Prometheus 文本格式。十进制定点运算使毫秒到秒的换算清楚可见;阈值比较则展示,仅仅改名、却不转换数据的生产端,为什么不能和正确的生产端共用同一条延迟规则。 from decimal import Decimal latency_ms = [Decimal('120'), Decimal('250'), Decimal('630')] latency_seconds = [value / Decimal('1000') for value in latency_ms] threshold_seconds = Decimal('0.300') correct_good = sum(value <= threshold_seconds for value in latency_seconds) rename_only_good = sum(value <= threshold_seconds for value in latency_ms) transferred_bytes = 1024 * 1024 used_ratio = Decimal('25') / Decimal('100') assert correct_good == 2 and rename_only_good == 0 assert sum(latency_seconds) == Decimal('1.000') assert used_ratio == Decimal('0.25') print(f"# within 0.300s: converted={correct_good}/3, rename_only={rename_only_good}/3") print('# HELP demo_last_request_duration_seconds Duration of the most recent completed request.') print('# TYPE demo_last_request_duration_seconds gauge') print(f'demo_last_request_duration_seconds {latency_seconds[-1]:.3f}') print('# HELP demo_transferred_bytes_total Bytes transferred since this process started.') print('# TYPE demo_transferred_bytes_total counter') print(f'demo_transferred_bytes_total {transferred_bytes}') print('# HELP demo_capacity_used_ratio Fraction of configured capacity currently used.') print('# TYPE demo_capacity_used_ratio gauge') print(f'demo_capacity_used_ratio {used_ratio}') 首行注释说明,转换后有三分之二的观测不超过零点三秒,而把毫秒数值直接当成秒时,三条都无法满足阈值。导出的最后请求耗时为零点六三秒,累计字节数为一百零四万八千五百七十六,容量使用比例为零点二五,在百分比界面上对应百分之二十五。三个数分别对应持续时间、累计大小与无量纲比例,不能因为都能画成曲线就混用。 ...

十月 30, 2026 · 1 分钟 · XB Field Notes
同一组延迟观测采用粗细不同的桶,得到不同的分位数估计。

直方图保留延迟分布的形状

平均值可能让一个令人不舒服的服务看起来很快。如果九十五次请求各耗时五十毫秒,另外五次耗时两秒,平均延迟也只有一百四十七点五毫秒。直方图能够在不保存每条原始请求的情况下,保留更多关于这部分慢请求的信息。但它保存的是分桶后的分布近似,不是全部原始观测,也不会天然提供精确的分位数。理解这个区别,才能避免把计算输出当成重新测得的真实延迟。 先看桶里究竟统计了什么 在 Prometheus 经典直方图中,桶的边界标签表示小于或等于该上界的全部观测。计数具有累积性:一条五十毫秒的请求,既进入一百毫秒桶,也进入所有更大的桶。正无穷桶覆盖全部观测,计数和总和组件则分别记录样本数量与累计耗时。因此,直接把所有累积桶相加会重复统计请求,不能把结果当成真实访问量。 相邻累积桶相减,可以得到互不重叠区间内的观测数量,但仍然不知道每条请求在该区间中的具体位置。边界兼容的直方图可以先把对应桶的计数相加,再估计总体分位数。直接平均多个实例已经算出的分位数,无法恢复合并后的分布,即使各实例自己的分位数都计算得很精确,也不存在这种简单的总体换算关系。 分辨率决定答案能有多精细 对于常见的正数经典桶,分位数函数先定位目标排名所在的桶,再假定该区间内的观测均匀分布,执行线性插值。真实延迟却可能集中在桶的某一侧,所以返回值是与桶宽相关的估计,并不是被重新找回的实际观测。应该围绕业务真正关心的运行区间设置边界,而不是把大部分精度浪费在与目标无关的范围内。 如果桶边界正好位于三百毫秒目标上,就可以直接统计达到该延迟要求的请求数。这通常比观察一个插值得出的高分位数是否恰好低于阈值,更贴近事件型目标的定义。不过,经典直方图每增加一条边界,都会为每组标签组合增加桶序列。现代 Prometheus 也支持原生直方图,当前文档建议在埋点及相关系统支持时优先考虑;标准指数型原生桶采用不同的插值方式,下面的实验特意只讨论经典桶。 坏掉的样子 上层桶过宽时,少量观测移动就可能让尾部估计出现看起来很大的跳跃。经典分位数计算还要求存在正无穷桶,遗漏它可能使结果无效。如果目标排名落在最后的无限桶中,Prometheus 会返回前一个有限桶的上界,而不是编造一个无限延迟。上述输出都不能告诉你最慢那条请求的准确耗时,追查单次异常仍需要更细的观测材料。 空样本总体没有可解释的观测分位数。跨实例桶边界不匹配、同一指标混入不同单位,或者累积计数出现不合法的下降,也会破坏结果含义。按时间查询累计计数器时,应先计算合适的增量或速率,再聚合实例,使重置得到正确处理。另外,全请求分布也要说明结果范围:快速失败可能降低表面延迟,却同时让服务变得更不可用,不能仅凭曲线变快就宣布改善。 快速验证 保存为 histogram-shape.py,使用 Python 3 执行。程序先从人工构造的原始样本计算精确的最近秩高分位数,再分别根据两套经典桶估计相同分位点。精简插值函数只适用于这里的正数、非空样本,并断言选中的桶有有限边界;它用于展示桶内假设,不替代 Prometheus 对所有特殊输入和边界情况的完整实现。 from math import ceil, inf from statistics import mean values = [0.05] * 95 + [2.0] * 5 coarse = [0.1, 0.3, 1.0, 5.0, inf] fine = [0.1, 0.3, 1.0, 2.0, 5.0, inf] def cumulative(bounds): return [sum(value <= bound for value in values) for bound in bounds] def estimate_p99(bounds): counts = cumulative(bounds) rank = 0.99 * counts[-1] lower, previous = 0.0, 0 for upper, count in zip(bounds, counts): if count >= rank: assert upper != inf and count > previous return lower + (upper - lower) * (rank - previous) / (count - previous) lower, previous = upper, count raise AssertionError('rank not covered') exact = sorted(values)[ceil(0.99 * len(values)) - 1] counts = cumulative(coarse) increments = [counts[0]] + [right - left for left, right in zip(counts, counts[1:])] assert counts == [95, 95, 95, 100, 100] assert increments == [95, 0, 0, 5, 0] and sum(increments) == 100 assert exact == 2.0 and abs(estimate_p99(coarse) - 4.2) < 1e-12 assert abs(estimate_p99(fine) - 1.8) < 1e-12 print(f"mean={mean(values):.4f}s; raw nearest-rank p99={exact:.1f}s") print(f"coarse cumulative counts={counts}; sum_of_buckets={sum(counts)}") print(f"classic linear p99: coarse={estimate_p99(coarse):.1f}s; fine={estimate_p99(fine):.1f}s") print(f"at or below 0.3s={counts[1]}/{counts[-1]}") 原始样本的百分之九十九分位数为两秒,粗桶却给出四点二秒,因为有关观测只被定位在一秒到五秒之间。增加两秒边界后,估计变成一点八秒。在这组数据中,较窄的桶确实减少了误差,但仍然没有等于真实样本分位数,说明提高分辨率不会凭空恢复原始数值。两项估计还与 Prometheus 3.5.1 的实际分位数函数做了核对,在万亿分之一的数值容差内一致。排障时应先查看所在区间和边界,再解释分位数的小幅变化。 ...

九月 20, 2026 · 2 分钟 · XB Field Notes