快照直方图是 Gauge,不是 Counter
决策状态: 已随
v1.4.0发布。
决策日期: 2026-07-11;发布前于 2026-07-17 完成修订。
适用范围: PG Exporter v1.4.0 及后续版本中的HISTOGRAM列与内置pg_xact_age采集器。
发布边界: 本文描述已经发布的源码与指标契约;Dashboard 与 recording rule 仍由消费者负责。
PG Exporter 最初把每个 SQL 结果单元映射成标量 Gauge 或 Counter。它能回答“最老事务有多老”,却无法保留当前总体的分布形状。一个 30 分钟事务与一百个 18 秒事务可能具有相同最大值,但需要完全不同的处置方式。
第一版 Histogram 在不把 PG Exporter 变成有状态事件处理器的前提下补上了分布能力。它的核心决策只有一句话:
PG Exporter Histogram 是一次真实 SQL 查询返回的全部行所重建的分布。它描述当前总体,而不是 exporter 进程启动以来累计看到的观测。
这句话决定了 exposition 类型、缓存、失败语义、PromQL 与 bucket 策略。
时间语义契约
参考总体是打开中的事务。事务开始时出现,随着时间经过移入更老的 bucket,在提交或回滚后消失。观测数和每个累计 bucket 都可能在两次抓取之间上升或下降。
普通 Prometheus Histogram 是累计 Counter:bucket、count,通常还有 sum,都会随时间增加。因此 Prometheus 的常规查询会先使用 rate(),再计算请求速率或时间窗口分布。这个假设不适用于每次重新生成的 SQL 快照。
所以 PG Exporter 把逻辑 Histogram 暴露成普通 Gauge 序列:
熟悉的命名和累计 le bucket 仍可供 histogram_quantile() 使用;Gauge 类型则如实表达所有值都可能下降。这是一项有意的语义折衷:输出具有经典 Histogram 的形状,但不是 Counter Histogram。
不要对这些序列应用 rate()、irate()、increase() 或 Counter reset 逻辑。应直接查询当前分布:
当前平均值同样直接计算:
配置与 SQL 契约
设计只增加一个用户可见的 usage:HISTOGRAM。
配置列中的每个非 NULL 值都是一次观测。具有相同完整标签元组的行属于同一个分布;同一查询中的多个 Histogram 列彼此独立聚合。
Bucket 是有限、包含上界的边界。配置加载阶段会拒绝空列表、重复值、非递增顺序、NaN 与无穷值。PG Exporter 自动追加 +Inf,用户不能自行配置。生成的 le 标签及 _bucket、_count、_sum 派生名称都是保留表面,冲突必须在抓取前失败。
scale 在 bucket 分配与求和之前生效;时间戳与布尔值沿用标量转换路径,不应用 scale。显式 default 会把 NULL 转成一次观测,否则忽略 NULL。
查询、缓存与原子性
每次真实 SQL 执行都从空 accumulator 开始。PG Exporter 对观测分组、分配有限 bucket、计算累计计数,并只在完整结果集验证成功后物化指标。
如果查询失败、缺少必需列,或任意观测无法转换成有限数值,本次执行中的标量与 Histogram family 都不会发布。失败在 collector query 边界上是原子的。
缓存命中只复用上一份不可变结果,不会再次累计同一批观测。下一次真实查询从零构造新快照,Histogram 状态不会跨执行保留。配置重载会丢弃旧 collector 与旧缓存,包括旧 bucket 布局。
这些规则让 Histogram 继续服从声明式 collector 模型:SQL 是权威数据源,TTL 决定查询是否执行,exporter 不发明第二个时间域。
为什么选择显式 Gauge 序列
几种看似更简单的方案被明确否决:
prometheus.NewConstHistogram会产生 Counter 风格的 Histogram 元数据;数值虽能编码,时间语义却是错误的。- OpenMetrics GaugeHistogram 能更精确地表达语义,但 PG Exporter 使用经典 Prometheus 文本,没有理由为了一个采集类型引入 OpenMetrics 专用模式。
- Native Histogram 解决的是存储和分辨率问题,不会把不断变化的 SQL 总体变成累计事件流。
- 在 SQL 中预聚合 bucket 会让每个 collector 重复分组逻辑,并产生第二套配置契约。
- 跨抓取保留观测会把“当前数据库状态”改成“本 exporter 进程曾看到的事件”,随之引入重启、持久化和重复计数义务。
最终方案刻意保持狭窄:输入原始 SQL 观测,输出一份当前分布。
参考采集器与 bucket 演进
第一版实现发布前围绕 pg_xact_age 完成修订:按数据库统计打开事务年龄与 idle-in-transaction 年龄,只保留 client backend,并采用适合即时运维问题的类对数 bucket 网格。目标查询是当前分位数、超过阈值的当前总体与当前均值。
以下 pgbench workload 用于制造多档 query time 与 hold time,验证实时分布。原设计目录不再承担文档权威,因此在这里保留可复现实例:
关键证据不是某个 benchmark 数字,而是:事务推进时 bucket 总体会移动和消失;缓存命中不会重复累计;重载新布局时不会保留旧样本。
结果与代价
这个设计让用户无需有状态 exporter 就能得到可聚合的当前分布。代价是用户必须理解:这里的 _bucket、_count、_sum 后缀并不代表 Counter 时间语义。文档、Dashboard 与告警必须明确说明这一点。
完整语法由采集器配置维护,已经交付的实现见 v1.4.0 发布注记。Prometheus 官方文档仍是普通 Counter Histogram 的权威,并解释了常规 rate() 查询为何依赖单调 count:Histograms and summaries。