跳转到主要内容

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

PG Exporter 如何组合 PostgreSQL、PgBouncer、Patroni 与缓存组件,同时不削弱 PostgreSQL 主抓取
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、告警或旧进程退役已经发生。功能进入公开分支和版本时,必须分别验证这些门槛。