直方图聚合与分位数主题封面,展示观察、判断与行动三个步骤。

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

延迟平均值可能隐藏一部分特别慢的请求。直方图通过记录落入不同桶的观测数量,保留更多分布信息,但仍然丢弃桶内部的细节。Prometheus 可以从这些计数估计分位数,无需保存每次请求的原始耗时。估计结果取决于桶边界与聚合方式,不能称为完整恢复了原始延迟分布,也不能因为输出包含小数就认为测量具有同样精度。 经典直方图由多条相关序列组成 经典直方图导出累计的 _bucket 计数器,以及 _count 与 _sum。le 标签表示每个桶包含的上界。一点五秒的观测会增加两秒桶和所有更大的桶,所以这些桶不是互不重叠的区间,不能直接相加得到总请求数。正无穷桶包含全部观测,与总计数对应。一个经典直方图因此不是一条可以单独理解的普通序列。 对于非负延迟观测,分别对总和与总计数应用 rate,可以得到每秒累计的耗时和每秒观测数量;两者相除就是以秒为单位的平均延迟。桶速率则描述选定时间范围内的分布。应该先对各条计数器求速率,再合并实例,这样各序列已经观测到的重置能够独立处理,不会在求和时被另一实例的增长掩盖。 先合并桶,再估计总体分位数 需要整个服务的一个分位数时,应在保留 le 的前提下合并桶速率,再调用 histogram_quantile。查询需要区分服务等维度时,也应保留相应标签。参与合并的桶边界要兼容,事件定义也要一致。如果先把 le 消掉,就丢失了经典分位数函数所需的桶身份,留下的数值无法继续表达累计分布。 不要平均各实例的分位数。分位数是分布中的位置,不是可相加的计数,即使各实例请求量完全相同,平均分位数也未必等于总体分位数。合并桶曲线可以具有与任何单个实例都不同的形状。如果问题是两秒以内完成的请求比例,恰好以两秒为边界的桶除以总计数,就可以直接回答这个阈值问题,不必绕到分位数再反推。 摘要指标可以导出客户端预先计算的分位数,这些数值也不能通过求和或平均合成总体分位数。不过,只要事件定义一致,它的总计数与总和仍可用于计算合并平均值。选择指标类型时,应从所需聚合能力出发,而不是仅看面板希望直接显示什么字段。 坏掉的样子 关注的分位数落在很宽的桶内时,输出容易显得比实际测量更精确。经典分位数计算会在选中的桶内插值,却不知道每个观测在桶内的真实位置。应围绕服务目标设置有用的边界,并在升级前审阅桶定义,避免滚动发布期间不同实例混用不兼容方案。事后加密桶边界也不会恢复旧观测已经丢失的细节。 桶或导出器缺失,与没有观测不同。面板上看似改善的分位数,也可能只是最慢实例的指标丢了。解释改善之前,应检查抓取完整性、参与计算的序列和样本数量。观测太稀疏或完全缺失时,分位数可能没有可用含义;把这种结果替换成零延迟,会制造虚假的成功信号,而不是补全测量。 快速验证 保存为 histogram-query.py,运行 python3 histogram-query.py。环境需要已经安装的 promtool,可以从 PATH 找到,或用 PROMTOOL 指定其可执行文件路径。本轮使用 Python 3.12.14 与 promtool 3.14.0。真实查询引擎在临时目录中评估合成样本,不启动服务器、监听端口或容器,也不联系外部服务。 from pathlib import Path from tempfile import TemporaryDirectory import os import shutil import subprocess # Uses a real, installed promtool; no server or Docker runtime is started. promtool = shutil.which(os.environ.get("PROMTOOL", "promtool")) if not promtool: raise SystemExit("Set PROMTOOL to an installed promtool executable") 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" - series: 'tail_duration_seconds_bucket{le="1"}' values: "0+0x5" - series: 'tail_duration_seconds_bucket{le="5"}' values: "0+90x5" - series: 'tail_duration_seconds_bucket{le="+Inf"}' values: "0+100x5" 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 - expr: 'histogram_quantile(0.99, rate(tail_duration_seconds_bucket[2m]))' eval_time: 5m exp_samples: - labels: '{}' value: 5 """ rules = r"""""" with TemporaryDirectory(prefix="prometheus-query-check-") as directory: root = Path(directory) (root / "tests.yml").write_text(tests, encoding="utf-8") if rules: (root / "rules.yml").write_text(rules, encoding="utf-8") result = subprocess.run([promtool, "test", "rules", "tests.yml"], cwd=root, text=True, capture_output=True, timeout=45) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 成功时输出 SUCCESS。正确合并后的百分之九十九分位数估计是四点九四秒;分别计算两个实例后再平均,得到三点三八五秒,并不是总体结果。满足两秒阈值的观测占一半,合并平均延迟为二点零八五秒。断言对浮点结果采用很小的数值容差,避免要求十进制小数必须具有完全相同的二进制表示。 ...

