跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

设计归档

PG Exporter 的架构决策、被否决方案与实现契约

设计归档解释 PG Exporter 为什么采用今天的实现方式。每篇文章都会区分设计、实现、合并、发布、软件包、部署与生产验证,避免把一项已经接受的设计误读成已经发布的功能。

当前产品行为以使用手册为准,已经交付的版本以发布归档为准;本栏目记录形成这些结果的理由、备选方案、不变量与验证证据。

1 - 把 PostgreSQL CSV 日志转成持久指标

PG Exporter PostgreSQL CSV 日志指标的产品边界、持久状态协议、有限指标契约与失败语义

决策状态: 已在开发线实现;截至 2026-08-28,尚未进入 main、tag 或公开软件包。
决策日期: 2026-08-24。
适用范围: Composite Exporter 中默认关闭的 PostgreSQL 14+ CSV 日志 collector。
取代: 我们最终没有做的 PostgreSQL 可观测性产品中独立产品的方向。
发布边界: 已有实现与源码测试;合并、release、软件包、Pigsty 部署与生产 canary 仍是独立门槛。

PostgreSQL 日志包含 SQL 快照无法重建的运维事实:deadlock、认证失败、被取消的 autovacuum worker、checkpoint 阶段、temporary file 删除、客户端断连,以及由日志策略选择记录的 statement duration。设计问题不在于这些记录是否有用,而在于如何暴露一小组可靠信号,同时不把 PG Exporter 变成日志平台。

最终边界刻意保持狭窄:

PG Exporter 持续读取一个本地 PostgreSQL CSV 日志目录,持久化有限的聚合状态,并在现有 /metrics 上发布低基数 Counter 与 Histogram。它不存储、查询或发送原始日志。

产品边界

Collector 属于现有 pg_exporter binary、软件包、service、target 与 /metrics。只有非空日志目录才会启用:

pg_exporter \
  --pg-log-dir=/var/log/postgresql \
  --pg-log-state-file=/var/lib/pg_exporter/pglog-state.json \
  --pg-log-poll-interval=1s

对应环境变量是 PG_EXPORTER_PG_LOG_DIRPG_EXPORTER_PG_LOG_STATE_FILEPG_EXPORTER_PG_LOG_POLL_INTERVAL。默认目录为空,默认状态文件为 /var/lib/pg_exporter/pglog-state.json,默认轮询间隔为一秒。

关闭时不存在 worker、目录扫描、状态锁、状态文件、日志 metric family 或额外 scrape 协调。--dry-run--explain 不启动 worker。

功能明确不提供 JSONLOG、stderr、syslog、journald、云日志 API、其他组件日志、日志发送、OTLP、Kafka、VictoriaLogs、Loki、全文搜索、TUI、session timeline、任意用户正则、reset endpoint 或自动根因分析。PostgreSQL logging setting 继续属于普通 SQL/部署配置,不存在 pg_log_setting_* 指标。

为什么选择 CSV,为什么必须读取完整 record

PostgreSQL 14 到 18 文档给出相同的 26 个 CSV 字段,包括 timestamp、user、database、process 与 session identity、per-session line number、severity、SQLSTATE、message 字段、application、backend type、parallel leader PID 与 query ID。PostgreSQL 示例导入表使用 (session_id, session_line_num) 作为主键。

CSV 不是一条物理行一个 record。Query、detail、hint 与 context 字段可以包含逗号、引号、回车和嵌入换行。bufio.Scannertail -F | regexstrings.Split 最终都会把一个逻辑 record 拆成多个假记录。

Collector 使用完整 CSV framing 状态机与标准 CSV decoder。Active file 末尾未完成的 record 会保留为 pending input;只有逻辑 record 完整解析后才提交 offset。单条 record 上限是 16 MiB。畸形或超大数据会进入有限 resync 路径,并增加明确的 parse 与 gap Counter,而不是静默前进并假装没有问题。

第一版只支持 PostgreSQL 14+,以获得固定 26 列 schema。连续 schema mismatch 会让组件降级,不会猜测另一种格式。

Polling、文件身份与轮转

