两个实例的累计桶阶梯先合并为总体阶梯,再估计 p99。合并时保留匹配的 le 桶身份。

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

延迟平均值可能隐藏一部分特别慢的请求。直方图通过记录落入不同桶的观测数量,保留更多分布信息,但仍然丢弃桶内部的细节。Prometheus 可以从这些计数估计分位数,无需保存每次请求的原始耗时。估计结果取决于桶边界与聚合方式,不能称为完整恢复了原始延迟分布,也不能因为输出包含小数就认为测量具有同样精度。 经典直方图先对每条计数器求速率,再合并兼容桶、保留 le,最后估计分位数。原文合成样例的总体 p99 为 4.94 秒,两秒阈值占比为 50%,平均值为 2.085 秒。正无穷桶返回值不是测得的最大值,也不是阈值保证。本轮绘图没有运行 promtool 或服务器。 经典直方图由多条相关序列组成 经典直方图导出累计的 _bucket 计数器,以及 _count 与 _sum。le 标签表示每个桶包含的上界。一点五秒的观测会增加两秒桶和所有更大的桶,所以这些桶不是互不重叠的区间,不能直接相加得到总请求数。正无穷桶包含全部观测,与总计数对应。一个经典直方图因此不是一条可以单独理解的普通序列。 对于非负延迟观测,分别对总和与总计数应用 rate,可以得到每秒累计的耗时和每秒观测数量;两者相除就是以秒为单位的平均延迟。桶速率则描述选定时间范围内的分布。应该先对各条计数器求速率,再合并实例,这样各序列已经观测到的重置能够独立处理,不会在求和时被另一实例的增长掩盖。 先合并桶,再估计总体分位数 需要整个服务的一个分位数时,应在保留 le 的前提下合并桶速率,再调用 histogram_quantile。查询需要区分服务等维度时,也应保留相应标签。参与合并的桶边界要兼容,事件定义也要一致。如果先把 le 消掉,就丢失了经典分位数函数所需的桶身份,留下的数值无法继续表达累计分布。 ...

一月 6, 2027 · 2 分钟 · XB Field Notes
成功请求均值为 89.47 毫秒;五次快速失败的均值为 5 毫秒。比例条显示,计入失败会降低总体均值,却不代表服务更有用。

健康的黄金信号

延迟、流量、错误与饱和度,是判断服务能否满足用户需求的四个互补角度。它们提供的是一组起始问题,并不是四个放到任何系统里都能自动证明健康的通用计数器。是否有用,取决于在哪里测量、描述哪些工作、采用什么时间窗口,以及各项统计如何对应。响应很快也可能意味着业务操作失败,因此不能看到延迟下降就直接宣布用户体验改善。 区分用户体验与工作需求 延迟描述一项工作在指定边界内花了多长时间。成功操作与失败操作应该分开观察,避免立即返回的错误让总体分布显得更健康。尾部信息很有价值,但必须同时保留观测数量;很少的样本即使算出了高分位数,也不能证明已经稳定掌握稀有行为。客户端延迟与服务器内部处理时间覆盖的链路不同,比较它们之前应先说明各自包含和遗漏的阶段。 流量描述需求或已经完成的工作,应采用请求每秒、字节每秒等合适单位,并明确统计的是到达、完成还是重试。队列卡住时,完成速率可以下降,但外部请求需求仍然持续增加。并发请求数则是数量,不是请求速率;它可以作为补充信息,却不能直接替换流量面板中的速率。批处理系统也应选择适合自身任务的工作单位,而不是机械套用接口请求数。 说明失败条件与资源压力 错误描述未达到承诺结果的工作。状态码失败是一种信号,但正常响应中包含错误业务数据,也可能违反服务契约。应明确哪些结果、哪些延迟违规进入指标,并使用相同统计范围的分母。除了比例,还应展示实际数量,使安静时段的一次错误与高峰时段影响大量用户的事故不会因为百分比相似而被混淆。 饱和度描述受约束资源距离可用上限有多近。处理器、内存、连接池、工作线程占用和队列等待时间,可以揭示不同的瓶颈。线程池全部忙碌不一定意味着不健康,但如果队列持续增长、最老任务不断等待,就能看出占用率为何值得关注。某一时刻的快照并不是整段时间的平均值,而很低的平均值也可能藏住短暂过载。容量阈值需要结合工作负载与实测,不能只在图上画一条没有依据的红线。 坏掉的样子 混用统计总体会制造难以解释的故事,例如延迟来自应用成功请求,错误来自入口代理,流量却来自另一个区域。它们可以独立变化,所以关联之前必须写明边界与过滤条件。快速拒绝请求可以改善全请求平均延迟,同时使错误率上升;只统计已经完成的请求,还会遗漏尚未结束的等待,让用户正在经历的停顿从延迟和吞吐图中同时消失。 四个信号中的任何一个都不能独立证明根因。处理器空闲时,数据库连接池可能已经耗尽;队列增长既可能源于更多需求,也可能因为下游变慢。应利用信号选择下一步排查方向,再查看链路、日志、依赖和变更证据。告警应针对可采取行动的用户影响或即将耗尽的资源,不能把任何单次仪表波动都等同于需要立刻叫醒值班人员的事故。 快速失败降低总体平均延迟,却仍是失败。比较时要对齐总体和单位;线程与队列仪表是单独的窗口结束快照。 快速验证 保存为 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
完整合成总体用十乘十方格表示一百个请求,其中四个失败标红。刻意选出的五条记录包含两个失败:四除以一百为百分之四,二除以五为百分之四十。被选记录的比例不是整个服务的失败比例。

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