一月 6, 2027 · 2 分钟 · XB Field Notes
浅色网格上显示文章标题,旁侧是依次标注 Observe、Decide、Act 的三个相连方框。

健康的黄金信号

延迟、流量、错误与饱和度,是判断服务能否满足用户需求的四个互补角度。它们提供的是一组起始问题,并不是四个放到任何系统里都能自动证明健康的通用计数器。是否有用,取决于在哪里测量、描述哪些工作、采用什么时间窗口,以及各项统计如何对应。响应很快也可能意味着业务操作失败,因此不能看到延迟下降就直接宣布用户体验改善。 区分用户体验与工作需求 延迟描述一项工作在指定边界内花了多长时间。成功操作与失败操作应该分开观察,避免立即返回的错误让总体分布显得更健康。尾部信息很有价值,但必须同时保留观测数量;很少的样本即使算出了高分位数,也不能证明已经稳定掌握稀有行为。客户端延迟与服务器内部处理时间覆盖的链路不同,比较它们之前应先说明各自包含和遗漏的阶段。 流量描述需求或已经完成的工作,应采用请求每秒、字节每秒等合适单位,并明确统计的是到达、完成还是重试。队列卡住时,完成速率可以下降,但外部请求需求仍然持续增加。并发请求数则是数量,不是请求速率;它可以作为补充信息,却不能直接替换流量面板中的速率。批处理系统也应选择适合自身任务的工作单位,而不是机械套用接口请求数。 说明失败条件与资源压力 错误描述未达到承诺结果的工作。状态码失败是一种信号,但正常响应中包含错误业务数据,也可能违反服务契约。应明确哪些结果、哪些延迟违规进入指标,并使用相同统计范围的分母。除了比例,还应展示实际数量,使安静时段的一次错误与高峰时段影响大量用户的事故不会因为百分比相似而被混淆。 饱和度描述受约束资源距离可用上限有多近。处理器、内存、连接池、工作线程占用和队列等待时间,可以揭示不同的瓶颈。线程池全部忙碌不一定意味着不健康,但如果队列持续增长、最老任务不断等待,就能看出占用率为何值得关注。某一时刻的快照并不是整段时间的平均值,而很低的平均值也可能藏住短暂过载。容量阈值需要结合工作负载与实测,不能只在图上画一条没有依据的红线。 坏掉的样子 混用统计总体会制造难以解释的故事,例如延迟来自应用成功请求,错误来自入口代理,流量却来自另一个区域。它们可以独立变化,所以关联之前必须写明边界与过滤条件。快速拒绝请求可以改善全请求平均延迟,同时使错误率上升;只统计已经完成的请求,还会遗漏尚未结束的等待,让用户正在经历的停顿从延迟和吞吐图中同时消失。 四个信号中的任何一个都不能独立证明根因。处理器空闲时,数据库连接池可能已经耗尽;队列增长既可能源于更多需求,也可能因为下游变慢。应利用信号选择下一步排查方向,再查看链路、日志、依赖和变更证据。告警应针对可采取行动的用户影响或即将耗尽的资源,不能把任何单次仪表波动都等同于需要立刻叫醒值班人员的事故。 快速验证 保存为 golden.py 后使用 Python 3 执行。实验包含一个十秒窗口内的人造已完成请求,以及另行标注的窗口结束时线程与队列快照,用于核对聚合与解读。它没有向真实服务施压,也没有测量当前服务器;分位数直接来自原始样本的最近秩计算,与利用直方图桶进行插值估计的方式不同。 from math import ceil from statistics import mean # Synthetic completed requests in one 10-second observation interval. records = [(True, 0.05)] * 90 + [(True, 0.8)] * 5 + [(False, 0.005)] * 5 success = sorted(duration for ok, duration in records if ok) failure = [duration for ok, duration in records if not ok] p99_success = success[ceil(0.99 * len(success)) - 1] traffic_rps = len(records) / 10 error_ratio = len(failure) / len(records) # These are end-of-interval gauges, not interval averages. busy_workers, total_workers = 4, 4 queued_requests, oldest_queue_seconds = 12, 1.2 assert p99_success == 0.8 and traffic_rps == 10 and error_ratio == 0.05 assert mean(failure) < mean(success) assert busy_workers / total_workers == 1 print(f"traffic={traffic_rps:.1f} completed requests/s; errors={error_ratio:.1%}") print(f"success p99={p99_success:.3f}s; failed mean={mean(failure):.3f}s") print(f"success mean={mean(success):.6f}s; all-request mean={mean(d for _, d in records):.6f}s") print(f"worker snapshot={busy_workers}/{total_workers}; queue={queued_requests}; oldest={oldest_queue_seconds}s") 输出显示,每秒完成十次请求,错误比例为百分之五,成功请求的百分之九十九分位数为零点八秒,失败请求平均只耗时零点零零五秒。成功请求平均约零点零八九四七四秒,而加入快速失败后,全请求平均值降低到零点零八五二五秒。这个可复现的差异说明,混合平均值下降不等于业务变得更有用,必须同时查看结果是否成功。 ...