Worker 周期性 reconcile 目录,而不把 fsnotify 当权威。面对 rename rotation、丢失通知、重启和已有多代日志时,polling 更容易实现成可移植、可审计的正确模型。

每个 cursor 记录稳定文件身份、generation、哈希后的路径身份、byte offset、size、小型内容 fingerprint、EOF/消失状态和 resync 状态。持久文件不保存明文日志路径。

Rename rotation 按 identity 跟随:旧文件改名后仍可读完,新文件使用自己的 cursor 开始。如果文件小于已提交 offset,collector 将其视为 truncate,把该 generation 的 offset 重置为零,并同时增加 rotation 与 input-gap 指标。Copy-truncate 可能在 reader 发现之前覆盖未读字节,因此设计报告不可避免的 gap,而不承诺不可能实现的 exactly-once。

未读完的文件消失和 fingerprint identity change 也会成为显式 gap。已经读完且消失的 cursor 可以作为 tombstone 淘汰。状态最多跟踪 256 个文件 identity,不会为了满足上限而删除仍有活动工作的 cursor。

首次启动且没有状态文件时,所有现有文件在当前 EOF 建立 baseline,避免把任意保留历史当成新 Counter。Baseline 会增加 state_resets_total 与一个 input-gap 原因,让运维人员看见指标进入了新 epoch。第一版不提供历史回填。

持久提交协议

从日志派生的 Counter 与 Histogram 必须跨 PG Exporter 重启保存。只推进 cursor 而不保存聚合会漏计;只发布聚合而没有匹配 cursor,重启后会重复计数。设计把它们放进同一个版本化状态对象提交。

一轮后台周期遵循以下顺序:

扫描并读取完整 record
    -> clone 上一版 state
    -> 更新 cursor、counter、histogram 与 self-state
    -> 验证单调性、标签、上限与 metric family
    -> 写入 0600 临时状态文件
    -> fsync 临时文件
    -> 原子 rename
    -> 在支持的平台 fsync 父目录
    -> 发布与之匹配的不可变指标快照

状态文件上限为 4 MiB,必须是权限 0600 的普通文件;symlink 与不安全权限会被拒绝。非阻塞状态锁阻止两个进程共同拥有同一游标。状态中的目录身份必须与配置来源一致。

发布点至关重要。Rename 前崩溃会保留旧 state 与旧 snapshot;rename 后、下一次 scrape 前崩溃会留下新 durable state,重启后可恢复相同 family。请求路径只读 atomic pointer,永远不扫描文件、不解析 CSV、不写状态、不调用 fsync

每轮 pass 最多处理 100,000 条 record 或 64 MiB。若仍有 backlog,worker 会立即继续下一轮,而不是等待普通 poll interval。

指标与基数

核心 family 是:

pg_log_records_total{severity}
pg_log_errors_total{severity,sqlstate_class}
pg_log_query_duration_seconds{kind}
pg_log_events_total{category,event}

常规默认表面还覆盖有限 exact SQLSTATE、checkpoint/restartpoint 数量与 duration/WAL 活动、autovacuum 结果与 duration、lock wait、temporary file 数量与大小、连接和 session duration。

与 SQL 快照直方图不同,日志 Histogram 是持久累计观测。它的 bucket、count 与 sum 连同 cursor 一起持久化,所以在显式 state reset 形成可见新 epoch 之前,普通 Prometheus Counter Histogram 的 rate() 等查询适用。

标签全部来自固定枚举。例如 query duration 的 kind 只有 statementexecuteparsebindother。Exact SQLSTATE 仅允许官方 code,最多 128 个已观察 code 序列,custom 与 unknown 合并为 other。完整日志指标表面有 1,200 序列硬上限。

原始 query、message、detail、hint、context、database、user、application、relation、query ID、client address、PID、session、transaction ID 与文件路径永远不进入 label 或 durable state。一条 record 可以贡献多个安全聚合,但最多只产生一个 primary classified event。

这些决策用取证细节换取可预测监控。Exporter 能告警 deadlock 或认证失败增加,却无法展示触发它们的 SQL 文本。

自监控与可信度

没有 reader 健康证据,业务指标本身并不可信。Collector 增加:

pg_exporter_log_bytes_read_total
pg_exporter_log_parse_errors_total{reason}
pg_exporter_log_files_watched
pg_exporter_log_last_record_timestamp_seconds
pg_exporter_log_state_persist_timestamp_seconds
pg_exporter_log_rotations_total{type}
pg_exporter_log_input_gaps_total{reason}
pg_exporter_log_state_resets_total

它还以 pg_exporter_component_*{component="postgres_log"} 进入统一健康面。输入、权限、parser、state、limit 或 family conflict 故障只会把日志组件标为 down。在安全的情况下,上一份已提交业务快照继续可用;up、最后成功时间与错误 Counter 会显示它已经陈旧或不完整。

--disable-intro 移除 exporter/component/log-reader 自监控,但保留 pg_log_* 业务指标,延续 exporter introspection 与领域数据的既有边界。

日志策略决定指标含义

Collector 只能测量 PostgreSQL 选择输出的 record。因此 pg_log_query_duration_seconds 是由 log_durationlog_min_duration_statement、采样与协议行为决定的条件分布。除非 PostgreSQL 确实配置为记录所有 query duration,否则不能把它描述成全部查询的全局分布。

Checkpoint、autovacuum、connection、disconnection、lock-wait 与 temporary-file family 同样只有在相应服务器设置产生消息时才有数据。PG Exporter 记录这些输入前提,但不把 setting 重复成日志派生指标。

PostgreSQL 日志可能包含敏感 statement 和客户端数据。Service account 需要读取指定目录,但不能把日志改成 world-readable。Collector 只保存有限聚合与哈希 source identity,不会把原始 record 复制进状态文件。

被否决的方案

  • /metrics 中读取日志被否决,因为解析与持久化时延会进入 PostgreSQL 抓取路径。
  • fsnotify 不能作为正确性权威,因为通知可能丢失,也不能替代重启后的 reconcile。
  • 首次启动处理全部历史文件被否决,因为 retention policy 会不可预测地定义 Counter 起点。
  • At-least-once 发布被否决,因为重复告警 Counter 不是可接受的恢复策略。
  • 动态标签和用户正则被否决,因为输入数据会控制基数与持久 schema。
  • 新 endpoint 或 binary 被否决,因为这是一个有限 metric source,不是第二个产品。

验收与剩余门槛

源码验收覆盖 PostgreSQL 14、16、18 fixture;quoted comma、quote、CRLF、multiline 与 partial record;rename rotation、truncate、重启恢复、state lock 与权限;状态持久化前后的 crash point;事件目录 fixture;序列预算;family conflict;关闭快路径;overlap scrape;shutdown;race test 与软件包元数据。

这些测试只能建立实现 candidate,不能证明公开版本。交付前,精确合并提交必须通过仓库 CI 与 package build,再用最终 service user 在 disposable PostgreSQL 实例上验证真实轮转和重启。Pigsty target、Dashboard、recording rule、告警、软件包升级与生产 canary 都需要独立证据。

Composite 协调契约见一个端点,多种来源。CSV schema 与日志输入前提以 PostgreSQL 官方文档为准:PostgreSQL 14 loggingPostgreSQL 18 logging

2 - 把 pgBackRest 指标移出抓取路径

为什么 PG Exporter 在有界后台 worker 中执行 pgBackRest,并提供不可变的 last-good 快照

决策状态: 已在开发线实现;截至 2026-08-28,尚未进入 main、tag 或公开软件包。
决策日期: 2026-08-23。
适用范围: Composite Exporter 内可选的 --pgbackrest 缓存组件。
发布边界: 源码行为已经实现并测试;公开版本、软件包、部署和独立 exporter 退役尚未验证。

pgBackRest 通过 pgbackrest info --output=json 暴露丰富的备份状态,但它提供的是命令,不是低延迟 metrics endpoint。命令可能检查本地配置、访问远程仓库、等待存储、输出大量 JSON,或因为与 PostgreSQL SQL 指标无关的原因失败。

如果每次 Prometheus 请求都执行命令,主数据库抓取的可用性与延迟就会绑定到备份基础设施。最终设计因此把 pgBackRest 当作缓存组件:

background worker
    -> pgbackrest version
    -> pgbackrest info --output=json
    -> 严格校验并构造指标
    -> 原子发布不可变快照

/metrics request
    -> 读取当前快照
    -> 在 PostgreSQL、Patroni、PgBouncer 之后合并

命令路径与抓取路径不会互相等待。

有界命令执行

Collector 直接执行配置的 binary,不经过 shell。生产默认值如下:

控制项 默认值 契约
命令 pgbackrest 以固定参数直接执行
刷新间隔 2 分钟 最小 10 秒
刷新超时 30 秒 覆盖一轮后台刷新
标准输出 16 MiB 硬上限;溢出立即取消命令
标准错误 64 KiB 有界诊断输出

Worker 先检测 pgBackRest 版本,优先使用数字输出;必要时回退到旧版文本形式。随后执行 info --output=json。版本与 JSON shape 一起校验,因为不同 pgBackRest 版本的可用字段和已知兼容情况并不相同。

这些上限是安全和可靠性契约。损坏仓库、意外命令或恶意 wrapper 都不能无限分配内存,也不能在溢出后留下长期运行的子进程。Runner 在取消后还设置了很短的 wait delay,避免进程清理无限阻塞。

发布前完成全部验证

命令退出成功并不代表刷新成功。JSON 还必须满足预期文档结构、对象数量上限、数值约束、stanza 与 repository 关系,以及 metric name 和 label 规则。最终 Prometheus family 在发布前完成 normalize。

只有完整合法的 candidate 才会替换当前快照。这样 Prometheus 请求可以走更便宜的合并路径:缓存 family 已经经过完整客户端校验,每次数据库抓取只需检查所有权和 header,不再重新验证整份备份数据。

Composite 所有权顺序仍然是:

PostgreSQL > Patroni > PgBouncer > pgBackRest > PostgreSQL Log

与高优先级来源冲突的 pgBackRest family 会被省略,并把组件标为 down;它不能往既有 family 添加样本,也不能让 PostgreSQL gather 变成 fatal。

Last-good 语义

至少成功刷新一次后,后续命令、超时、解析或资源上限故障会保留上一份业务 family。组件报告 up=0,增加有限错误原因,并保留最后成功时间。

这并不是“假装备份来源健康”,而是有意分开两个事实:

  • 最新刷新失败;
  • 如果年龄可见,上一份合法备份状态仍有价值。

相对备份年龄 Gauge 在故障期间继续增长。Worker 保存上一份合法 JSON 与版本,并可在后台只重算与当前时间相关的值。/metrics 本身永远不重新解析 JSON,也不执行 pgBackRest。

第一次成功之前没有 last-good 业务快照。健康保持 down,端点仍会返回 PostgreSQL 和其他合法组件。

健康与重叠抓取

缓存组件通过 component="pgbackrest" 进入统一健康面。除了 parse、gather 等原因,它还包含命令执行与资源上限错误。

重叠 Prometheus 请求不会排队等待完整 Composite scrape。它读取同一个原子 pgBackRest 快照,并在 family 不冲突时纳入结果。因此 overlap 期间仍可使用缓存数据,不会启动第二条命令,也不会由请求路径修改组件健康状态。

为什么不能每次抓取都执行

按需执行看似能提供更“新”的数据,却会产生多个不良契约:

  • Prometheus timeout 变成备份命令 timeout;
  • 并发抓取可能并发检查仓库;
  • repository 延迟可能隐藏 PostgreSQL 指标;
  • scrape storm 会放大本地与远程存储负载;
  • 命令输出大小与进程清理变成 HTTP handler 职责。

独立 exporter 进程提供更强的进程隔离,但会保留额外端口、target、软件包和健康模型。Composite 模式提供迁移选择,并不声称所有环境必须立即退役独立进程。

通用命令 plugin framework 同样被拒绝。pgBackRest 的 JSON、版本兼容、安全、指标与 last-good 语义都具有领域特性。把它当任意 shell 输出会削弱验证,并让尚未稳定的 plugin API 进入产品表面。

运维结果

启用组件意味着 PG Exporter service 获得执行 pgBackRest 和读取其配置的权限。官方 PG Exporter 软件包无需捆绑 pgBackRest binary;可执行文件与 repository credential 继续由主机备份安装负责。