指标与日志是对已观测行为的不同表示,并不是互相竞争的真相定义。指标通常在一组维度下汇总测量结果,日志记录则描述一个事件,并能够携带更具体的上下文。只要数据覆盖与语义支持,两者都可以回答聚合问题。不能因为某张仪表盘更整齐,就认为它天然更权威;也不能因为日志字段很多,就默认其中包含完整且可直接比较的业务总体。 对于结账服务,一个已完成请求计数器可以把每个最终结果归为成功或错误。路由、结果等少量有界维度,方便持续比较流量与失败比例。诊断日志则可以利用关联编号、错误类型和依赖响应,解释某一次失败请求的具体原因。这两类信息互相帮助,但它们采用的分母仍然需要分别确定。共享服务名称或时间图形,并不足以证明两份数据统计了相同的请求集合。 原文受控批次明确成员关系,并为每个请求定义一个最终结果。五条诊断记录是刻意筛选的,未证明可以代表总体。历史算术与格式校验结果保持不变,本图不增加运行验证或生产覆盖证据。 先决定测量包含什么 累计计数器本身不是每秒请求数。实际速率查询需要跨时间观测,并考虑进程重启等导致的计数重置。不过,一个起点、终点和成员都明确的完整合成批次,可以直接计数。不能悄悄把生产中的时间窗口速率换成两个无关快照的比例,也不能把经过选择的事件流与覆盖全部请求的计数器混为同一个统计总体。计算形式相似,并不代表时间口径已经一致。 日志也不是只能靠人逐条阅读。只要终态事件结构稳定且记录完整,就能聚合出与请求计数器相同的总量。选择时需要考虑事件体积、索引、查询延迟、保留期限和维护复杂度。指标经常适合反复执行的汇总查询,但如果每个请求都生成一个新的标签值,指标存储也可能迅速变得昂贵。究竟哪种方式成本较低,取决于实际维度和使用方式,而不是单凭指标或日志这个名称判断。 坏掉的样子 假设一百个已完成请求里有四个失败。为了排查问题,日志系统或人工查询特意保留其中五个请求,里面包含两个失败。完整总体的失败比例是百分之四,被选日志的失败比例则是百分之四十。两次算术运算都没有错,但把后者宣称为整个服务的失败率就错了,因为这五条记录既不是全部请求,也没有证明是能够代表总体的样本。排障数据的选择目的必须与统计推断分开。 把重试过程中的失败次数当成最终失败请求数,也会造成类似误差:同一个请求的一次尝试可能失败,随后却成功完成。日志缺失、重复投递、时间窗口不同和路由归类不一致,都可能令看起来相关的图表互相矛盾。用于可靠性计算时,应明确每个请求只有一个最终结果,同时保留尝试级证据用于诊断。发现不一致后,不能只改图表标题,或者简单平均两个比例来掩盖口径差异。 快速验证 将代码保存为 metrics-logs.py,使用 Python 3,并把官方 promtool 可执行程序放入 PATH。保留的 2026-10-15 完整运行记录采用 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
采集失败告警指向三个排查入口:目标错误、指标端点与公共 API 路径对比,以及日志和变更。这些是待读的证据,不代表 API 一定故障,也不代表发布导致了故障。