十一月 29, 2026 · 1 分钟 · XB Field Notes
标题卡旁有三个方框,分别标注 Observe、Decide 和 Act。

指标与日志都需要明确统计总体

指标与日志是对已观测行为的不同表示,并不是互相竞争的真相定义。指标通常在一组维度下汇总测量结果,日志记录则描述一个事件,并能够携带更具体的上下文。只要数据覆盖与语义支持,两者都可以回答聚合问题。不能因为某张仪表盘更整齐,就认为它天然更权威;也不能因为日志字段很多,就默认其中包含完整且可直接比较的业务总体。 对于结账服务,一个已完成请求计数器可以把每个最终结果归为成功或错误。路由、结果等少量有界维度,方便持续比较流量与失败比例。诊断日志则可以利用关联编号、错误类型和依赖响应,解释某一次失败请求的具体原因。这两类信息互相帮助,但它们采用的分母仍然需要分别确定。共享服务名称或时间图形,并不足以证明两份数据统计了相同的请求集合。 先决定测量包含什么 累计计数器本身不是每秒请求数。实际速率查询需要跨时间观测,并考虑进程重启等导致的计数重置。不过,一个起点、终点和成员都明确的完整合成批次,可以直接计数。不能悄悄把生产中的时间窗口速率换成两个无关快照的比例,也不能把经过选择的事件流与覆盖全部请求的计数器混为同一个统计总体。计算形式相似,并不代表时间口径已经一致。 日志也不是只能靠人逐条阅读。只要终态事件结构稳定且记录完整,就能聚合出与请求计数器相同的总量。选择时需要考虑事件体积、索引、查询延迟、保留期限和维护复杂度。指标经常适合反复执行的汇总查询,但如果每个请求都生成一个新的标签值,指标存储也可能迅速变得昂贵。究竟哪种方式成本较低,取决于实际维度和使用方式,而不是单凭指标或日志这个名称判断。 坏掉的样子 假设一百个已完成请求里有四个失败。为了排查问题,日志系统或人工查询特意保留其中五个请求,里面包含两个失败。完整总体的失败比例是百分之四,被选日志的失败比例则是百分之四十。两次算术运算都没有错,但把后者宣称为整个服务的失败率就错了,因为这五条记录既不是全部请求,也没有证明是能够代表总体的样本。排障数据的选择目的必须与统计推断分开。 把重试过程中的失败次数当成最终失败请求数,也会造成类似误差:同一个请求的一次尝试可能失败,随后却成功完成。日志缺失、重复投递、时间窗口不同和路由归类不一致,都可能令看起来相关的图表互相矛盾。用于可靠性计算时,应明确每个请求只有一个最终结果,同时保留尝试级证据用于诊断。发现不一致后,不能只改图表标题,或者简单平均两个比例来掩盖口径差异。 快速验证 将代码保存为 metrics-logs.py,使用 Python 3,并把官方 promtool 可执行程序放入 PATH。本次完整运行采用 Python 3.12.14 与 promtool 3.14.0。真实校验器从标准输入读取生成的指标文本,不建立服务器、容器或网络连接。合成请求集合按构造保证完整,诊断日志子集则是刻意选取,并非随机抽样。程序同时输出实际工具版本,避免把历史容器版本误写成这次使用的版本。 import json import subprocess from collections import Counter # A complete synthetic population, with exactly one terminal outcome per request. requests = [{"request_id": f"r{i}", "route": "/checkout", "outcome": "error" if i in {7, 18, 53, 94} else "success"} for i in range(1, 101)] counter = Counter(request["outcome"] for request in requests) selected_logs = [request for i, request in enumerate(requests, 1) if i in {1, 7, 18, 33, 55}] logged_errors = sum(record["outcome"] == "error" for record in selected_logs) text = "# HELP demo_requests_total Completed synthetic requests.\n# TYPE demo_requests_total counter\n" for outcome, count in sorted(counter.items()): text += f'demo_requests_total{{route="/checkout",outcome="{outcome}"}} {count}\n' version = subprocess.run(["promtool", "--version"], capture_output=True, text=True, check=True).stdout.splitlines()[0] result = subprocess.run(["promtool", "check", "metrics"], input=text, text=True, capture_output=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr population_rate = counter["error"] / len(requests) selected_rate = logged_errors / len(selected_logs) assert population_rate == 0.04 and selected_rate == 0.4 assert "request_id" not in text and len(counter) == 2 # Full event coverage would recover the same aggregate; these five logs cannot. assert sum(r["outcome"] == "error" for r in requests) == counter["error"] print(json.dumps({"requests": len(requests), "errors": counter["error"], "population_error_rate": population_rate, "exposed_metric_series": len(counter), "selected_logs": len(selected_logs), "selected_log_errors": logged_errors, "selected_log_error_rate": selected_rate, "exposition_validated_by": version}, indent=2)) 输出应包含一百个请求、四个错误及零点零四的总体比例。筛选后的五条日志中有两个错误,得到零点四。导出文本只有两条按结果区分的计数序列,标签中不包含请求编号。Promtool 负责验证格式,Python 断言负责验证算术结果和事先定义的成员关系。这不是实际抓取流程、计数器重置处理或存储费用基准测试,因此不能把格式检查通过扩大为整套监控链路已经验证。 ...