运维人员必须对组件健康和最后成功年龄告警,不能只看备份指标仍然存在。替换独立 exporter 前,还必须用最终软件包和真实 service user 验证实际仓库。源码测试、tag、打包、Pigsty target 修改与生产等价性是独立验收门槛。

外层协调器与失败规则见一个端点,多种来源

3 - 我们最终没有做的 PostgreSQL 可观测性产品

为什么我们探索过 local-first PostgreSQL 事故工作台,最后却把目标收缩为 PG Exporter 内部的 CSV 日志指标

决策状态: 2026-08-24 被进程内 PostgreSQL CSV 日志指标方案取代。
研究日期: 2026-08-13。
仍然有效: CSV framing、轮转、SQLSTATE、隐私和有限基数结论。
不再适用: 新产品、仓库、binary、品牌、交互查询工具、TUI、Agent 或日志发送管道。

在收敛 PostgreSQL 日志指标范围之前,我们探索过一个大得多的产品:local-first 事故工作台。它可以检查结构化日志、重建 session 时间线、提供交互过滤与终端界面,并最终演进成节点信号 Agent。

这次探索之所以有价值,恰恰是因为产品没有被做出来。它把任何正确 PostgreSQL 日志 parser 都需要面对的事实,与会改变 PG Exporter 产品类别和运维模型的野心分开了。

最初的假设

用户问题真实存在。事故发生时,DBA 往往拥有本机 PostgreSQL 日志,却没有在中央平台准备好查询。一个理解 PostgreSQL CSV record、SQLSTATE、session、轮转和多行字段的工具,可以在不上传 SQL 文本、不先部署后端的前提下给出有用时间线。

当时设想的工作台强调:

  • 正确解析 PostgreSQL 14+ CSV,而不是使用逐物理行正则;
  • 按时间、severity、SQLSTATE、database、user、application 与 session 过滤;
  • 跨 rename rotation 保留上下文;
  • 在没有 Web 服务的机器上提供 terminal-first 工作流;
  • 默认本地处理,保护隐私;
  • 从同一 parser 产生低基数 Prometheus 指标;
  • 未来可能增加持久游标和多输出的 daemon。

这是一个自洽的产品想法,也远大于眼前需求。

研究确认了什么

几项结论在转向后仍然成立。

PostgreSQL 14 到 18 文档给出相同的 26 字段 CSV 布局,其中包含 session_idsession_line_numbackend_type、leader PID 与 query ID。PostgreSQL 的示例导入表使用 (session_id, session_line_num) 作为主键。CSV 字段可以包含逗号、引号和换行,因此逐物理行 scanner 不是正确 parser。

SQLSTATE 比本地化 message 文本稳定,是错误分类的第一依据。Message grammar 仍适合 checkpoint、autovacuum、lock wait、temporary file 与连接事件等有限 PostgreSQL 事件,但必须版本化并有测试。

原始 query、detail、hint、context、user、database、application、client address、PID、session、transaction ID 与文件路径都不适合作为默认 metric label。Prometheus 适合有限计数和分布,不是日志索引的替代品。

轮转、部分写入、重启恢复与游标持久化是产品需求,不只是 parser 实现细节。重启后静默重计或漏计的工具,会产生比没有工具更糟的证据。

为什么否决更大的产品

决定性问题不是技术可行性,而是范围与责任。

工作台需要独立命令、输出 schema、UX、软件包、文档、支持表面与发布周期。有状态 Agent 还会引入服务管理、升级、队列、背压、重试、磁盘保留、安全政策与发送 SLO。TUI 和日志索引优化人工调查,Prometheus Exporter 优化有限的机器可读状态。把它们放进同一个 MVP,会延迟最小有用结果,也更难证明可靠性。

新产品也没有得到必须独立存在的验证。眼前请求不是“搜索全部日志”或“把日志送到另一个后端”,而是“从 PostgreSQL 结构化日志提取少量可靠运维指标”。PG Exporter 已经拥有这项工作需要的 target identity、/metrics、软件包、组件健康与 Pigsty 集成。