告警告诉你该读什么

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

九月 22, 2026 · 2 分钟 · XB Field Notes
一至五秒的宽区间与一至两秒的窄区间,对同一个两秒原始 p99 给出不同估计:4.2 秒和 1.8 秒。

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

平均值可能让一个令人不舒服的服务看起来很快。如果九十五次请求各耗时五十毫秒,另外五次耗时两秒,平均延迟也只有一百四十七点五毫秒。直方图能够在不保存每条原始请求的情况下,保留更多关于这部分慢请求的信息。但它保存的是分桶后的分布近似,不是全部原始观测,也不会天然提供精确的分位数。理解这个区别,才能避免把计算输出当成重新测得的真实延迟。 同一组原文观测会因桶边界不同,产生不同的经典直方图估计。本样例中较窄桶减小了误差,但没有还原原始分位数。与边界对齐的阈值计数对已记录观测可以精确。标准指数型原生桶使用不同的插值模型。本轮没有重跑 Python 样例或其历史 Prometheus 对比。 先看桶里究竟统计了什么 在 Prometheus 经典直方图中,桶的边界标签表示小于或等于该上界的全部观测。计数具有累积性:一条五十毫秒的请求,既进入一百毫秒桶,也进入所有更大的桶。正无穷桶覆盖全部观测,计数和总和组件则分别记录样本数量与累计耗时。因此,直接把所有累积桶相加会重复统计请求,不能把结果当成真实访问量。 相邻累积桶相减,可以得到互不重叠区间内的观测数量,但仍然不知道每条请求在该区间中的具体位置。边界兼容的直方图可以先把对应桶的计数相加,再估计总体分位数。直接平均多个实例已经算出的分位数,无法恢复合并后的分布,即使各实例自己的分位数都计算得很精确,也不存在这种简单的总体换算关系。 分辨率决定答案能有多精细 对于常见的正数经典桶,分位数函数先定位目标排名所在的桶,再假定该区间内的观测均匀分布,执行线性插值。真实延迟却可能集中在桶的某一侧,所以返回值是与桶宽相关的估计,并不是被重新找回的实际观测。应该围绕业务真正关心的运行区间设置边界,而不是把大部分精度浪费在与目标无关的范围内。 如果桶边界正好位于三百毫秒目标上,就可以直接统计达到该延迟要求的请求数。这通常比观察一个插值得出的高分位数是否恰好低于阈值,更贴近事件型目标的定义。不过,经典直方图每增加一条边界,都会为每组标签组合增加桶序列。现代 Prometheus 也支持原生直方图,当前文档建议在埋点及相关系统支持时优先考虑;标准指数型原生桶采用不同的插值方式,下面的实验特意只讨论经典桶。 坏掉的样子 上层桶过宽时,少量观测移动就可能让尾部估计出现看起来很大的跳跃。经典分位数计算还要求存在正无穷桶,遗漏它可能使结果无效。如果目标排名落在最后的无限桶中,Prometheus 会返回前一个有限桶的上界,而不是编造一个无限延迟。上述输出都不能告诉你最慢那条请求的准确耗时,追查单次异常仍需要更细的观测材料。 空样本总体没有可解释的观测分位数。跨实例桶边界不匹配、同一指标混入不同单位,或者累积计数出现不合法的下降,也会破坏结果含义。按时间查询累计计数器时,应先计算合适的增量或速率,再聚合实例,使重置得到正确处理。另外,全请求分布也要说明结果范围:快速失败可能降低表面延迟,却同时让服务变得更不可用,不能仅凭曲线变快就宣布改善。 快速验证 保存为 histogram-shape.py,使用 Python 3 执行。程序先从人工构造的原始样本计算精确的最近秩高分位数,再分别根据两套经典桶估计相同分位点。精简插值函数只适用于这里的正数、非空样本,并断言选中的桶有有限边界;它用于展示桶内假设,不替代 Prometheus 对所有特殊输入和边界情况的完整实现。 ...

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