十月 16, 2026 · 2 分钟 · XB Field Notes
观察、决策、行动三个方框,旁边强调说清症状并链接下一步检查。

告警告诉你该读什么

告警应让读者知道第一步该查什么,而不只是展示一个红色标记。它需要说明观察到的条件、受影响范围、持续时间、负责团队,以及接下来要看的证据。这些内容属于规则的可观察行为,应当和触发条件一起测试,不能等事故发生以后才把通知文案当作装饰补上。 把观察与解释分开 一次 scrape 失败,说明 Prometheus 没有成功收集目标的指标,并不能独立证明公共 API 已经不可用。服务可能仍能处理用户请求,但指标处理器失败;也可能指标采集正常,业务操作却一直报错。应该组合面向用户症状的探测与内部证据,并在告警里准确说明本次到底是哪一种观察触发,避免把采集失败直接写成业务停机。 最近发布是有价值的排查线索,却不是因果证明。证书到期、依赖故障、资源短缺或网络异常,都可能发生在没有部署的时刻。把相关变更记录和状态、日志、用户侧指标放在一起比较;在没有建立关联之前,不要把最近一次提交自动标记为根因。好的上下文帮助缩小范围,而不是提前替调查下结论。 保持身份稳定,让上下文可用 Prometheus 根据表达式返回的每组标签识别告警实例。服务、团队等稳定路由属性可以放进 labels;当前数值、解释文字和 runbook 链接放进 annotations。如果把不断变化的时间戳或测量值塞进标签,告警身份也会不断改变,pending 状态可能始终等不到持续时间结束。注解可以随情况变化提供解释,而不重新定义这是否还是同一条告警。 runbook 开头应给出可访问、只读的检查:先看目标的具体采集错误,再比较指标端点和用户请求路径,最后关联相关日志与变更记录。同时写清正常时应看到什么、看不到时进入哪个分支、什么时候升级求助。通知链接不要带凭据或敏感查询参数,还要实际确认值班读者有权访问目标页面。链接存在但读者无法打开,同样不能算有效的排查入口。 坏掉的样子 规则可能正确触发,却没有路由到任何接收人;也可能链接的文档已被删除,或者聚合时丢失了本应定位实例的标签。噪声阈值则会为不需要决策的短暂现象反复打断人。只有确实需要及时人工判断时才使用 page,其他情况应进入合适的看板、工单或自动处理。Alertmanager 的分组和抑制,与 Prometheus 判断规则是否 firing,是两段需要分别验证的流程。 可选的 for 要求表达式在连续评估中持续保持 active,达到指定时长后才进入 firing。它不是滑动平均窗口,也没有包含通知发送的等待时间。如果表达式不再产生该标签集合,本例规则就会停止活跃。尤其要注意,stale 或完全缺失的序列不是显式的零,因此告警安静不等于业务恢复;采集链路和预期目标清单需要独立监控,不能把“看不见”解释为健康。 快速验证 使用 Prometheus 发行包中的 promtool;下面实验已用 3.14.0 执行。在一个空的本地目录里,把接下来的两个代码块分别保存成 alerts.yml 和 alerts.test.yml。规则故意只描述指标采集可用性,不代表 API 可用性 SLO。severity 和 team 是测试标签,执行测试不会发送通知或改动任何现有告警配置。 ...

九月 22, 2026 · 2 分钟 · XB Field Notes
浅色网格上显示文章标题,旁侧是依次标注 Observe、Decide、Act 的三个相连方框。

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

平均值可能让一个令人不舒服的服务看起来很快。如果九十五次请求各耗时五十毫秒,另外五次耗时两秒,平均延迟也只有一百四十七点五毫秒。直方图能够在不保存每条原始请求的情况下,保留更多关于这部分慢请求的信息。但它保存的是分桶后的分布近似,不是全部原始观测,也不会天然提供精确的分位数。理解这个区别,才能避免把计算输出当成重新测得的真实延迟。 先看桶里究竟统计了什么 在 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