品牌和仓库工作因此成为干扰。过早给推测中的产品命名,会让架构显得比用户价值更确定。设计审查最终选择删除整个公开命名表面,而不是继续打磨它。

替代决策

2026-08-24,目标被有意收缩为:

在现有 PG Exporter 进程里增加一个默认关闭的 PostgreSQL CSV 后台 collector,只在现有 /metrics 上暴露有限的运维指标。

替代方案明确排除日志发送、OTLP、VictoriaLogs、Kafka、Web UI、TUI、全文搜索、session timeline、任意用户正则、自动根因分析和其他组件日志。

它保留真正决定可信度的难题:完整 CSV framing、文件身份、rename 与 truncate、持久游标加累计值、原子持久化、不可变快照、固定标签、序列上限与显式 gap。

最终工程契约见把 PostgreSQL CSV 日志转成持久指标

为什么保留被取代的记录

删除探索过程会隐藏这些诱人功能为何缺席;直接发布原始研究同样会误导读者,因为其中包含已经被推翻的命名工作、市场快照与产品建议。

这篇校准后的记录只保留有用因果链:

真实事故工作流
    -> 正确 CSV 与 session 研究
    -> 范围过大的工作台与 Agent 提案
    -> 范围和所有权审查
    -> PG Exporter 内部的有限日志指标

未来维护者不应仅因为交互调查听起来有价值就重新打开大产品路线。只有在用户确实需要现有日志栈无法提供的本地查询体验、至少两个真实消费者验证了事件模型,并且有人愿意承担独立产品生命周期时,才值得重新评估。

来源

本文保留的稳定外部事实来自 PostgreSQL 自身的 CSV 日志定义:PostgreSQL 14PostgreSQL 18。原研究中的产品与品牌快照有意不再转载。

4 - 一个端点,多种来源:Composite Exporter 契约

PG Exporter 如何组合 PostgreSQL、PgBouncer、Patroni 与缓存组件,同时不削弱 PostgreSQL 主抓取

决策状态: 已在开发线实现;截至 2026-08-28,尚未进入 main、tag 或公开软件包。
决策日期: 2026-08-11;2026-08-23 与 2026-08-24 又增加了缓存组件。
适用范围: PostgreSQL、PgBouncer、Patroni、pgBackRest 与 PostgreSQL CSV 日志指标的可选 Composite 协调层。
发布边界: 已有设计与实现证据;合并、tag、软件包、部署和生产替换仍是独立门槛。

PG Exporter 最初是针对一个 PostgreSQL 兼容目标的声明式 SQL Exporter。但在一台 Pigsty 节点上,PgBouncer 管理库、Patroni Prometheus 端点、pgBackRest 本地命令和 PostgreSQL 结构化日志同样包含重要状态。为每种来源各运行一个 exporter 初看简单,却会为同一个数据库实例产生多个 target、端口、标签、健康语义和生命周期责任人。

Composite 设计允许一个 PG Exporter 进程在现有 /metrics 上暴露这些来源。它的目标不是简单拼接指标,而是把失败和所有权规则定义到足够精确,使新增可选组件永远不会削弱原有 PostgreSQL 契约。

不可协商的主线不变量

PostgreSQL 始终必选并且权威。只有 PostgreSQL collector 的 gather error 能成为 Composite gatherer 返回的 fatal error。PgBouncer、Patroni、pgBackRest 或日志指标失败时可以移除自己的数据并更新自己的健康状态,但不能把一份本来合法的 PostgreSQL 响应变成 HTTP 500。

这条规则针对组合端点最危险的故障模式:可选 Patroni 请求的 TLS 错误、缓慢的备份仓库或畸形日志记录,都不能隐藏运维人员诊断事故时最需要的数据库指标。

所有可选组件默认关闭。对应 flag 与 URL 为空时,PG Exporter 不创建 collector、不发请求、不执行命令、不扫描文件、不注册组件健康指标,也不会给旧抓取路径增加协调锁。

一个协调层,三种执行模型

不同来源的时延和新鲜度契约并不相同,因此不应被塞进同一个通用 adapter:

Prometheus /metrics
        |
        v
Composite coordinator
  |-- PostgreSQL SQL       实时、权威
  |-- PgBouncer SQL        实时、可选
  |-- Patroni HTTP         实时、可选
  |-- pgBackRest snapshot  缓存、可选
  `-- PostgreSQL log       缓存、可选

PostgreSQL 保留既有实时 SQL 路径。PgBouncer 同样在每次抓取时运行 SQL,因为查询成本低,而且现有 exporter 语义已经有用。Patroni 实时抓取,因为 role、leader、DCS 与 timeline 状态可能立即变化。

pgBackRest 与 PostgreSQL 日志使用后台 worker 和不可变快照。在 Prometheus 请求中执行备份命令或扫描日志,会让数据库指标可用性依赖磁盘、仓库、parser 与持久化时延。它们的详细契约分别记录在把 pgBackRest 指标移出抓取路径把 PostgreSQL CSV 日志转成持久指标中。

实时工作并发且有界

PgBouncer 与 Patroni 在同一个 sidecar timeout 下并发执行,默认上限为 10 秒。PostgreSQL 与它们并行,但不会被这个可选 deadline 取消,避免 sidecar 预算变成 PostgreSQL 权威查询的新超时。

协调层也不会让重叠的 Prometheus 请求排队等待可选实时工作。若已有一轮完整 Composite scrape 运行中,重叠请求会收集 PostgreSQL、进程指标与无锁缓存快照,并把实时可选工作标记为降级或省略,而不是等待上一轮结束。

这是降级策略,不只是性能优化。抓取风暴发生时,应先降低可选完整性,而不是增加主路径延迟和 goroutine 队列。

Metric family 所有权

合并多个 registry 必须为同名 family 指定确定性规则。最终顺序是:

PostgreSQL > Patroni > PgBouncer > pgBackRest > PostgreSQL Log

进程与 HTTP 自监控也排在 PostgreSQL 之后。低优先级来源只有在高优先级来源未占用 family 名及其 Histogram/Summary 保留派生名时才能加入。即使 Help、类型和标签完全相同,也不能往现有 family 追加样本。

这个整族规则避免两个组件部分共同拥有一个指标,也会阻止高优先级 foo Histogram 与低优先级字面 foo_count 并存。可选组件中没有冲突的 family 仍可保留;冲突会把该组件标为 gather 失败,却不会改变 PostgreSQL 结果。

Patroni 需要解析,而不是盲透传

Patroni 已经提供 Prometheus 指标,但字节拼接会绕过验证:只要输出冲突元数据、OpenMetrics 专用语法、过量数据或重复 family,最终响应就可能非法。Composite collector 因此会抓取、解析、验证,并重新编码受支持的经典 Prometheus 语义。

设计保留 Patroni 的 family 名、Help、类型、标签与 sample value,不给每个业务指标重命名或增加 component 标签。来源身份属于 target label 和组件健康面,不应进入所有业务序列。

出站 HTTPS 使用系统 root 加可选 CA 文件。设计不提供 insecure_skip_verify;证书错误应成为可见的 Patroni 组件故障,而不是静默削弱传输验证。

健康不是一个布尔值

每个启用组件都有固定、低基数的健康面:

pg_exporter_component_enabled{component="..."}
pg_exporter_component_up{component="..."}
pg_exporter_component_scrape_duration_seconds{component="..."}
pg_exporter_component_scrape_errors_total{component="...",reason="..."}
pg_exporter_component_last_success_timestamp_seconds{component="..."}

这些信号回答不同问题:enabled 是配置状态,up 是最近一次组件结果,last_success 表示陈旧程度,错误 Counter 则保留 connect、timeout、TLS、parse、gather、命令执行、state 或 source 等有限原因。

它们不会替代 PostgreSQL 既有的 /up/health/primary/replica 语义。那些路由继续描述 PostgreSQL target;改成“所有组件都健康”会破坏从未选择 Composite 可用性定义的路由与故障转移用户。

--disable-intro 只抑制 exporter 与 component 自监控,不抑制业务指标,延续该参数原有含义。

被否决的替代方案

设计有意拒绝了几种更宽泛的抽象:

  • 通用 plugin registry 会在真实组件尚未稳定之前,把生命周期和优先级暴露成公共扩展 API,并增加首轮审计成本。
  • 串行采集会让一个可选来源耗尽完整请求预算,后续来源甚至无法开始。
  • Patroni 字节透传省掉一次解析,却同时放弃冲突、协议和资源验证。
  • 给所有 family 增加 component 标签会破坏既有指标契约、扩大序列数,也不能解决名称所有权。
  • “所有来源必须成功”会让可选集成降低整体可用性。
  • 同一个改动里替换全部独立 exporter 与 Pigsty target,会把源码正确性和部署迁移混在一起并移除回滚选择。

最终设计刻意明确而非通用:具名组件、固定优先级、实时与缓存两条路径,以及 PostgreSQL-first 失败语义。

结果与发布门槛

Composite 模式可以减少 target 与进程数量,同时保留既有 namespace。代价是一个进程要承担更多凭据、网络客户端、parser、后台 worker 与健康状态。运维人员必须只授予已启用组件所需权限,并把 family 冲突视为配置或兼容缺陷。

开发树中已有实现,不等于稳定版、Pigsty target 迁移、Dashboard、recording rule、告警或旧进程退役已经发生。功能进入公开分支和版本时,必须分别验证这些门槛。

5 - 快照直方图是 Gauge,不是 Counter

为什么 PG Exporter 每次查询都重新构造 SQL 分布,并用 Gauge 序列暴露 bucket、count 与 sum

决策状态: 已随 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 序列:

pg_xact_age_seconds_bucket{datname="app",le="10"} 5
pg_xact_age_seconds_bucket{datname="app",le="30"} 9
pg_xact_age_seconds_bucket{datname="app",le="+Inf"} 11
pg_xact_age_seconds_count{datname="app"} 11
pg_xact_age_seconds_sum{datname="app"} 214

熟悉的命名和累计 le bucket 仍可供 histogram_quantile() 使用;Gauge 类型则如实表达所有值都可能下降。这是一项有意的语义折衷:输出具有经典 Histogram 的形状,但不是 Counter Histogram。

不要对这些序列应用 rate()irate()increase() 或 Counter reset 逻辑。应直接查询当前分布:

histogram_quantile(
  0.95,
  sum by (datname, le) (pg_xact_age_seconds_bucket)
)

当前平均值同样直接计算:

sum by (datname) (pg_xact_age_seconds_sum)
/
sum by (datname) (pg_xact_age_seconds_count)

配置与 SQL 契约

设计只增加一个用户可见的 usage:HISTOGRAM

- seconds:
    usage: HISTOGRAM
    bucket: [1, 3, 10, 30, 100, 300, 1000, 3000]
    description: Open transaction age snapshot in seconds

配置列中的每个非 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,验证实时分布。原设计目录不再承担文档权威,因此在这里保留可复现实例:

\set query_band random(1, 4)
\if :query_band = 1
  \set query_ms 300
\elif :query_band = 2
  \set query_ms 3000
\elif :query_band = 3
  \set query_ms 10000
\else
  \set query_ms 30000
\endif

\set hold_band random(1, 4)
\if :hold_band = 1
  \set hold_ms 300
\elif :hold_band = 2
  \set hold_ms 3000
\elif :hold_band = 3
  \set hold_ms 10000
\else
  \set hold_ms 30000
\endif

BEGIN;
SELECT pg_current_xact_id(), pg_sleep(:query_ms / 1000.0);
\sleep :hold_ms ms
COMMIT;

关键证据不是某个 benchmark 数字,而是:事务推进时 bucket 总体会移动和消失;缓存命中不会重复累计;重载新布局时不会保留旧样本。

结果与代价

这个设计让用户无需有状态 exporter 就能得到可聚合的当前分布。代价是用户必须理解:这里的 _bucket_count_sum 后缀并不代表 Counter 时间语义。文档、Dashboard 与告警必须明确说明这一点。

完整语法由采集器配置维护,已经交付的实现见 v1.4.0 发布注记。Prometheus 官方文档仍是普通 Counter Histogram 的权威,并解释了常规 rate() 查询为何依赖单调 count:Histograms and summaries