跳转到主要内容

25 望闻问切:监控体系与可观测诊断

“有监控”很容易,“知道发生了什么”很难。

一个看起来成熟的平台可能同时拥有:

26 个 PostgreSQL 仪表盘
3,000 多种指标名
数百条记录规则
几十条告警规则
集中日志
追踪后端
值班通知

事故发生时却仍然只能说:

CPU 高了
连接多了
复制慢了
面板红了

这些句子描述了现象,没有回答五个决定性问题:

  1. 用户旅程是否真的失败、变慢、读旧或产生错误结果?
  2. 这是首发症状、伴随现象,还是已经被证据支持的机制?
  3. 观察数据是零、尚未刷新、被重置、被采样,还是根本缺失?
  4. 哪个 owner 应采取什么首个安全动作
  5. 什么证据能够证明恢复,而不是仅仅“面板变绿”?

第 24 章先定义了服务、SLI、SLO、控制目标、缺失语义和告警治理。本章做下一步:

observation contract
  -> 指标、日志、事件和追踪
      -> PostgreSQL 原生统计与 SQL 基线
          -> Pigsty 采集、存储、规则、面板和通知
              -> 可行动告警
                  -> 有界诊断包
                      -> 合成规则与离线路由演练
                          -> 覆盖表、盲区与生产门禁

这条链的重点不是“多收集一些数据”,而是让每个结论都能说明:

question       想回答什么
source         哪个系统产生事实
semantics      值、窗口、标签、reset 和缺失是什么意思
cost           采集、查询和保留会付出什么代价
action         谁可以做什么
verification   怎样证明结论与恢复
boundary       什么仍然不知道

本章目标

完成本章后,你应当能够:

  1. 从用户、入口、数据库和主机四层组织观察问题;
  2. 区分症状信号、原因信号、控制信号与监控系统自身信号;
  3. 说明指标、日志、事件和追踪各自适合回答什么;
  4. 为 label cardinality、采样、保留和查询成本建立预算;
  5. 把“没有数据”区分为无流量、采集故障、查询错误、延迟与真实零值;
  6. 正确读取 pg_stat_activitypg_locks 与 wait event;
  7. 使用 pg_stat_databasepg_stat_iopg_stat_walpg_stat_checkpointerpg_stat_archiver
  8. 解释累计计数、瞬时状态、估算值和进度视图的不同时间语义;
  9. 区分 WAL distance、时间 lag 与 commit-correlated freshness;
  10. 观察 autovacuum、冻结年龄、dead tuple、对象增长和维护进度;
  11. 正确解释 pg_stat_statements 的聚合键、reset、deallocation 与权限;
  12. 解释为什么 normalized query text 仍不能随意进入证据;
  13. 为慢语句、锁等待、临时文件和错误日志设置有界政策;
  14. 评估 auto_explainANALYZE、timing、采样和参数泄露成本;
  15. 把第 24 章 SLO 合同变成 multiwindow、multi-burn-rate 规则;
  16. 区分 page、ticket、diagnostic 与 proposed test route;
  17. 为 alert 的 for、分组、抑制、恢复和缺失语义写测试;
  18. 防止 fast burn、slow burn 与预算工单形成重复风暴;
  19. 防止 metamonitoring 抑制独立用户症状或正确性告警;
  20. 理解 Pigsty v4 的 VictoriaMetrics、VictoriaLogs、VictoriaTraces、 VMAlert、Alertmanager、Grafana、pg_exporter 与 Vector;
  21. clsinsip、database 与 queryid 在不同粒度间下钻;
  22. 从仪表盘回到 PostgreSQL SQL、日志和主机事实复核;
  23. 自动保存有界、私密、可复验的诊断包;
  24. 区分首发症状、相关现象、候选机制与根因;
  25. 限制诊断查询的 timeout、并发、结果量和权限;
  26. 用隔离时间序列验证 pending、firing、recovery 与 missing;
  27. 用空 receiver 离线验证路由,不触碰真实 pager;
  28. 输出覆盖矩阵,诚实标出尚未实现的应用 SLI;
  29. 对当前沙箱给出“机制通过、生产待决”的可审计结论。

本章不做什么

本章不是一个“复制几十条 PromQL 就上线”的规则包,也不会:

  • 把当前沙箱阈值包装成所有生产环境的通用答案;
  • 修改在线 VMAlert、Alertmanager、Grafana 或 PostgreSQL;
  • 向在线 Alertmanager 提交合成告警;
  • 接入 webhook、邮件、短信、Slack 或真实 pager;
  • 运行 EXPLAIN ANALYZE、压测、统计 reset 或数据库故障注入;
  • 导出 query text、bind value、日志正文、client address 或凭据;
  • pg_up=1 证明订单服务可用;
  • pg_lag=0 证明 read-your-writes;
  • failed_count>0 直接证明归档仍在失败;
  • 用“当前无告警”证明通知链可达;
  • 用一次仪表盘相关性宣布 root cause;
  • 声称 pg36_shop 已经具有真实应用埋点。

实验使用 pg36_shop 这一 synthetic teaching service。应用层五种信号仍是刻意 保留的缺口:

request outcome counter
request duration histogram
commit-correlated freshness probe
domain reconciliation gauge
restore-evidence age gauge

缺口不是失败的写作,而是本章必须保留的事实。如果没有应用事件,平台不能从 数据库组件指标“推算”出一个看似完整的用户 SLO。

从监控到可观测诊断

监控回答已知问题

监控通常从一个已知条件出发:

if bad_ratio_1h > threshold
and bad_ratio_5m > threshold
for 2m
then page

它适合稳定、可计算、可自动执行的问题:

  • SLO 是否快速燃烧;
  • exporter 是否持续不可达;
  • 规则是否持续报错;
  • 恢复证据是否超过政策期限;
  • 容量预测是否进入评审窗口。

可观测诊断解释未知状态

诊断从“不知道为什么”出发,需要沿不同证据层缩小假设:

user latency burn
  -> entry queue or rejection?
      -> pool saturation or connection churn?
          -> PostgreSQL active wait or lock?
              -> queryid cost or plan drift?
                  -> storage, WAL, checkpoint, vacuum or host constraint?

可观测性不是一个产品名,也不是“拥有 logs + metrics + traces”自动获得的属性。 它要求系统输出足够的、语义明确的证据,让操作者能区分多个竞争解释。

诊断不等于根因

本章采用四层语言:

层次 可以说什么 例子
首发症状 最早被可靠观察到的服务偏离 availability fast burn 先触发
伴随现象 与症状同窗出现 pool queue、lock wait 同时升高
候选机制 现象与某机制一致 长事务可能阻止 vacuum 推进
根因证据 机制被复现/独立证实,反例被排除,修复验证闭环 释放特定锁后等待消失且合成路径恢复

“两条线一起升高”最多是相关性。要升级为根因,至少需要:

mechanism
independent corroboration or reproduction
falsification attempts
repair verification

四层问题,而不是四套孤岛面板

本章把信号按问题分为四层:

主要问题 首选事实
用户 旅程是否成功、及时、正确、足够新鲜 eligible events、合成探针、domain reconciliation
入口 请求在哪排队、被路由、重试或拒绝 应用 edge、HAProxy、PgBouncer
数据库 PG 状态能否解释症状 activity、locks、I/O、WAL、maintenance、queryid
主机 资源或基础设施是否构成约束 CPU、memory、disk、network、clock

从下往上推断很危险:

CPU 90%
  therefore users are slow          # 不成立

one replica is down
  therefore service is unavailable  # 不成立

pg_up == 1
  therefore orders are correct       # 不成立

从上往下诊断更稳健:

user symptom is real
  -> locate affected operation and path
  -> correlate entry and database evidence
  -> test competing mechanisms
  -> choose the least harmful action
  -> verify user and control signals

正确性与恢复就绪又是特殊控制:

one unexplained cross-tenant row
  cannot be averaged away

old or missing restore evidence
  cannot be replaced by backup job success

四种信号,各有边界

信号 擅长 不擅长 必须声明
metric 趋势、比率、聚合、规则 高维上下文、单请求故事 type、unit、label、reset、missing
log 离散事件、错误上下文、状态迁移 完整总体比率、无界扫描 schema、采样、脱敏、保留
event 发布、切换、配置和所有权时间线 单独证明因果 actor、target、result、clock
trace 单次跨组件路径 未采样总体、长期预算 sample、baggage、PII、correlation

四者不是竞争关系:

metric detects
event bounds the change window
trace follows one affected request
log explains a discrete failure
SQL verifies PostgreSQL state

同样,也不应强迫每个问题都使用四种信号。一个能够由累计计数精确回答的问题, 不需要先扫描全部日志;一个需要参数上下文的问题,也不应把 error text 做成 metric label。

时间语义比数值更重要

观察数据至少有五种时间性质:

类型 示例 典型陷阱
当前状态 pg_stat_activity.state 一次采样漏掉短暂事件
累计计数 pg_stat_database.xact_commit 把总数当速率、忽略 reset
滚动窗口 rate(counter[5m]) 窗口过短、采样不足
估算值 n_dead_tup 当成精确 bloat
当前进度 pg_stat_progress_vacuum 没有行不等于从未运行

还要处理采集链延迟:

database state time
  -> exporter scrape time
      -> storage ingestion time
          -> rule evaluation time
              -> route grouping delay
                  -> receiver delivery time

如果数据源有意延迟 30 秒,而规则在“当前时刻”查询,最新样本可能尚未可见。 VMAlert 支持 evaluation delay;正确值取决于实际采集和存储延迟,不能机械照抄。

本章的规则分层

实验生成 18 条记录规则和 13 条告警规则。它们分成三类:

第 24 章已经接受的七条

告警 路由 目的
PG36ShopAvailabilityFastBurn page 1h + 5m,14.4x
PG36ShopAvailabilitySlowBurn page 6h + 30m,6x
PG36ShopAvailabilityBudgetTicket ticket 3d + 6h,1x
PG36ShopFreshnessFastBurn page commit-correlated freshness
PG36ShopCorrectnessMismatch page 不允许平均稀释的正确性
PG36ShopCapacityHorizon ticket 经评审预测才进入工作队列
PG36MonitoringPathBroken page 观察或通知路径不可证明工作

仍待治理接受的六条

latency burn
restore evidence stale
active archive risk
long transaction horizon
freeze-age horizon
expected traffic but SLI missing

它们有完整规则和测试,但统一标记:

route: test
severity: candidate
governance_status: proposed-not-accepted

“代码写好了”不等于“组织接受了 page/ticket 政策”。这个显式不一致检查很重要: 第 24 章定义了 latency SLO,却没有接受 latency alert candidate。本章没有暗中 补齐生产政策,而是把它暴露为待决项。

诊断记录

复制距离、长事务、freeze age、dead tuples、exporter 状态、VMAlert rule error 和 notification failure 首先是诊断或控制输入。它们不会因为“容易写阈值”就 自动变成 page。

当前沙箱的真实快照

正式实验公共摘要: observability-run.json

采集时间为 2026-07-29T22:48:21Z。快照只代表该时刻:

项目 观察值
Pigsty v4.5.0
PostgreSQL 18.6
pg_exporter v1.4.0
VictoriaMetrics v1.148.0
VictoriaLogs v1.52.0
VictoriaTraces v0.9.4
Alertmanager 0.33.1
VictoriaMetrics series 44,842
live VMAlert groups 17
live alert rules 50
live recording rules 698
live rule errors 0
current VMAlert alerts 0
pg_up / pg_exporter_up instances 4 / 4
pg36_shop_* application SLI series 0

目标身份通过三层交叉确认:

host            pg-test-1
Patroni scope   pg-test
PostgreSQL      cluster_name=pg-test
role            primary
replication     pg-test-2 + pg-test-3, async streaming
observed WAL gap 0 + 0 bytes

0 bytes 是当时的发送—回放距离,不是 read-your-writes 证明。

pg_stat_statements 快照:

extension version      1.12
schema                 monitor
rows                   194
calls                  122,303
query text exported    false
stats reset            retained

归档快照有一个关键反例:

failed_count           21
last failure           18:57:58Z
last successful archive 22:27:54Z

如果规则只是:

pg_archiver_failed_count > 0

它会在系统已经恢复后永久报警,直到统计被重置。候选规则因此同时检查:

15m 内出现新失败
AND 最近成功归档已经停滞

再回到 pgBackRest 与恢复证据复核。累计 counter、当前故障和恢复就绪是三个 不同结论。

实验为什么分成在线与隔离两部分

在线只读基线

在线部分只做:

  • HTTP health/API/metrics 读取;
  • VictoriaMetrics 即时查询;
  • 通过元节点进入真实 pg-test-1
  • statement_timeout=5slock_timeout=500ms 的只读 SQL;
  • 聚合 activity、locks、I/O、WAL、checkpointer、archiver、 replication 和 pg_stat_statements
  • 记录版本、reset、freshness 与缺口。

不会 reset、reload、写表、运行计划、制造负载或读取 query text。

隔离规则与路由

规则文件上传到沙箱元节点的:

/tmp/pg36-ch25.XXXXXXXX

随后:

  1. vmalert -dryRun 检查规则语法;
  2. vmalert-tool 启动 loopback-only VictoriaMetrics;
  3. 注入合成时间序列;
  4. 验证 normal、pending、firing、recovery 和 missing;
  5. amtool check-config 检查 Alertmanager 配置;
  6. 用八组标签离线解析到空 receiver;
  7. 用五个用例验证抑制边界;
  8. 清理临时目录并确认目录不存在。

它不会接触在线 VMAlert 或在线 Alertmanager。

本章目录

25.1 从问题选择可观测信号

25.2 PostgreSQL 核心运行信号

25.3 SQL 可观测基线

25.4 把观察契约变成告警

25.5 Pigsty 可观测体系

25.6 从告警到诊断包

25.7 实战:实现并演练观察契约

官方资料

本章技术语义优先回到原始文档:

下一章 第 26 章 容量规划与压测基线 会在这些观察 语义之上建立需求模型、容量水位和可比较基线;第 28 章 VACUUM、冻结与膨胀治理 再深入维护信号。第 31 章把本章的诊断包作为故障排查入口,处理慢查询、锁、 连接、复制与磁盘等具体事件。


上一章:纲举目张:SLO、SOP 与组织治理 · 返回下卷导读 · 下一章:胸有成竹:容量规划与压测基线 · 查看全书目录 · 查看索引中心

25.1 从问题选择可观测信号

先选指标,再问它能说明什么,是监控系统膨胀的主要原因。

有 PostgreSQL
  -> 收集所有 pg_* 指标
      -> 导入所有 dashboard
          -> 给红色曲线加阈值
              -> 事故时再猜它们与用户有什么关系

更可靠的顺序是:

decision
  -> question
      -> evidence
          -> semantics
              -> collection cost
                  -> action and verification

例如,“副本 lag 多大”不是一个完整问题。它可能对应三种完全不同的决定:

决定 真正问题 需要的信号
是否把 read-after-write 流量路由到副本 已知 commit 是否在该读取路径可见 commit token probe
是否有 WAL 保留风险 primary 与 replica 的 LSN 距离是否持续扩大 replication position + WAL retention
是否解释查询结果陈旧 用户查询走了哪条路径、返回哪个版本 routing event + application semantics

一个 pg_lag 无法同时替代这三个答案。

本节建立一份问题驱动的信号合同。可执行版本见 signal-contract.json

25.1.1 用户体验、服务入口、数据库与主机四层

第一层:用户是否获得了正确服务

用户层不是浏览器 RUM 的同义词,而是最接近服务承诺的测量点。对 pg36_shop

journey       place-order
eligible      authorized and valid request admitted by the app
good          success returned and token reconciles to one committed order
timely        final result within 250 ms
fresh         committed token visible on declared read path within 5 s
correct       no unexplained invariant mismatch

这些定义直接决定分子和分母:

$$ \text{availability bad ratio}

\frac{\text{failed or unreconciled eligible attempts}} {\text{all eligible attempts}} $$

如果只收 HTTP 2xx/5xx,会漏掉:

  • 返回 200,但事务后来失败;
  • 客户端超时,但写入已经提交;
  • 重试创建了两个订单;
  • 响应成功,但订单属于错误租户;
  • primary 写入成功,随后从副本读取不到;
  • 应用在 admission 前正确拒绝无效请求。

所以用户层信号常常要组合两个测量点:

application edge outcome
  +
commit outcome / idempotency reconciliation

eligible event 必须先定义

没有 eligible 分母,错误率会随着流量分类任意变化。例如:

all inbound requests
  includes scanners, malformed requests, health checks, unauthorized calls

admitted order attempts
  excludes pre-admission syntax and authorization rejection
  includes server outcomes after admission

“用户断开”是否计入也不能临场决定:

disconnect before admission and no server outcome
  -> may be excluded by a predeclared rule

disconnect after transaction may have committed
  -> unknown outcome, must reconcile

正确性不是可用性的一部分

如果 10,000,000 个订单中有一行串租:

availability could still be 99.99999%
security and correctness are still failed

因此正确性使用 control signal:

pg36_shop_reconciliation_mismatches > 0

它需要:

  • invariant 是有界枚举;
  • reconciliation 有输入边界;
  • 结果有 hash 或不可变运行记录;
  • stale/missing 本身是控制失败;
  • 修复后由独立核对确认,而不是把 gauge 手工设为零。

第二层:请求在哪个入口发生了什么

入口层回答的是路径问题:

client
  -> application edge
      -> HAProxy service port
          -> PgBouncer pool
              -> PostgreSQL session

每个入口有不同的拒绝、排队和重试语义:

入口 关键问题 典型证据
application edge 请求是否 admission、是否重试、最终 outcome request counter、duration histogram、release id
HAProxy 选择了哪个 backend、健康检查如何判断 backend/session/queue、routing event
PgBouncer 等待 server connection 还是正在执行 client/server/pool state、wait duration
PostgreSQL backend 在执行、等待还是 idle in transaction activity、wait event、locks

“连接数高”可能代表:

healthy concurrency
idle application connections
pool wait
session leak
long transaction
blocked query fan-out
maintenance workers

如果没有入口身份与状态,单个总数无法区分。

用 operation class,避免用 endpoint 爆炸

应用指标需要能分段,但不能把完整 URL 做标签:

good:
  operation_class="place-order"
  operation_class="read-order"
  operation_class="reconcile-order"

bad:
  path="/tenant/123/order/9a7..."

operation class 是有界业务语义;原始 path 可能包含 tenant、order 和 token, 既造成 cardinality 爆炸,也造成数据泄露。

第三层:PostgreSQL 能否解释用户症状

数据库层分成当前状态与累计事实:

current
  pg_stat_activity
  pg_locks
  progress views
  pg_stat_replication

cumulative
  pg_stat_database
  pg_stat_io
  pg_stat_wal
  pg_stat_checkpointer
  pg_stat_archiver
  pg_stat_statements

它们回答:

  • 请求是否在 PostgreSQL 内;
  • backend 在 CPU 上运行还是等待;
  • 等待是 lock、I/O、client、WAL、buffer pin 还是其他类别;
  • transaction 已持续多久;
  • 哪个 queryid 消耗时间、I/O、临时块或 WAL;
  • checkpoint 写入和同步成本如何变化;
  • WAL 生成、发送、归档和回放是否推进;
  • vacuum/freeze 是否被阻止;
  • dead tuple、对象大小和统计新鲜度如何变化。

它们不直接回答:

  • 用户请求是不是 eligible;
  • 应用是否返回了正确结果;
  • 哪个 tenant 受到影响;
  • 用户是否走了 replica read path;
  • 恢复点是否被业务接受。

数据库信号是解释层,不是服务层的替代品。

current 与 cumulative 必须分开

下面两个事实可以同时成立:

pg_stat_activity now shows no lock wait
pg_stat_database deadlocks counter increased in the last hour

前者是“现在没有”,后者是“窗口内曾经发生”。不能因为 current view 已经恢复, 就否定累计异常;也不能因为累计 counter 非零,就宣称当前仍在发生。

第四层:主机是否构成资源约束

主机层观察:

  • CPU utilization、run queue、steal;
  • memory pressure、swap、OOM;
  • filesystem capacity、inode、mount state;
  • block-device latency、queue、throughput;
  • network loss、retransmit、bandwidth;
  • clock synchronization;
  • process、cgroup、systemd 和 kernel event。

主机指标要与 PostgreSQL 语义交叉:

high CPU
  + PostgreSQL active non-waiting backends
  + queryid execution time rises
  -> consistent with compute saturation

high CPU
  + user SLI healthy
  + batch window declared
  -> may be expected work

disk latency rises
  + pg_stat_io read_time rises
  + shared reads rise
  -> storage path becomes a stronger candidate

disk latency rises
  + PG reads unchanged
  -> investigate other processes or filesystem activity

主机指标很适合 falsify 假设。例如,若 PostgreSQL 认为 I/O 时间增加,而设备 层没有对应变化,可能是 OS cache、采集时间窗、虚拟化层或统计口径不同,而不是 直接得出“磁盘坏了”。

四层不是固定下钻顺序

通常从用户症状向下,但也有例外:

durability control
  archive stopped before user impact
  -> page is justified by imminent durability loss

metamonitoring
  rule evaluation failed
  -> health becomes unknown
  -> establish independent observation first

freeze-age horizon
  no current impact
  -> ticket before emergency anti-wraparound work

正确原则不是“永远只看用户”,而是:

page requires current user impact,
integrity/durability emergency,
or loss of the observation path.

原因和容量信号默认进入 diagnostic 或 ticket。

建立问题卡

每个新信号先填一张问题卡:

question: 已知 commit 是否在 replica read path 五秒内可见?
decision: 是否临时把 read-after-write journey 路由到 primary?
layer: user
source: commit-correlated synthetic probe
good: token visible within 5s
bad: token not visible within 5s
missing: unknown and probe-path failure
dimensions: [service, read_path, environment]
fallback: direct primary-path probe
owner: service
first_safe_action: preserve tokens and use primary path
verification: new tokens meet bound on declared path

若填不出 decision、missing 和 action,这个信号还不适合变成告警。

反例:从副本时间戳推用户新鲜度

常见快捷方式:

clock_timestamp() - pg_last_xact_replay_timestamp()

在一个空闲副本上,最近没有 WAL,它可能显示“很久以前”;但副本其实完全 追平。持续写入时,它又只能说明最后一次 replay 的时间,不知道用户关心的 commit 是否已经可见。

新鲜度需要:

write unique token
capture commit outcome
poll declared read path
measure until token visible

WAL distance 和 replay timestamp 仍有诊断价值,但不能替代 commit correlation。

25.1.2 指标、日志、事件和追踪各回答什么

metric:把总体变成可计算时间序列

metric 最适合:

  • event ratio;
  • latency distribution;
  • rate、increase 和 trend;
  • 容量 horizon;
  • 规则自动评估;
  • 多实例聚合。

四种常见类型:

类型 语义 PostgreSQL/Pigsty 示例 主要陷阱
counter 只增,进程/reset 后重置 transaction、deadlock、WAL byte 直接比较总值
gauge 可升可降的当前/最近值 connections、lag、object size 对瞬时噪声 page
histogram bucket counter + count/sum request duration bucket 不一致、聚合错误
summary 客户端计算 quantile 某些应用延迟 quantile 难以跨实例聚合

counter 要转成窗口

rate(pg_db_xact_total[5m])
increase(pg_archiver_failed_count[15m])

不要:

pg_archiver_failed_count > 0

除非语义明确要求“生命周期内从未失败”。大多数运行告警关心的是新失败与当前 推进,而不是历史存在。

gauge 需要稳定条件

pg36_shop_restore_evidence_age_seconds > 90 * 24 * 60 * 60

evidence age 是 gauge,但它不是“数据库坏了”。它是恢复控制未满足,应进入 change gate 或 ticket,除非业务政策另有紧急定义。

histogram 要从 bucket 算事件比率

延迟 SLO 目标是 99% 在 250 ms 内:

$$ \text{latency bad ratio}

1 - \frac{\text{rate}(\text{bucket}_{le=0.25})} {\text{rate}(\text{count})} $$

它与 p99 不完全等价。event-based SLO 直接计算“多少 eligible events 超标”; quantile 是分布位置,适合探索,但不一定能直接算错误预算。

metric contract 的最小字段

name
type and unit
producer
labels and cardinality bound
counter reset or gauge freshness
scrape interval
retention
missing semantics
query examples
owner
deprecation plan

没有 type,就不知道能否 rate();没有 reset,就不知道突然下降是改善还是 重启;没有 missing,就可能把 absence 变成健康。

log:保存离散上下文

日志适合回答:

  • 哪个错误类别发生;
  • 状态何时迁移;
  • lock wait 在 deadlock_timeout 后是否被记录;
  • 哪个 temporary file 被创建;
  • Patroni 何时改变角色;
  • pgBackRest 哪一步失败;
  • 配置 reload 或连接认证发生什么。

日志不适合作为默认总体统计:

scan all PostgreSQL logs
count matching strings
divide by all lines

因为:

  • 日志行不等于 eligible event;
  • multiline 和格式变化影响计数;
  • sampling 会丢事件;
  • rotation/retention 改变分母;
  • 查询成本随数据量增长;
  • 原始 SQL、参数和用户信息可能泄露。

structured log 仍然需要 schema

推荐将字段分为:

stable dimensions
  cluster / instance / database / severity / error_code

bounded correlation
  release / operation_class / incident_id

restricted payload
  message / statement / detail / parameter

restricted payload 不应自动复制到 alert annotation、ticket 或公开 evidence。 “JSON 格式”只解决解析,不解决敏感性。

event:把变化放进时间线

发布、切换、配置和容量变化最好是结构化 event:

{
  "event_id": "change-20260729-017",
  "kind": "application-release",
  "target": "pg36_shop/l2-sandbox",
  "actor_role": "shop-release-operator",
  "started_at": "...",
  "completed_at": "...",
  "result": "success",
  "rollback_ref": "..."
}

它能帮助回答:

symptom started at 10:02
release completed at 09:58
pool config changed at 10:01
failover did not occur

event 本身仍不证明因果。一个发布接近事故,只是强候选,需要机制和修复验证。

event 的 clock 与 identity

事件至少记录:

  • stable event id;
  • actor role,而不是随意字符串;
  • exact target;
  • UTC 时间和时间源;
  • request、approval、execution、result;
  • rollback/roll-forward reference;
  • source integrity。

若主机时钟相差两分钟,所谓“先发生”会被颠倒。诊断包要同时保存:

collector clock
database clock
rule evaluation clock

trace:跟随单次路径

trace 能展示:

edge span
  -> service span
      -> pool wait
          -> database call
              -> downstream service

它适合:

  • 一次请求在哪里耗时;
  • 重试和 fan-out 如何展开;
  • 哪个 dependency 返回错误;
  • sampled slow path 与正常 path 有何不同。

它不适合单独算完整 SLO:

  • 采样意味着不是全部事件;
  • tail sampling 会改变总体;
  • trace backend 丢失不能解释为“无慢请求”;
  • baggage 可能携带 tenant、token 或 PII;
  • 数据库 span 未必包含真实执行等待。

不要把 SQL 文本塞进 span

推荐:

db.system=postgresql
db.namespace=test
db.operation.name=SELECT
db.query.summary=read-order
db.queryid=<bounded hash identity if policy allows>

谨慎或禁止:

db.statement=<raw SQL with literals>
bind.parameters=<customer data>
connection.string=<credential>

queryid 也不是绝对安全身份:它是 hash,可能冲突;相同文本在不同 search_path 下语义也可能不同。它的价值是聚合和关联,不是授权或数据分类。

四种信号如何组合

一个 latency fast burn 的调查:

metric
  availability healthy, latency bad ratio burns

event
  pool size changed four minutes before onset

trace
  sampled requests spend time waiting for a server connection

log
  PgBouncer reports pool saturation but no auth errors

SQL
  PostgreSQL active sessions and query cost are stable

host
  CPU and storage are stable

这组证据支持“入口池排队”而不是“数据库执行变慢”。如果回滚池配置后:

  • queue 恢复;
  • trace pool wait 恢复;
  • user latency windows 恢复;
  • 没有 correctness mismatch;

因果证据才明显增强。

选择最便宜的充分信号

同一问题可能有多种来源:

count transaction commits
  pg_stat_database counter        cheap, aggregate
  parse every log line            expensive, context-rich
  trace every transaction         very expensive, sampled

如果只需要总体 rate,优先 counter;若要解释一类错误,再查询有界日志;若要 追单次跨服务路径,再使用 trace。不要因为存储便宜就永久收集一切。

25.1.3 标签基数、采样、保留与缺失数据

cardinality 是维度乘积

一个 metric 的 series 数近似为:

seriesi=1nlabeli×histogram buckets \text{series} \approx \prod_{i=1}^{n}\left|\text{label}_i\right| \times \text{histogram buckets}

假设:

service             20
environment         3
operation_class     30
status_class        6
release             10 retained concurrently
histogram bucket    15

则:

20×3×30×6×10×15=1,620,000 20 \times 3 \times 30 \times 6 \times 10 \times 15 = 1{,}620{,}000

如果再加:

tenant_id  100,000
order_id   unbounded

系统不仅会爆炸,还会把业务标识复制到监控存储。

bounded label allowlist

本章允许:

Pigsty identity
  cls / ins / ip

service identity
  service / operation_class / environment

bounded optional
  read_path / invariant / recovery_class / objective_id / release

禁止:

customer_id / tenant_id / order_id / idempotency_token
trace_id / raw_sql / raw query text / error_message
client_addr / password / token

release 虽然有界,也要限制同时保留多少版本;滚动发布若不断产生唯一 commit SHA,而旧 series 长期不消失,仍会增长。

Pigsty 的 pg_query_* 指标有一个需特别说明的例外:label 名为 query,值是 数值型 queryid,而不是 SQL 文本。它的上限受 pg_stat_statements.max 约束; 仍要结合 database,并禁止把 raw query text 填进同名 label。

label 与 annotation 的边界

label 用于:

  • series identity;
  • query aggregation;
  • alert grouping/routing;
  • silence matcher。

annotation 用于人读的说明,不参与 series identity。但 annotation 也不能放 秘密。一个安全模式:

labels:
  service: pg36_shop
  operation_class: place-order
  environment: production
  objective_id: SLO-AVAILABILITY

annotations:
  summary: availability budget burning
  runbook: RB-USER-SYMPTOM
  dashboard: dashboard://pg36-shop-slo

不要:

labels:
  order_id: 8f...
  query: SELECT ...
annotations:
  error: password authentication failed for ...

先测 cardinality,再加维度

增加 label 前回答:

  1. 值域是否有硬上限?
  2. 谁控制它?
  3. 每个值保留多久?
  4. query 与 dashboard 是否真实使用?
  5. alert 是否需要它来路由?
  6. 是否能在日志/trace 中按需查,而不进入 metric?
  7. 该字段是否是 PII、secret 或业务主键?

若只是“以后可能有用”,默认不加。

sampling 必须改变措辞

日志或 trace 被采样后,允许说:

sampled slow requests show pool wait

不允许说:

all slow requests are caused by pool wait

采样合同至少包括:

  • head、tail 或 probabilistic;
  • sample rate;
  • error 是否强制保留;
  • rate 随流量是否变化;
  • dropped count;
  • decision point;
  • 是否能重建总体;
  • 变更历史。

PostgreSQL log_min_duration_samplelog_statement_sample_rate 也是采样政策。 如果前者关闭,后者值为 1 并不代表所有语句都会被采样记录。

retention 决定能否回答窗口问题

若 SLO 是 rolling 28d,而原始 SLI 只保留 7d:

cannot recompute 28d objective from raw data

可以使用长期 recording rule,但必须记录:

  • 原始数据保留多久;
  • 聚合数据保留多久;
  • rule expression 的版本;
  • label 是否在聚合时丢失;
  • reset/缺口如何进入结果;
  • 回填是否发生。

诊断与治理保留期也不同:

数据 典型目的 关注点
高频 raw metric 近期诊断 容量大、粒度高
recording rule 长期趋势/SLO 语义版本
log body 事件上下文 敏感、访问、删除
trace sampled path 成本与 PII
incident evidence 决策与复盘 完整性、最小化

“永久保存以备万一”通常同时违反成本和隐私原则。

缺失数据至少有七种解释

1. 真实没有事件
2. producer 没有初始化零 series
3. exporter 失败
4. scrape/ingestion 延迟
5. storage/query 失败
6. label 或 metric rename
7. service / instance 已按计划退役

还有 counter reset、staleness marker 和查询窗口不足。

absent() 不是万能答案

判断 SLI missing 需要一个独立期望:

pg36_expected_service_traffic == 1
unless on (service, environment)
pg36_sli_sample_fresh == 1

如果服务夜间没有流量,request counter 没新样本不一定故障。可以用:

  • 独立 synthetic probe;
  • admission counter;
  • deployment/inventory declaration;
  • scrape target freshness;
  • expected schedule。

关键是不能用被监控对象自身同时证明“应该有数据”和“数据存在”。

zero、empty、NaN、stale 与 error

结果 含义
0 series 存在,当前值或计算结果为零
empty vector selector 没匹配 series
NaN 运算未定义,例如 0/0
stale 时序被标记过期
query error 规则没有获得结果

把 empty vector 用 or vector(0) 填零可能很危险:

bad_ratio or vector(0)

如果 bad ratio 消失是 exporter 故障,这会把“未知”变成“100% 健康”。只有在 零值语义、identity join 与独立 freshness 都被证明时,才能安全补零。

低流量与分母

本章记录规则没有用一个任意常数把分母抬高:

bad_rate / total_rate

低流量时要显式决策:

  • 使用更长窗口;
  • 同时要求 minimum event count;
  • 使用 synthetic probe;
  • 保持 unknown;
  • 用 ticket 而不是 page;
  • 聚合到更稳定的 operation class。

若写:

bad_rate / clamp_min(total_rate, 1)

每秒不到一个请求时,会改变真实 event ratio。clamp_min 可以防数值问题, 不能免费替代低流量政策。

counter reset

对 counter 使用 rate()/increase() 可以处理正常 reset,但仍要观察:

  • reset 是否过于频繁;
  • target identity 是否变化;
  • scrape window 是否跨越长缺口;
  • process restart 是否是事故的一部分;
  • 历史 recording rule 是否有断点。

PostgreSQL 统计也有 reset:

pg_stat_* stats_reset
pg_stat_statements_info.stats_reset
per-row stats_since
minmax_stats_since

数值突然变小,必须先问 reset,而不是直接说“负载下降”。

监控系统也要被监控

至少观察:

  • exporter/scrape freshness;
  • VictoriaMetrics query 与 ingestion;
  • VMAlert group/rule error;
  • missed evaluation;
  • Alertmanager notification failure;
  • 独立 canary 的 receipt;
  • external blackbox。

本章现场快照显示:

VMAlert rule errors                  0
VMAlert missed iterations nonzero   0
Alertmanager notification failures  0
current alerts                      0

这只能证明计数器当前没有记录失败。没有一条最近被 receiver 确认收到的 canary, 不能宣称真实通知链可达。

current lab cardinality snapshot

同一快照中:

VictoriaMetrics total series            44,842
total label-value pairs                 387,724
distinct metric names                   3,078
vmalert recording-rule series           698

这些是容量基线,不是目标上限。应当记录随时间的:

series growth rate
top metric by series
top label by values
unused/high-cost metric
query latency and cache pressure
retention impact

一个新 exporter 可能“工作正常”,但在一周内把 series 增长十倍。

信号引入评审

上线前用下面的表:

问题 必须通过
question 能写成一个可证伪问题
decision 观察结果会改变具体决定
source producer 和采集路径明确
type counter/gauge/histogram/event/log/trace 明确
labels 有界、必要、无 secret/PII
time scrape、window、delay、reset 明确
missing 不会默认为健康
cost series、bytes、query、retention 有预算
access 最小权限与脱敏明确
action owner、首个安全动作、停止线明确
test 正常、异常、缺失、恢复均可重放
retirement rename/deprecation 有迁移方案

本节验收

你应当能够对任意一个候选指标回答:

它服务于哪一个决定?
属于用户、入口、数据库还是主机层?
它是症状、原因、控制还是 metamonitoring?
数值是 current、counter、window、estimate 还是 progress?
哪些 label 有界,哪些绝对不能进入?
缺失、reset、NaN 与低流量分别怎么处理?
采集和保留成本是多少?
谁看到它后可以安全地做什么?
哪个独立事实可以复核?

如果只能回答“Grafana 上有这条线”,信号合同还没有建立。


返回本章目录 · 下一节:PostgreSQL 核心运行信号 · 查看全书目录 · 查看索引中心

25.2 PostgreSQL 核心运行信号

PostgreSQL 自带两类观察接口:

dynamic current state
  当前 backend、锁、复制、进度

cumulative statistics
  自 reset 以来的 transaction、I/O、WAL、maintenance、statement

查询视图很简单,正确解释并不简单。以下事实可以同时成立:

pg_stat_activity 当前没有 lock wait
过去五分钟 lock wait 曾导致用户超时

pg_stat_archiver.failed_count = 21
当前归档已经恢复并持续成功

pg_stat_io.read_time 增加
物理磁盘没有等量读取,因为 OS page cache 参与

replica WAL distance = 0
应用仍可能因为路由、事务快照或缓存读到旧结果

n_dead_tup = 0
表仍可能存在已分配但未归还给操作系统的空间

本节目标不是记住所有列,而是掌握一套读法:

view
  -> source and update path
      -> current/cumulative/estimate/progress
          -> reset and snapshot
              -> independent corroboration
                  -> safe action

完整视图以当前版本官方文档为准: PostgreSQL 18 Monitoring Stats

25.2.1 会话、事务、等待与锁

先确认统计功能是否开启

关键设置:

SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN (
  'track_activities',
  'track_counts',
  'track_functions',
  'track_io_timing',
  'track_wal_io_timing',
  'stats_fetch_consistency'
)
ORDER BY name;

它们不是同一个开关:

设置 作用 关闭后的含义
track_activities 当前命令与开始时间 activity 信息受限
track_counts 数据库/表等累计活动 autovacuum 也依赖它
track_functions 函数调用统计 不代表函数没有运行
track_io_timing 数据文件 I/O timing 时间未测量,不是零成本
track_wal_io_timing WAL I/O timing WAL 时间未测量
stats_fetch_consistency 一个事务内统计读取一致性 影响缓存/快照行为

统计有开销,timing 尤其依赖平台时钟成本;但关闭后必须把“未测量”保留下来。 绝不能把:

track_wal_io_timing=off
wal_write_time=0

解释为 WAL 写入没有花时间。

本章沙箱:

track_activities       on
track_counts           on
track_functions        all
track_io_timing        on
track_wal_io_timing    off
stats_fetch_consistency cache

pg_stat_activity 是当前 backend 视图

先使用不导出 query text 的聚合:

SELECT
  backend_type,
  state,
  wait_event_type,
  count(*) AS sessions,
  max(clock_timestamp() - xact_start)
    FILTER (WHERE xact_start IS NOT NULL) AS max_xact_age,
  max(clock_timestamp() - query_start)
    FILTER (WHERE query_start IS NOT NULL) AS max_query_age
FROM pg_stat_activity
GROUP BY backend_type, state, wait_event_type
ORDER BY backend_type, state NULLS LAST, wait_event_type NULLS LAST;

为什么带 backend_type?PostgreSQL 18 里不只有 client backend:

autovacuum launcher / worker
background writer
checkpointer
walwriter / walsender / walreceiver
io worker
slotsync worker
logical replication worker

如果把后台进程与 client backend 混在一起,某个长期运行的 background worker 可能被误判为“用户 SQL 运行几小时”。

statewait_event 独立

常见错误:

state = active
  therefore CPU is executing

实际:

state=active, wait_event is null
  -> backend 正在运行,或刚好未被采样到等待

state=active, wait_event is not null
  -> SQL 仍是 active,但正在等待

state=idle, wait_event_type=Client
  -> 等客户端发下一条命令

state=idle in transaction
  -> 事务仍开着,可能保留 snapshot/lock/xmin

所以等待查询写成:

SELECT
  pid,
  backend_type,
  state,
  wait_event_type,
  wait_event,
  clock_timestamp() - xact_start AS xact_age,
  clock_timestamp() - query_start AS query_age
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND state = 'active'
  AND wait_event IS NOT NULL
ORDER BY query_start;

这条查询没有读取 query 列。需要 SQL 上下文时,应在受限交互会话中按 queryid、application、database 和 owner 缩小范围,避免把全文复制进工单。

wait event 是“正在等什么”,不是“根因”

wait_event_type 先把等待分大类:

Lock
LWLock
IO
Client
IPC
Activity
Timeout
BufferPin
Extension

同一种等待可能有多种机制:

Lock
  application transaction contention
  DDL conflicting with queries
  idle in transaction retaining locks

IO
  cache miss
  sequential scan
  checkpoint-related work
  WAL read/write
  extension access

Client
  server waits for client
  slow consumer
  application not reading results

因此:

wait type
  -> affected backend/queryid
  -> blocker/resource
  -> user path
  -> corroborating counter/host evidence

才形成诊断。

PostgreSQL 18 的异步 I/O 引入 io worker 等 backend type 和相应等待。升级后 不要假设旧版 wait event 列表仍完整;dashboard 和规则要按当前版本校验。

当前统计在一个事务里可能保持不变

累计统计不是每次访问都无条件读取最新值。PostgreSQL 会把统计写入共享内存, 各进程最迟按一定节奏 flush;访问者又可能在当前事务内缓存读取结果。

这段会造成困惑:

BEGIN;
SELECT xact_commit FROM pg_stat_database WHERE datname = current_database();
-- 等待或在其他连接产生工作
SELECT xact_commit FROM pg_stat_database WHERE datname = current_database();
COMMIT;

stats_fetch_consistency=cache 下,同一事务后续读取可能继续看到缓存值。 诊断时优先:

每次采样使用短事务
不要在长事务里刷新 dashboard 数据
必要时调用 pg_stat_clear_snapshot()
记录采样时间和 stats_fetch_consistency

pg_stat_clear_snapshot() 清的是当前 session 的统计 snapshot,不是重置全局 统计;不要与 pg_stat_reset* 混淆。

统计更新也有时间边界

累计统计通常在 transaction 完成后才反映:

active transaction
  current activity can show it
  cumulative table/database changes may not yet be flushed

因此调查进行中的大事务:

  • activity 看当前 transaction age;
  • locks 看当前持有/等待;
  • progress 看支持的维护动作;
  • WAL/IO counter 看累计变化;
  • 不等待累计表统计“先证明它存在”。

crash、恢复与复制会改变统计历史

PostgreSQL 正常关闭会保存累计统计;非正常关闭、从 base backup 恢复或 PITR 可能导致统计 reset。跨 failover 比较时:

same metric name
  does not imply same counter history

必须同时保存:

  • member/timeline;
  • stats reset;
  • postmaster start;
  • role transition;
  • source instance;
  • sampling window。

long query 与 long transaction 不同

query_age = now - query_start
xact_age  = now - xact_start

场景:

状态 query age xact age 风险
active long query 约等于或短于 xact 执行/等待资源
idle in transaction 当前 query 已结束 lock、xmin、vacuum
active in old transaction 当前 query 短 很长 snapshot/业务批次
idle 上条 query 的开始时间不代表在执行 无 transaction 通常只是连接

因此不要用 query_start 对所有 state 排序然后自动 cancel。

idle in transaction 为什么危险

它可能:

  • 保留 row/table lock;
  • 持有旧 snapshot;
  • 阻碍 dead tuple 回收;
  • 拉长 backend_xmin
  • 占用 connection/pool slot;
  • 让后续应用错误更难定位。

诊断字段:

SELECT
  pid,
  datname,
  usename,
  application_name,
  state,
  clock_timestamp() - xact_start AS xact_age,
  wait_event_type,
  wait_event,
  backend_xid,
  backend_xmin
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND state = 'idle in transaction'
ORDER BY xact_start;

取消或终止是变更动作,不属于本章 L0 采集。先确认:

  • owner/application;
  • transaction 是否仍有不可重试副作用;
  • pool mode;
  • unknown commit outcome;
  • cancel 与 terminate 的差异;
  • rollback/重连影响;
  • 用户症状是否关联。

pg_locks 是锁申请,不是完整业务解释

安全聚合:

SELECT locktype, mode, granted, count(*) AS locks
FROM pg_locks
GROUP BY locktype, mode, granted
ORDER BY locktype, mode, granted;

找 blocker 可使用 pg_blocking_pids()

SELECT
  a.pid AS waiting_pid,
  a.datname,
  a.usename,
  a.application_name,
  a.wait_event_type,
  a.wait_event,
  clock_timestamp() - a.query_start AS wait_age,
  pg_blocking_pids(a.pid) AS blocking_pids
FROM pg_stat_activity AS a
WHERE cardinality(pg_blocking_pids(a.pid)) > 0
ORDER BY a.query_start;

这个函数给出 blocker PID,但仍要判断:

direct blocker or blocker behind blocker?
transaction or prepared transaction?
DDL, row lock, advisory lock, relation extension?
which user journey?
is blocker making progress?
what is safe to cancel?

锁图而不是最长列表

事故中更有用的是:

waiting backend
  -> direct blocker
      -> root blocker
          -> owner / transaction age / state

并保存:

  • edge 采样时刻;
  • blocker state;
  • backend_xid/xmin
  • queryid,而不是默认 query text;
  • application/release;
  • lock type/mode;
  • user impact。

一条锁边可能瞬间消失。诊断包应保存有界快照,而不是事后只看当前视图。

deadlock 与普通阻塞

普通 lock wait 可以持续;deadlock 是一个等待环,PostgreSQL 会检测并中止其中 一个 transaction。

观察:

pg_stat_database.deadlocks       cumulative
log_lock_waits                   waits beyond deadlock_timeout
deadlock error log               discrete event
application retry/outcome        user semantics

log_lock_waits=on 只在等待超过 deadlock_timeout 后记录,短等待不会出现。 日志“没有 lock wait”不能证明没有短暂锁竞争。

权限边界

普通用户只能看到其他 session 的有限信息。pg_read_all_stats 能读取全库统计和 其他 session 的更多信息,但这仍然是高敏感可观测权限:

  • query text 可能含业务值;
  • application name 可能带身份;
  • client address 暴露拓扑;
  • activity 能推断业务行为。

建议:

exporter role
  stable, narrow, machine-only

interactive diagnostic role
  time-bounded, reviewed, pg_read_all_stats or narrower

evidence export
  aggregate and redact, no query text/client address

不要因为它不是 superuser 就把它当低风险权限。

查询本身也会进入观察结果

读取 pg_stat_activity 时,你自己的查询也是 active;访问许多系统视图也会拿 AccessShareLock。本章正式快照出现的 relation locks 就包括采集查询本身。

因此:

  • 标识 collector application;
  • 从结果中区分自身;
  • 限制 statement timeout;
  • 不在 tight loop 高频轮询;
  • 避免一次展开所有 query text;
  • 将 observer effect 写进证据。

25.2.2 缓冲、I/O、WAL、检查点与复制

数据路径不是“内存或磁盘”二选一

一个 PostgreSQL page 读取可能经过:

PostgreSQL shared buffers
  -> operating-system page cache
      -> filesystem / block layer
          -> physical or virtual storage

所以:

PostgreSQL read()
  may be satisfied by OS cache

shared buffer hit
  does not require an OS read

device read
  may be caused by another process

pg_stat_io 明确不区分物理磁盘和 OS page cache。必须结合 node/block-device 证据。

pg_stat_database 给出数据库级累计轮廓

常用字段:

SELECT
  datname,
  numbackends,
  xact_commit,
  xact_rollback,
  blks_read,
  blks_hit,
  temp_files,
  temp_bytes,
  deadlocks,
  blk_read_time,
  blk_write_time,
  stats_reset
FROM pg_stat_database
WHERE datname IS NOT NULL
ORDER BY datname;

它适合:

  • database-level rate;
  • hit/read 变化;
  • temp spill 趋势;
  • rollback/deadlock 变化;
  • reset-aware baseline。

不适合:

  • 归因到具体 query;
  • 直接推物理 IOPS;
  • blks_hit / (blks_hit + blks_read) 单独判断内存是否足够;
  • 比较不同 reset 区间的裸总数。

cache hit ratio 不是性能分数

$$ \text{hit ratio}

\frac{\Delta hits} {\Delta hits + \Delta reads} $$

即使正确用窗口增量,也受 workload 影响:

  • 大表顺序扫描天然产生 reads;
  • 小表热点容易高命中;
  • OS cache 命中仍记为 PostgreSQL read;
  • 低 hit 可能是合理批处理;
  • 高 hit 不代表 CPU、lock 或 plan 健康。

把它作为 workload 特征,不要设一个跨服务的“低于 99% 就 page”。

pg_stat_io 按谁、什么对象、什么上下文拆分

PostgreSQL 18 可按:

backend_type
object
context

观察:

reads / read_bytes / read_time
writes / write_bytes / write_time
extends / extend_bytes / extend_time
hits / evictions / reuses
fsyncs / fsync_time

先聚合非零工作:

SELECT
  backend_type,
  object,
  context,
  sum(reads) AS reads,
  sum(read_bytes) AS read_bytes,
  sum(read_time) AS read_ms,
  sum(writes) AS writes,
  sum(write_bytes) AS write_bytes,
  sum(write_time) AS write_ms,
  sum(fsyncs) AS fsyncs,
  sum(fsync_time) AS fsync_ms
FROM pg_stat_io
GROUP BY backend_type, object, context
HAVING
  coalesce(sum(reads), 0)
  + coalesce(sum(writes), 0)
  + coalesce(sum(fsyncs), 0) > 0
ORDER BY backend_type, object, context;

窗口诊断需要两次快照求 delta 或 exporter counter rate。裸累计值只说明 reset 以来总量。

timing 要与 byte/count 一起读

read_time rises, read_bytes rises
  -> more work or slower work

read_time per read rises
  -> average operation cost rises, but distribution unknown

read_bytes rises, device reads stable
  -> OS cache may serve more reads

timing is zero and tracking is off
  -> unknown, not free

平均值会掩盖 tail。需要时结合:

  • block-device latency histogram;
  • request latency histogram;
  • trace sampled tail;
  • queryid-level I/O;
  • workload mix。

buffer、backend 与 checkpoint 写入

dirty buffer 可能由 backend、background writer、checkpointer 等路径写出。 pg_stat_io 帮助按 backend type 区分;pg_stat_checkpointer 给 checkpoint 累计结果。

PostgreSQL 18:

SELECT *
FROM pg_stat_checkpointer;

重要字段包括:

num_timed
num_requested
num_done
restartpoints_*
write_time
sync_time
buffers_written
slru_written
stats_reset

解释:

requested checkpoint rate rises
  -> workload/config/administrative events may force checkpoints

write_time dominates
  -> checkpoint spreads writes

sync_time spikes
  -> fsync phase or storage path needs correlation

buffers_written rises
  -> more dirty data, not automatically a problem

不要只看一次 write_time 总值。计算每窗口的 checkpoint count、buffer、write 和 sync delta,再与用户 latency、WAL rate 和 host I/O 对齐。

checkpoint 不是越少越好

过频可能增加写入压力和 full-page image;过稀可能:

  • 增加 crash recovery 时间;
  • 需要更多 WAL;
  • 在 checkpoint 集中更多工作;
  • 改变恢复和容量特征。

任何调参要结合:

checkpoint completion
WAL generation
dirty buffer path
storage latency
recovery objective
memory and workload

本章只观察,不修改 checkpoint_timeoutmax_wal_size 或 completion target。

pg_stat_wal 是 WAL 生成轮廓

SELECT
  wal_records,
  wal_fpi,
  wal_bytes,
  wal_buffers_full,
  stats_reset
FROM pg_stat_wal;

用途:

  • WAL byte rate;
  • full-page image 比例变化;
  • WAL buffer full;
  • 与 write workload、checkpoint、replication、archive 对齐。

不能直接说明:

  • 哪个 query 产生 WAL;
  • WAL 是否已归档;
  • replica 是否已回放;
  • recovery 能否成功。

query 归因需要 pg_stat_statements.wal_bytes 等;归档与恢复需要另外的证据。

full-page image 的上下文

checkpoint 后 page 首次修改可能记录 full-page image,以支持恢复。wal_fpi 变化与:

  • checkpoint 频率;
  • 工作集;
  • full_page_writes
  • page 修改模式;
  • compression;
  • backup/recovery policy

相关。不要看到 FPI 高就关闭安全机制。

pg_stat_archiver 同时有 counter 和最后事件

SELECT
  archived_count,
  last_archived_wal,
  last_archived_time,
  failed_count,
  last_failed_wal,
  last_failed_time,
  stats_reset
FROM pg_stat_archiver;

三种不同问题:

历史是否失败过?
  failed_count since reset

最近是否出现新失败?
  increase(failed_count[window])

当前是否推进?
  last success age + WAL generation + archive queue

沙箱快照:

failed_count           21
last_failed_time       18:57:58Z
last_archived_time     22:27:54Z

最近成功晚于失败,说明历史 counter 非零不能证明当前仍失败。

候选告警:

pg36:archive_failures:increase15m > 0
and on (cls, ins, ip)
(time() - pg_archiver_finish_time) > 900

这仍只是 recovery risk candidate。下一步要复核:

  • 当前 WAL 是否继续生成;
  • archive command/pgBackRest 状态;
  • repository;
  • latest archive;
  • backup/WAL coverage;
  • 最近 restore drill。

“归档恢复”不等于“恢复就绪”。

replication 有位置、时间、状态三套语义

primary:

SELECT
  application_name,
  state,
  sync_state,
  sent_lsn,
  write_lsn,
  flush_lsn,
  replay_lsn,
  pg_wal_lsn_diff(sent_lsn, replay_lsn) AS sent_replay_gap_bytes,
  write_lag,
  flush_lag,
  replay_lag
FROM pg_stat_replication
ORDER BY application_name;

replica:

SELECT
  status,
  sender_host,
  sender_port,
  written_lsn,
  flushed_lsn,
  latest_end_lsn,
  last_msg_send_time,
  last_msg_receipt_time,
  latest_end_time
FROM pg_stat_wal_receiver;

公开/长期证据不一定应保存 sender/client address;可以保留 member identity、 state、sync_state 和位置差。

位置 gap

sent - write    network/receiver write path
write - flush   replica flush path
flush - replay  replay/apply path

位置差按 WAL byte 计,不是秒:

catch-up timeWAL gap bytescurrent generation rate \text{catch-up time} \ne \frac{\text{WAL gap bytes}}{\text{current generation rate}}

因为 replay throughput、workload、conflict、I/O 和 future WAL 都会变化。它可 用于容量与趋势,不应直接承诺 RTO。

时间 lag 可能是 NULL

write_lagflush_lagreplay_lag 是最近同步交互产生的测量,并不保证 持续给出“当前落后秒数”。空闲系统可能为 NULL;这不等于零,也不等于故障。

使用:

  • LSN distance;
  • connection/state;
  • last message;
  • known commit probe;
  • workload/WAL generation

共同解释。

本章正式 SQL 快照中,两条 streaming async replica 的 LSN gap 都为 0,而 lag interval 为 NULL。这正好说明:

NULL time lag
  can coexist with zero position gap in an idle/current sample

sync_state 与 commit durability

sync_state=async 表示它不是当前同步确认的一部分。即使 gap 为零:

  • 后续 commit 仍可能尚未复制;
  • primary 立即丢失会有 write gap 风险;
  • 应用 commit acknowledgment 语义取决于 synchronous_commit 和配置;
  • user freshness 取决于读取路径。

pg_stat_replication 与第 20 章 HA 合同一起读,不要从瞬时 gap 倒推出 durability guarantee。

replication slot 与 WAL retained risk

除了 replica gap,还要观察:

SELECT
  slot_name,
  slot_type,
  active,
  wal_status,
  safe_wal_size,
  pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
    AS retained_bytes
FROM pg_replication_slots
ORDER BY slot_name;

slot identity 可能属于 extension/consumer;不要自动删除 inactive slot。风险:

  • WAL retention 填满磁盘;
  • logical consumer 落后;
  • slot 失效;
  • consumer 被误认作废弃。

容量规则默认 ticket,动作需 owner 和 consumer 证据。

复制冲突

hot standby 查询可能与 replay 冲突。观察:

  • pg_stat_database_conflicts
  • replica activity;
  • query cancellation log;
  • feedback/delay 设置;
  • retained xmin/WAL;
  • user read path。

降低冲突的设置可能增加 bloat 或 freshness lag,不能只优化一张图。

25.2.3 vacuum、冻结、膨胀与对象增长

vacuum 有四个主要目的

常规 vacuum 不是“清空表”,而是:

  1. 回收 dead row version,使空间可在表内复用;
  2. 更新 visibility map,支持 index-only scan 等;
  3. 防止 transaction ID wraparound;
  4. 维护统计/冻结等运行状态。

ANALYZE 更新 planner statistics;它与 vacuum 可以一起运行,但目的不同。

autovacuum 依赖统计

track_counts=on 不只是“多收指标”。autovacuum 使用累计活动决定何时处理表。 关闭它会影响维护机制。

每表触发近似由:

vacuum threshold
  base threshold + scale factor * relation tuples

analyze threshold
  base threshold + scale factor * relation tuples

决定,并受 insert threshold、per-table storage parameter、cost limit、worker 数、 全局设置等影响。

不能只看“autovacuum process 存在”。要看:

  • table change pressure;
  • last vacuum/autovacuum;
  • dead tuples;
  • analyze freshness;
  • blockers;
  • progress;
  • freeze age;
  • runtime/duration。

表级统计是 estimate + counter

SELECT
  schemaname,
  relname,
  n_live_tup,
  n_dead_tup,
  n_mod_since_analyze,
  n_ins_since_vacuum,
  last_vacuum,
  last_autovacuum,
  last_analyze,
  last_autoanalyze,
  vacuum_count,
  autovacuum_count,
  analyze_count,
  autoanalyze_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC NULLS LAST
LIMIT 50;

n_live_tupn_dead_tup 是估算;count 是自 reset 累计。它们适合找候选,不 适合直接计算“精确 bloat 百分比”。

dead tuple 不等于 bloat

dead tuple
  an old row version no longer visible to current snapshots

reusable free space
  vacuum processed space available for future tuples

table bloat
  allocated pages exceed what current data/layout needs

filesystem size
  file blocks currently allocated

vacuum 后 n_dead_tup 可能下降,但文件通常不缩小,因为普通 vacuum 将空间 留给表内复用。需要归还操作系统的操作通常更重,涉及 rewrite/lock/extra disk; 不能因为文件没缩就说 vacuum 失败。

长事务如何阻止回收

MVCC 需要保留旧版本给仍可能看见它的 snapshot。长 transaction、prepared transaction、replication slot/feedback 等可能拉住 xmin:

updates/deletes create dead versions
  -> vacuum evaluates global visibility horizon
      -> old xmin still needs them
          -> cannot remove
              -> dead tuples/object size grow

观察:

SELECT
  pid,
  datname,
  usename,
  application_name,
  state,
  backend_xid,
  backend_xmin,
  clock_timestamp() - xact_start AS xact_age
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;

还要查:

  • prepared transactions;
  • replication slots;
  • replica feedback;
  • vacuum progress;
  • table-level age。

“找到最老 PID 就 terminate”不是安全策略。

freeze age 是剩余空间,不是普通 latency

transaction ID 是有限循环空间。旧 tuple 必须被 freeze,避免 wraparound 后 可见性灾难。

数据库级:

SELECT
  datname,
  age(datfrozenxid) AS xid_age,
  mxid_age(datminmxid) AS multixact_age
FROM pg_database
ORDER BY xid_age DESC;

表级:

SELECT
  n.nspname,
  c.relname,
  age(c.relfrozenxid) AS xid_age,
  mxid_age(c.relminmxid) AS multixact_age,
  pg_total_relation_size(c.oid) AS total_bytes
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'm')
  AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY xid_age DESC
LIMIT 50;

不要把阈值写成与配置无关的魔法数字。应比较:

current age
autovacuum_freeze_max_age
table override
consumption rate
vacuum throughput
blocker
remaining time with uncertainty

本章候选 PG36FreezeAgeHorizon 使用固定数字只是 isolated lab 的测试输入, 明确标为 proposed;生产规则应由当前配置和容量政策生成。

anti-wraparound autovacuum 的特殊性

为防 wraparound 启动的 autovacuum 通常不会像普通 autovacuum 那样轻易被冲突 动作自动打断。不要把它当“可以随时 kill 的后台噪声”。

如果已经进入紧急区:

  • 停止增加风险的长事务;
  • 找出不能推进的表与 blocker;
  • 评估 I/O/空间/锁;
  • 按 runbook 控制维护;
  • 不并行执行未经评估的 rewrite;
  • 保留 evidence。

最优策略是在容量 horizon 阶段用 ticket 解决,而不是等 emergency page。

progress view 是当前进度,不是历史

vacuum:

SELECT
  pid,
  datid,
  relid,
  phase,
  heap_blks_total,
  heap_blks_scanned,
  heap_blks_vacuumed,
  index_vacuum_count,
  num_dead_item_ids,
  max_dead_item_ids
FROM pg_stat_progress_vacuum;

PostgreSQL 还为:

  • ANALYZE
  • CREATE INDEX / REINDEX
  • CLUSTER / VACUUM FULL
  • COPY
  • base backup

提供相应 progress view,具体列按版本文档。

没有行只表示当前没有该动作被报告,不表示:

  • 从未运行;
  • 上次成功;
  • 下一次会成功;
  • 没有被瞬间启动后失败。

历史需要日志、事件和累计 count。

progress 百分比可能不单调

不同 phase 使用不同总量;并行、索引清理和 dead item cycle 也会改变解释。 不要把:

heap_blks_scanned / heap_blks_total

当成整个 vacuum 的精确完成百分比。应同时显示 phase 和相关量。

analyze freshness

planner statistics 变旧会导致估算偏差。候选信号:

n_mod_since_analyze
last_analyze / last_autoanalyze
table size
query plan estimate vs actual
statistics target
column distribution

n_mod_since_analyze 是估算/累计线索,不是每行精确 change counter。对热点、 分区和高度偏斜列,需要 workload-aware 策略。

对象增长要拆分

SELECT
  n.nspname,
  c.relname,
  pg_relation_size(c.oid) AS heap_bytes,
  pg_indexes_size(c.oid) AS index_bytes,
  pg_total_relation_size(c.oid) AS total_bytes
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'm')
  AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY total_bytes DESC
LIMIT 50;

增长可能来自:

  • 正常业务数据;
  • index 数量;
  • TOAST;
  • dead/reusable space;
  • fillfactor;
  • 分区保留;
  • 临时/中间对象;
  • rewrite;
  • 失控 batch。

不要只按总大小 page。需要:

growth rate
retention expectation
free-space horizon
maintenance/rewrite headroom
business volume
owner plan

容量是预测和计划问题,默认 ticket。

partition 会改变聚合方式

父表和各分区的:

  • size;
  • table stats;
  • autovacuum;
  • analyze;
  • index;
  • freeze age

需要分别观察,再按业务分区策略聚合。只看父表可能近乎空;只列每个分区又会 产生巨大 cardinality。

推荐:

metric
  aggregate by parent / age band / size band

dashboard
  top-N and drill-down

SQL
  on-demand exact partition inventory

不要把每个临时分区名永久做成高基数 alert label。

bloat estimate 的边界

extension 或 SQL 估算 bloat 常依赖:

  • row width;
  • null bitmap;
  • alignment;
  • fillfactor;
  • statistics;
  • page sample;
  • index type。

它是排序候选,不是字节级财务账。要做重操作前:

  1. 复核对象大小与增长趋势;
  2. 判断空间能否复用;
  3. 找出生成机制和 blocker;
  4. 评估 rewrite/lock/replication/WAL/backup 影响;
  5. 准备额外磁盘和回退/前滚;
  6. 在维护窗口验证。

本章不会自动 VACUUM FULLREINDEX

把三类维护信号分开

类别 问题 典型动作
运行正确性 wraparound 是否接近 高优先级维护/停止风险来源
性能卫生 dead tuple、stats 是否影响 workload vacuum/analyze 调整与 blocker 修复
容量 对象/索引/WAL 是否耗尽空间 retention、扩容、结构优化、rewrite 计划

它们的 severity、owner 和时间尺度不同。一个“表大”告警不能同时代表三者。

沙箱快照如何读

正式采集在 pg-test-1 观察到:

user tables             1
estimated dead tuples   0
max table freeze age    1,050

这不是“vacuum 永远健康”的证明:

  • synthetic workload 很小;
  • 只有一次瞬时快照;
  • 没有长期增长率;
  • 没有生产 transaction rate;
  • 没有生产配置与 margin;
  • estimate 可能变化。

可以得出的结论只有:

at captured_at,
the declared sandbox target reported this small current baseline.

PostgreSQL 信号最小关联表

用户症状 PG 入口 原生证据 外部复核
latency active/wait activity、locks、queryid pool、host、trace
errors rollback/deadlock database counter、logs app outcome
stale read replica path replication position/state commit token probe
write latency WAL/checkpoint wal、checkpointer、I/O device、app histogram
query spill temp bytes database、statement、logs plan/work_mem policy
vacuum delay old xmin activity、table stats、progress workload/change event
freeze risk XID age database/class age rate/horizon/capacity
recovery risk archive archiver + WAL pgBackRest + restore drill

本节验收

你应当能解释:

  1. active 为什么仍可能在等待;
  2. 同一 transaction 为什么可能读到缓存的累计统计;
  3. track_wal_io_timing=off 为什么不能把时间解释为零;
  4. pg_stat_io 为什么不等于物理磁盘 I/O;
  5. failed_count>0 为什么不等于当前归档失败;
  6. time lag NULL 与 WAL gap 0 为什么可以同时出现;
  7. WAL gap 0 为什么不能证明用户新鲜度;
  8. n_dead_tup 为什么不等于精确 bloat;
  9. 普通 vacuum 为什么通常不缩小文件;
  10. progress view 没有行为什么不证明历史成功;
  11. query、lock、vacuum 和 freeze 信号分别对应哪种动作;
  12. 任何取消、终止、reset、vacuum 或配置变更为什么不属于 L0 观察。

上一节:从问题选择可观测信号 · 返回本章目录 · 下一节:SQL 可观测基线 · 查看全书目录 · 查看索引中心

25.3 SQL 可观测基线

数据库“忙”只是结果,SQL workload 才是来源之一。

要回答:

哪类语句消耗了时间?
调用量还是单次成本改变?
时间花在执行、计划、I/O、JIT、WAL 还是临时块?
慢是一直慢,还是 tail 中少量异常?
统计从什么时候开始?
query identity 是否稳定?
日志和计划采集付出了多少额外成本?
证据是否泄露业务值?

需要组合三个接口:

pg_stat_statements
  聚合 workload population

bounded logging
  离散慢语句、错误、锁等待、临时文件

auto_explain
  对满足政策的一部分执行自动记录计划

三者都有盲区,也都有成本。最危险的做法是为了“可观测”无界记录所有 SQL 和 参数,结果既拖慢系统,又制造一份高密度敏感数据库。

25.3.1 pg_stat_statements 的统计口径与重置

它不是默认自动完整可用

pg_stat_statements 需要:

  1. 出现在 shared_preload_libraries
  2. PostgreSQL 重启后模块被加载;
  3. 每个需要查询视图的 database 安装 extension;
  4. query identifier 可用;
  5. 查询角色有足够权限;
  6. 容量、track policy 和 reset 被声明。

检查:

SELECT name, setting, source
FROM pg_settings
WHERE name IN (
  'shared_preload_libraries',
  'compute_query_id',
  'pg_stat_statements.max',
  'pg_stat_statements.track',
  'pg_stat_statements.track_utility',
  'pg_stat_statements.track_planning',
  'pg_stat_statements.save'
)
ORDER BY name;

extension 可能不在 public

SELECT
  e.extname,
  e.extversion,
  n.nspname AS extension_schema
FROM pg_extension AS e
JOIN pg_namespace AS n ON n.oid = e.extnamespace
WHERE e.extname = 'pg_stat_statements';

Pigsty 沙箱中的结果:

shared_preload_libraries        pg_stat_statements, auto_explain
compute_query_id                auto
pg_stat_statements.max          10000
pg_stat_statements.track        all
pg_stat_statements.track_utility off
pg_stat_statements.track_planning off
pg_stat_statements.save         on
extension version              1.12
extension schema               monitor

所以查询使用:

monitor.pg_stat_statements
monitor.pg_stat_statements_info

不要硬编码成 public.pg_stat_statements

聚合键不是 query text

一行主要按以下身份聚合:

dbid
userid
queryid
toplevel

这意味着:

  • 同一 queryid 在不同 database 分开;
  • 不同执行角色分开;
  • top-level 与 nested statement 分开;
  • query text 是一个代表性 normalized text,不是主键;
  • 同一可见文本仍可能因语义环境不同获得不同 queryid;
  • hash collision 在理论和实践上都可能发生。

因此关联时保留:

cluster / instance
dbid
userid or role class
queryid
toplevel
stats_since

只保存 queryid 而丢掉 database/user/toplevel,会把不同 workload 合并。

normalization 不是脱敏承诺

常量通常会被替换:

SELECT * FROM orders WHERE order_id = 123;

-- representative normalized form
SELECT * FROM orders WHERE order_id = $1;

但不能据此认为 query 列可以公开:

  • object 名可能包含客户或项目身份;
  • comment 可能含敏感上下文;
  • dynamic SQL 结构可能暴露值;
  • utility statement 的行为不同;
  • query text 与日志/错误组合可能重新识别业务;
  • 权限边界本身说明它被视为敏感。

本章 evidence 明确:

query text exported   false
bind values exported  false
client address        false

queryid 是关联键,不是密码学身份

queryid 是内部 hash:

  • 算法可能随 major version 改变;
  • collision 可能发生;
  • object identity 与 search_path 会影响语义;
  • 相同显示文本可能代表不同解析对象;
  • failover/upgrade 前后要重新建立 baseline。

不要用 queryid:

  • 做访问控制;
  • 证明 SQL 完全相同;
  • 做永久跨版本业务 ID;
  • 取代 plan fingerprint;
  • 取代 application operation class。

它适合:

  • workload 聚合;
  • before/after 比较;
  • dashboard 下钻;
  • 与受限日志/plan 关联;
  • top-N 诊断。

从总时间拆成频率与单次成本

$$ \text{total execution time}

\text{calls} \times \text{mean execution time} $$

top total time 可能来自:

very frequent cheap query
rare extremely slow query
both

基本查询不要一开始读 text:

SELECT
  dbid,
  userid,
  queryid,
  toplevel,
  calls,
  round(total_exec_time::numeric, 2) AS total_exec_ms,
  round(mean_exec_time::numeric, 3) AS mean_exec_ms,
  round(min_exec_time::numeric, 3) AS min_exec_ms,
  round(max_exec_time::numeric, 3) AS max_exec_ms,
  rows,
  shared_blks_hit,
  shared_blks_read,
  temp_blks_written,
  wal_bytes,
  stats_since,
  minmax_stats_since
FROM monitor.pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 50;

注意:

  • min/max 易受单次异常影响;
  • mean 掩盖分布;
  • row count 的语义随 statement 类型变化;
  • block 是 PostgreSQL block,不是任意存储 byte;
  • I/O timing 依赖开关;
  • WAL byte 不等于 commit durability;
  • top-N 会漏掉排名外 workload。

用 delta,而不是跨 reset 比裸值

两个采样点:

t0: calls_0, total_exec_0, stats_since_0
t1: calls_1, total_exec_1, stats_since_1

只有 identity 与统计窗口连续时才计算:

Δcalls=calls1calls0 \Delta calls = calls_1 - calls_0

$$ \text{window mean}

\frac{\Delta total_exec_time} {\Delta calls} $$

如果:

  • row 消失;
  • stats_since 改变;
  • postmaster restart;
  • extension reset;
  • entry deallocated 后重新创建;
  • failover 到另一成员;

就不能直接减。

pg_stat_statements_info 是解释入口

SELECT *
FROM monitor.pg_stat_statements_info;

核心:

dealloc
stats_reset

dealloc 增长表示 statement entry 因容量压力被丢弃。此时 top workload 可能 被 churn 影响;应评估:

  • pg_stat_statements.max
  • query shape 数量;
  • dynamic SQL;
  • reset;
  • 内存成本;
  • 是否需要更稳定的 application query pattern。

沙箱正式快照:

dealloc              0
stats_reset          2026-07-29T18:57:02Z
statement rows       194
calls                超过 100,000

这是一次窗口基线,不是永久容量结论。

每行还有自己的时间边界

PostgreSQL 18 的 statement 行包含:

stats_since
minmax_stats_since

如果只 reset 某条或只 reset min/max,不同 entry 的窗口可能不同。一个 dashboard 不能只在标题写全局“过去 24 小时”,却忽略每行实际开始时间。

reset 是破坏性观察动作

函数可以全局或选择性 reset;当前版本还支持只 reset min/max。它很有用,但 会销毁比较基线。

原则:

incident capture
  never reset to make the graph easier

planned benchmark
  may reset in an isolated target with explicit evidence

production baseline
  prefer snapshots/deltas and recording rules

本章 L0 采集明确禁止:

pg_stat_statements_reset
pg_stat_reset*

如果必须 reset:

  • 记录 request/approval;
  • exact target;
  • old reset time;
  • snapshot/hash;
  • reason;
  • expected observation gap;
  • downstream dashboard impact;
  • new baseline start。

planning 与 execution 并非一一对应

pg_stat_statements.track_planning=on 可以收集 plan count/time,但:

  • 默认通常为 off;
  • 对高并发相同 query,plan 统计更新会有明显开销;
  • prepared/cached plan 改变 plan/execute 次数关系;
  • 执行失败与计划失败的记录条件不同;
  • utility 和 nested track policy 影响 population。

沙箱 track_planning=off,所以:

total_plan_time = 0

表示未收集,而不是规划不耗时。不要为了填满 dashboard 直接在生产开启;先做 负载测试和决策评审。

track=all 的语义

top 只跟踪 top-level;all 还跟踪 nested statement。沙箱设置 all,再用 toplevel 区分。否则 stored procedure 或 function 内 SQL 可能看不见,或被 误与 top-level 混合。

top-N 查询要保护系统

观察查询也消耗:

  • shared memory lock;
  • sort;
  • format/round;
  • dashboard 并发;
  • network;
  • 结果存储。

生产查询建议:

SET LOCAL statement_timeout = '5s';
SET LOCAL lock_timeout = '500ms';

-- 只读、限制列、限制行、先 aggregate identity

不要每 5 秒在每个 database:

SELECT * FROM pg_stat_statements ORDER BY total_exec_time DESC;

尤其不要自动导出完整 query text。

建立 workload baseline

基线至少分:

calls rate
total/mean/max execution
rows per call
shared hit/read/write
temp blocks
WAL bytes
JIT time/count
planning if explicitly enabled
stats reset / entry age

按:

service operation
database
role class
queryid
release/change window

关联。不要仅按 instance;failover 后 workload 会移动。

25.3.2 慢语句、锁等待、临时文件与错误日志

慢日志是离散样本

核心设置:

SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN (
  'log_min_duration_statement',
  'log_min_duration_sample',
  'log_statement_sample_rate',
  'log_duration',
  'log_statement',
  'log_lock_waits',
  'deadlock_timeout',
  'log_temp_files',
  'log_min_error_statement',
  'log_parameter_max_length',
  'log_parameter_max_length_on_error'
)
ORDER BY name;

log_min_duration_statement

记录执行时间达到阈值的语句;0 表示记录全部,-1 关闭。

优点:

  • 对超过阈值的 population 较完整;
  • 可以看到离散 tail;
  • 能与 queryid、错误和时间线关联。

代价:

  • I/O 与格式化;
  • log volume;
  • SQL/参数泄露;
  • 高频略慢语句形成洪水;
  • 日志系统成为新瓶颈。

log_min_duration_sample

达到 sample threshold 后,再按 log_statement_sample_rate 采样。它适合控制 高流量环境的日志量,但“未出现”不能解释为“未发生”。

如果:

log_min_duration_sample = -1
log_statement_sample_rate = 1

采样路径仍是关闭的。不要只看 rate。

duration 的边界

语句 duration 与用户 end-to-end latency 不同。用户时间可能包括:

network
application queue
pool wait
server execution
result transfer
application processing
retry
commit reconciliation

PostgreSQL 慢日志只覆盖 server 侧语句范围。

lock wait 日志

log_lock_waits=on 在等待超过 deadlock_timeout 时记录。沙箱:

log_lock_waits   on
deadlock_timeout 50ms

这意味着:

  • 超过约 50 ms 的 lock wait 有机会被记录;
  • 更短等待可能很多但没有日志;
  • deadlock detection cadence 与日志量相关;
  • 不能为了更多日志随意降低 timeout;
  • 日志是离散事件,当前 lock view 是即时状态。

调查组合:

user latency window
pg_stat_activity current wait
pg_locks / pg_blocking_pids
lock wait logs
deadlock counter
application transaction boundary

temporary file

log_temp_files 在临时文件删除时记录超过阈值的文件。沙箱为:

1024 kB

这类日志说明:

  • 某操作产生了 temp file;
  • 大小达到记录政策;
  • 日志时刻可能是删除时刻,不是创建/峰值时刻。

关联:

  • pg_stat_database.temp_files/temp_bytes
  • pg_stat_statements.temp_blks_read/written
  • queryid;
  • plan;
  • sort/hash/window;
  • workload concurrency;
  • work_mem policy。

不要看到 temp 就全局提高 work_mem。它按 operation/node/worker 使用,高并发 会把一个小改动放大成内存风险。

error log 与用户 outcome

PostgreSQL error 包含 SQLSTATE、severity、detail、context 等;应用可能:

  • retry 后成功;
  • 返回失败;
  • 超时但 commit;
  • 屏蔽错误;
  • 将一个 DB error 映射成不同业务 outcome。

所以错误日志要与应用 outcome 关联,而不是用 log line 数直接做 availability 分子。

稳定聚合维度:

cluster / instance / database
severity / SQLSTATE class
application / operation class
release

不适合作 label:

full message
detail
statement
parameter
customer/order/tenant

structured CSV/JSON 不会自动安全

沙箱使用:

logging_collector on
log_destination csvlog
log_directory /pg/log/postgres
log_filename postgresql-%a.log

CSV 方便 Vector 解析,但:

  • statement 字段仍可能敏感;
  • detail/context 可能含值;
  • file permission/collector group 需要评审;
  • 集中存储扩大读取面;
  • retention 必须独立设置。

从 metric 到 log,而不是无界 log query

正确流程:

metric identifies:
  service, environment, operation, time window

event identifies:
  release/change boundary

log query:
  bounded window + stable fields + maximum rows

result:
  aggregate error classes, do not export bodies

本章诊断包规定:

log query limit  1000
body export      forbidden

需要正文时,应在受限界面临时查看并按事件政策处理,而不是复制进公共工单。

日志缺失也有语义

没有日志可能是:

  • 事件未发生;
  • threshold 未达到;
  • sampling 丢失;
  • collector/Vector 失败;
  • rotation/retention;
  • parse schema drift;
  • 权限或查询错误;
  • 日志写入阻塞;
  • service 在另一 instance。

必须同时观察 log pipeline。

25.3.3 auto_explain 的采样、嵌套语句与开销

auto_explain 解决什么

pg_stat_statements 告诉你:

which queryid is costly

EXPLAIN/EXPLAIN ANALYZE 告诉你:

how one plan is structured and executed

auto_explain 在满足政策时自动把 plan 写入日志,适合捕捉难以手工复现的慢 执行。

它不是零成本“打开即可”。

最重要的开关

SELECT name, setting, unit, source
FROM pg_settings
WHERE name LIKE 'auto_explain.%'
ORDER BY name;
设置 问题
log_min_duration 多慢才记录;默认 -1 不启用
sample_rate 满足阈值的 statement 采样多少
log_analyze 是否实际执行统计进入 plan
log_timing ANALYZE 时是否逐节点计时
log_buffers 是否记录 buffer 使用
log_wal 是否记录 WAL 使用
log_nested_statements function 内语句是否记录
log_parameter_max_length 参数记录长度
log_format text/xml/json/yaml
log_verbose 是否输出额外细节
log_settings 是否记录影响 planning 的设置
log_triggers trigger 统计

log_analyze 的隐藏成本

官方文档特别警告:启用 log_analyze 后,即使最终语句没有达到 log_min_duration 而不写日志,也可能需要为所有语句做 per-plan-node timing。

如果同时:

log_analyze=on
log_timing=on
sample_rate=1

开销可能非常高,尤其是大量短 node 的 workload。

log_timing=off 可以保留 rows 等实际统计并减少逐节点时钟调用,但仍需实测。

沙箱的真实设置

本章只读检查发现:

auto_explain.log_min_duration       1000 ms
auto_explain.log_analyze            on
auto_explain.log_timing             on
auto_explain.log_nested_statements  on
auto_explain.sample_rate            1
auto_explain.log_parameter_max_length -1

这不是推荐模板。它是一个需要单独 overhead 与泄露评审的真实基线:

  • sample_rate=1 没有限制 eligible statement;
  • log_analyze + log_timing 有全局计时成本;
  • nested 可能显著放大日志量;
  • parameter length -1 允许完整参数;
  • 阈值 1 秒只限制最终写 plan,不一定消除 instrumentation cost。

本章不 reload、不改参数,只把风险记录下来。

BUFFERSWAL 依赖 ANALYZE

自动计划中的实际 buffer/WAL 信息要求 analyze path。不能关闭 analyze 后仍假装 获得运行时资源。

设计取舍:

plan shape only
  lower runtime detail, lower cost

analyze without timing
  actual rows/buffer possibility, less per-node clock cost

analyze with timing
  richer node time, potentially high overhead

必须在代表性 workload 上量化。

nested statement

function、trigger、procedure 内可能包含真正慢的 SQL:

top-level CALL
  cheap wrapper
  nested SQL expensive

log_nested_statements=on 能看见,但会:

  • 增加 volume;
  • 重复上下文;
  • 暴露更多 query/parameter;
  • 让一个 top-level 请求产生多份 plan。

同时使用 pg_stat_statements.track=alltoplevel 关联。

采样测试应覆盖 tail

开启前在隔离 workload 回答:

baseline TPS / p50 / p99
CPU and system time
log bytes per second
collector lag
plan count
short statement overhead
long statement capture probability
nested amplification
parameter redaction
rollback path

不要只跑一条慢查询说“开销不大”。

不要在事故中临时全开

事故中:

log_min_duration=0
log_analyze=on
log_timing=on
sample_rate=1

可能:

  • 进一步降低吞吐;
  • 加剧 disk/log pipeline;
  • 泄露参数;
  • 改变被观察 workload;
  • 制造新的故障。

更安全顺序:

  1. 用现有 SLI、activity、wait、queryid 缩小范围;
  2. 使用已有日志与 plan;
  3. 评估只对 session/role/database 的受控方法;
  4. 明确 timeout、采样、持续时间和 rollback;
  5. 由变更政策批准;
  6. 观察 overhead;
  7. 到期自动撤销并验证。

auto_explain 不是 plan history 系统

计划只在满足政策时进入日志;sampling、rotation、retention、parse 都会造成 缺口。若要做 plan regression:

  • 在测试/发布流程保存 EXPLAIN (FORMAT JSON)
  • 记录 schema/statistics/version/settings;
  • 与 queryid/plan fingerprint 关联;
  • 不要把生产日志当完整 plan catalog;
  • 不要在没有参数与数据分布语义时做机械 diff。

25.3.4 日志不得泄漏密码、令牌和敏感参数

SQL observability 是高敏感数据面

可能出现:

password in connection/DDL statement
API token in INSERT/UPDATE
tenant/customer/order identity
email/phone/address
medical/payment attributes
session variable
RLS predicate context
error detail containing row values
dynamic SQL comment
connection URI

PostgreSQL 文档明确警告,statement logging 可能暴露敏感数据,甚至明文密码。

参数记录的两套限制

log_parameter_max_length
  非错误 statement 的参数记录限制

log_parameter_max_length_on_error
  错误发生时的参数记录限制

一般语义:

0    disable
-1   no length limit
N    truncate to N bytes

auto_explain.log_parameter_max_length 又是独立设置。

沙箱:

log_parameter_max_length           -1
log_parameter_max_length_on_error   0
auto_explain.log_parameter_max_length -1

这表示错误路径参数被禁用,但普通慢 statement/auto_explain 仍可能完整记录参数。 是否安全取决于 protocol、query 和应用,不能因为“一项为 0”宣布无泄露。

log_statement 的风险

log_statement=all 会记录每条 statement;DDL 中尤其可能出现:

CREATE ROLE app LOGIN PASSWORD '...';
ALTER ROLE app PASSWORD '...';

即使参数化 DML 避免 literal,DDL、utility、comment 和 dynamic SQL 仍可能带 秘密。生产不得把“排障方便”当默认充分理由。

文件模式:0600 不是唯一安全答案

PostgreSQL log_file_mode 控制 collector 创建文件的 mode。

0600
  PostgreSQL OS owner only

0640
  owner read/write + restricted collector group read

Pigsty 使用 Vector 收集日志时,受控 group read 可能是合理实现。安全要求是:

not world-readable
collector group explicitly declared
membership minimal
no interactive user by default
directory traversal restricted
rotation preserves mode
central storage ACL reviewed

沙箱为 0640。本章没有证明 collector group membership 已完成生产审批,因此 将其作为待评审事实,而不是自动判定安全或不安全。

传输到 VictoriaLogs 扩大了边界

原本只有 database host 上的日志,集中后可能被:

  • Grafana data source;
  • log query API;
  • incident automation;
  • backup/export;
  • 多个 operator

访问。

必须重新定义:

ingestion TLS/auth
tenant isolation
query role
retention
deletion
export
redaction
audit
backup

“源文件权限正确”不能证明集中日志安全。

metric label、alert annotation、ticket 是二次泄露面

最常见事故不是直接开放 log file,而是自动化复制:

log body
  -> alert annotation
      -> chat webhook
          -> ticket
              -> email
                  -> postmortem

本章诊断包只导出:

  • error class/count;
  • queryid;
  • database/role class;
  • time window;
  • release/change id;
  • source link without credentials。

不导出:

  • statement;
  • bind value;
  • log body;
  • client address;
  • tenant/order/customer。

redaction 要在尽量靠近 source 的位置

优先级:

  1. 应用不把 secret 放进 SQL/comment;
  2. 参数化查询;
  3. PostgreSQL logging policy 限制;
  4. collector parse/redact;
  5. storage ACL/retention;
  6. alert/evidence allowlist。

最后一步 regex 不是万能防线:

  • 数据格式会变化;
  • 编码/截断会绕过;
  • 新字段未被匹配;
  • secret 可能已进入上游缓存/备份。

query text 的受控交互查看

真正排障有时需要 SQL text。正确做法不是绝对禁止人看,而是区分:

machine evidence
  no query text

interactive diagnosis
  time-bounded pg_read_all_stats or narrower role
  approved operator
  restricted UI/session
  no casual copy
  audit and expiry

public/reference summary
  queryid and aggregates only

这既保留诊断能力,也控制扩散。

prepared statement 不自动消除泄露

参数化可以让 statement text 不含 literal,但:

  • bind parameter logging 仍可能记录值;
  • 错误 detail/context 可能包含;
  • 应用 comment/baggage 可能包含;
  • object/table/schema 名仍可能敏感;
  • auto_explain parameter logging 是独立开关。

所以要检查完整链路,不只看应用 ORM。

慢日志与审计日志不是一回事

慢日志回答 performance sample;审计回答谁在何时对什么对象做了什么。两者:

  • 目的不同;
  • 保留不同;
  • 访问不同;
  • 完整性要求不同;
  • 敏感性不同。

不要让 log_statement=all 同时冒充性能、审计、合规和安全检测。需要审计时, 使用明确的审计政策、对象范围和审阅流程。

安全基线查询

SELECT name, setting, source
FROM pg_settings
WHERE name IN (
  'logging_collector',
  'log_destination',
  'log_directory',
  'log_filename',
  'log_file_mode',
  'log_statement',
  'log_min_duration_statement',
  'log_min_duration_sample',
  'log_statement_sample_rate',
  'log_parameter_max_length',
  'log_parameter_max_length_on_error'
)
OR name LIKE 'auto_explain.%'
ORDER BY name;

这只是配置事实,还要复核:

  • actual file mode/owner/group;
  • directory mode;
  • Vector identity/config;
  • VictoriaLogs access/retention;
  • sample log 是否被正确 parse/redact;
  • alert/template 是否复制 body;
  • backup 是否包含日志。

本章 L0 只记录配置,不读取日志正文。

SQL 可观测基线清单

extension
  preload / version / schema / query id

population
  track / utility / nested / max

time
  global reset / per-row stats_since / minmax_stats_since

cost
  planning / timing / auto_explain / logging volume

security
  query visibility / parameters / file group / central log ACL

diagnosis
  calls / exec / rows / blocks / temp / WAL

correlation
  service / operation / release / database / role / queryid

boundary
  no query text in automatic evidence

本节验收

你应当能够解释:

  1. 为什么 extension schema 不能假设是 public
  2. 一行 pg_stat_statements 的四个核心聚合维度;
  3. normalization 为什么不是脱敏保证;
  4. queryid 为什么不能做永久或安全身份;
  5. calls、total、mean、max 各说明什么;
  6. dealloc、global reset、stats_sinceminmax_stats_since 的差异;
  7. track_planning=off 时 plan time 为零意味着什么;
  8. 慢语句、采样慢语句、lock wait 和 temp file 日志各何时产生;
  9. auto_explain.log_analyze + log_timing 为什么可能给所有语句带来成本;
  10. nested statement 为什么同时增加诊断价值与泄露/volume;
  11. 0640 如何在受控 collector group 下成立;
  12. 为什么自动 evidence 只保留 queryid 与聚合,而交互查看另行授权。

上一节:PostgreSQL 核心运行信号 · 返回本章目录 · 下一节:把观察契约变成告警 · 查看全书目录 · 查看索引中心

25.4 把观察契约变成告警

规则语法正确,只证明 parser 接受了它。

一条生产告警还必须同时通过:

semantic
  expression 真正测量合同定义的事件吗?

temporal
  window / scrape / eval delay / for / recovery 正确吗?

governance
  page、ticket 还是 diagnostic?谁接受了这个政策?

actionability
  owner 能执行首个安全动作吗?

routing
  grouping / inhibition / receiver 会制造风暴或静默吗?

exercise
  正常、异常、缺失、错误和恢复都被测试了吗?

本章生成的文件:

它们是教学候选,不是在线配置。

25.4.1 SLO 燃烧率与用户影响

从 error ratio 到 burn rate

若 SLO target 为:

S=99.9%=0.999 S = 99.9\% = 0.999

允许的 bad ratio:

B=1S=0.001 B = 1 - S = 0.001

某窗口实际 bad ratio 为 $E$,燃烧率:

burn rate=EB \text{burn rate} = \frac{E}{B}

例如:

actual bad ratio  1.44%
budget ratio      0.1%
burn rate         14.4x

若持续 28 天,会花掉 14.4 倍预算;若只持续 1 小时,约花掉:

14.4×1h28d2.14% \frac{14.4 \times 1h}{28d} \approx 2.14\%

这解释了为什么 14.4x/1h 常被用作“快速消耗约 2% 月度预算”的起点。它是 政策起点,不是自然常数。

为什么使用两个窗口

只用长窗口:

stable
but slow to detect a sudden severe outage

只用短窗口:

fast
but noisy and easy to flap

组合:

bad_ratio_long > threshold
and on (service, operation_class, environment)
bad_ratio_short > threshold

长窗确认预算风险,短窗确认问题仍在发生。

本章可用性 fast burn

记录规则预先计算五个窗口:

5m / 30m / 1h / 6h / 3d

fast:

- alert: PG36ShopAvailabilityFastBurn
  expr: |
    pg36_shop:sli_availability_bad_ratio:rate1h > (14.4 * 0.001)
    and on (service, operation_class, environment)
    pg36_shop:sli_availability_bad_ratio:rate5m > (14.4 * 0.001)
  for: 2m

标签固定:

class: symptom
route: page
severity: SEV-1
objective_id: SLO-AVAILABILITY
owner_function: service
runbook_id: RB-USER-SYMPTOM
governance_status: accepted-ch24

fast、slow 与 ticket 是一套政策

规则 长/短窗 burn for 路由
fast 1h / 5m 14.4x 2m SEV-1 page
slow 6h / 30m 6x 5m SEV-2 page
budget 3d / 6h 1x 15m ticket

它们不是三个独立“数据库报警”:

  • fast 处理快速用户损害;
  • slow 处理持续预算风险;
  • ticket 处理应进入 backlog 的可靠性债务。

当 fast firing 时,同一 service/environment/objective 的 slow 和 ticket 可以 被抑制,避免一个事件三次通知;原始 alert state 和证据仍保留。

burn threshold 必须跟 target 绑定

延迟目标是 99%:

budget ratio 0.01
14.4x threshold 0.144

可用性目标是 99.9%:

budget ratio 0.001
14.4x threshold 0.0144

把同一个绝对 error ratio 复制过去会错一个数量级。

规则生成器若使用 policy object,应从:

objective target
window
burn policy

计算,而不是在 YAML 中散落魔法数。

event ratio,不是 instance ratio

错误写法:

count(pg_up == 0) / count(pg_up)

它算的是实例比例,不是用户 event ratio。三节点中一个 down:

  • 服务可能完全正常;
  • 某些 read path 可能降级;
  • failover 可能正在进行;
  • 用户失败比例不一定是 1/3。

正确 availability SLI 来自 application outcome:

sum by (service, operation_class, environment) (
  rate(pg36_shop_request_outcomes_total{
    eligible="true",
    outcome!="good"
  }[5m])
)
/
sum by (service, operation_class, environment) (
  rate(pg36_shop_request_outcomes_total{
    eligible="true"
  }[5m])
)

instrumentation 要预初始化 bounded outcome series,否则“没有 bad outcome series”与“零 bad events”会混淆。

latency 规则暴露了治理缺口

第 24 章定义 SLO-LATENCY,但 accepted alert list 没有 latency candidate。 本章没有悄悄将它变成生产 page,而是:

alert: PG36ShopLatencyFastBurn
route: test
severity: candidate
governance_status: proposed-not-accepted

规则和测试都存在,生产门禁仍拒绝。这体现:

technical readiness
  != policy acceptance

owner 还要决定:

  • latency threshold 是否 250 ms;
  • eligible event 与 availability 是否相同;
  • success 但慢、failure 且慢如何计数;
  • 低流量如何处理;
  • page severity;
  • 首个安全动作;
  • 与 availability page 如何去重。

freshness 必须使用 commit-correlated event

规则:

pg36_shop:sli_freshness_bad_ratio:rate1h > (14.4 * 0.01)
and on (service, read_path, environment)
pg36_shop:sli_freshness_bad_ratio:rate5m > (14.4 * 0.01)

分母是携带已知 commit token 的 probe,bad 是五秒内不可见。

不要替换成:

pg_lag > 5

因为 pg_lag 的语义、空闲行为和用户 path 都不同。

正确性不燃烧

- alert: PG36ShopCorrectnessMismatch
  expr: pg36_shop_reconciliation_mismatches > 0
  for: 0m

它没有:

burn rate
error budget
planned exclusion
slow ticket before page

一个未解释 mismatch 就进入完整性响应。缺失或 stale reconciliation 是另一个 control failure,不是零 mismatch。

恢复就绪也是 control

候选:

pg36_shop_restore_evidence_age_seconds
  > 90 * 24 * 60 * 60

它应:

  • block 高风险 release 或建立 ticket;
  • 引导查看不可变 restore manifest;
  • 安排隔离 restore drill;
  • 不把 backup success 当替代。

当前第 24 章已定义 control objective,但告警候选仍待接受,所以走 test sink。

容量预测先要求 reviewed

pg36_shop_capacity_horizon_days < 14
and on (service, environment)
pg36_shop_capacity_forecast_reviewed == 1

为什么第二个条件重要?

short history
step change
seasonality
retention change
one-time load
broken collector

都可能产生虚假线性 forecast。未经评审的模型不应 page。即使 reviewed,也默认 ticket,因为当前没有用户损害。

用户影响必须写进规则

同一个 threshold 若不能说明 user impact,就不能决定 severity:

CPU > 90%
  user impact unknown

availability fast burn
  eligible order attempts failing/unreconciled

correctness mismatch
  acknowledged state may be invalid or cross-tenant

monitoring path broken
  service health unknown

这也是为什么 PG36HostCpuHigh 保持 diagnostic。

25.4.2 阈值、持续时间、去抖、抑制与分组

threshold 是政策,不是颜色

阈值来源可以是:

  • SLO/error budget;
  • control objective;
  • 容量 horizon;
  • 安全/完整性不变量;
  • 组件规格;
  • 统计/历史基线;
  • 供应商硬限制。

每种来源对应不同动作。不要在 Grafana 中选一个“看起来红”的值,然后反向写 解释。

for 的状态机

典型:

inactive
  expression false/no matching series

pending
  expression true, for not elapsed

firing
  expression remains true for duration

inactive again
  expression false or series disappears

for 不是简单 sleep:

  • evaluation interval 决定检查粒度;
  • rule error 与 no-data 的行为不同;
  • restart/state persistence 要配置;
  • data delay 可能让最新点不可见;
  • label set 改变会创建新的 alert identity;
  • 短暂 false 可重置 pending。

VMAlert 文档说明:no data 会重置 pending;evaluation error 则不会按普通 false 处理,而会保留此前状态。必须监控 rule error,不能只看业务 alert。

for 不能修复错误语义

CPU > 90% for 10m

仍然不知道用户是否受损。

pg_archiver_failed_count > 0 for 5m

仍然会把历史失败永久当当前失败。

先修 expression,再用 for 处理持续性。

scrape、evaluation 与 delay

假设:

scrape interval       30s
storage visibility    up to 30s
evaluation interval   1m
for                   2m

“2m 后 page”不代表现实事件开始后精确两分钟:

event -> next scrape -> ingest -> visible eval -> pending -> later eval -> firing

可能更久。规则验收应测 end-to-end detection time,而不是只读 YAML。

VMAlert 支持 group eval_delay/query latency offset 等。取值要根据真实 ingestion 延迟,并在 rule test 与 dashboard query 中使用一致时间对齐。

recording 与 alert group 分离

VMAlert 一个 group 内顺序执行 rule,但 recording result 异步写回 remote storage。若同组后续 alert 立即读取前一个 recording result,可能看不到本轮 结果。

不要:

groups:
  - name: bad-chain
    rules:
      - record: A
        expr: ...
      - alert: B
        expr: A > ...

本章分成:

pg36-shop-sli-recording
pg36-postgresql-diagnostic-recording
pg36-observation-meta-recording

pg36-shop-slo-alerts
pg36-shop-control-alerts
pg36-postgresql-proposed-alerts
pg36-observation-path-alerts

测试用 group_eval_order 先运行 recording groups,再运行 alert groups。

rule result limit

错误 label 可能让一条 rule 产生数十万结果。VMAlert group 支持 result limit; 生产应按预计 cardinality 设置并监控 exceeded error。

限制不是修复:

  • 先保证 labels 有界;
  • 再估算 series;
  • 限制作为故障保护;
  • 超限时 health unknown,不能默默丢结果;
  • 监控 rule error。

Alertmanager group_by

本章:

group_by:
  - service
  - environment
  - alertname

没有把 ins 放进 user symptom grouping。原因:

one user symptom
  may correlate with many instances

group by instance
  can create one page per component

组件信息在诊断包和 dashboard,下游 page 围绕服务事件。

group timing

group_wait: 30s
group_interval: 5m
repeat_interval: 4h

含义大致是:

  • group_wait:新组第一次通知前等待相关 alert 聚合;
  • group_interval:组变化后再次发送的最小间隔;
  • repeat_interval:持续 alert 重复提醒间隔。

取值要与 severity 匹配。SEV-1 等 30 秒可能可以,也可能太慢;真实策略需 notification SLO 和业务 owner 接受。

dedup identity 来自 labels

改变 label 会创建新 alert:

release changes
instance changes
error_message changes
queryid changes

因此不要把高变化字段放 alert label。它们会:

  • 重置 for
  • 打破 dedup;
  • 增加通知;
  • 让 silence 失效。

inhibition 只去重,不删除事实

本章第一条:

source
  availability fast burn, SEV-1

target
  availability slow burn or budget ticket

equal
  service / environment / objective_id

它不会抑制:

  • 不同 service;
  • 不同 environment;
  • freshness;
  • correctness;
  • monitoring path。

第二条:

monitoring path broken
  inhibits only derived-missing alerts
  with observation_dependency=true
  for same service/environment

它不抑制独立 blackbox 看到的 user symptom。

正确性永不被普通抑制

本章对抗测试会拒绝任何 target matcher 包含:

PG36ShopCorrectnessMismatch
class=integrity

原因:

monitoring broken
  does not make known corruption less important

availability outage
  does not excuse cross-tenant mismatch

可以在 incident tooling 中关联同一事件,但不能静默正确性 page。

silence 与 inhibition 不同

inhibition 是配置中的关系规则;silence 是带 matcher 和期限的操作。

silence 必须:

  • owner;
  • reason;
  • start/end;
  • exact matcher;
  • replacement observation;
  • incident/change reference;
  • review/expiry。

不要为了维护直接 silence 整个 cluster 的所有 alert。planned maintenance 也 不应从 SLO event 中事后删除。

recovery 与 flapping

恢复条件可以:

  • expression 直接 false;
  • 使用 hysteresis;
  • 使用 keep_firing_for(若平台与政策采用);
  • 由 Alertmanager repeat/group timing 控制通知;
  • 要求独立 verification。

不要只让 threshold 出现巨大滞后,导致真实恢复长时间仍 page。

测试至少覆盖:

brief spike does not fire
sustained condition fires
condition recovers
one window recovers but other does not
series disappears
rule query errors
label identity changes

25.4.3 每条告警绑定所有者、证据和首个安全动作

page 是打断权

page 会打断一个人的当前工作或睡眠。因此最低合同:

current user/integrity/durability/observation-path impact
accountable owner function
tested route
runbook
first safe action
verification
dashboard/source
silence policy
expiry/review

没有这些字段,报警不是“先上线再补文档”的半成品,而是潜在事故放大器。

route class

route 何时使用 例子
page 需要立即有人介入 fast burn、correctness、monitoring path
ticket 当前可排期但有 deadline/owner capacity、slow reliability debt
diagnostic 解释 symptom,不独立打断 replica distance、CPU、pool queue
test 尚未治理接受或只做演练 latency candidate、archive candidate

severity 不应与 component 数值机械绑定。

owner_function,不是个人名字

owner_function: service

真实 Alertmanager route 再映射到当前值班表。好处:

  • 人员轮换不改 rule;
  • 服务责任与平台执行分开;
  • 可以审查 role 是否可达;
  • 可以测试 fallback escalation。

本章空 receiver 只是验证标签分支,未证明真实 on-call 可达。

首个安全动作要降低不确定性或损害

fast availability:

freeze latest risky release
reconcile unknown outcomes before retrying writes

不是:

restart PostgreSQL
kill top query
fail over
increase pool

这些数据库动作可能扩大未知写结果。

freshness:

route affected read-after-write journey to primary
preserve commit tokens

正确性:

freeze affected writes
preserve reconciliation boundary

monitoring path:

establish independent blackbox view
before changing monitored database

首个动作不是完整修复,而是事故中最不容易后悔的下一步。

source/dashboard URL:

dashboard://pg36-shop-slo
runbook id
rule source id
time window

不要:

https://user:password@monitor/...
signed URL with long-lived token
raw log query containing tenant/order

receiver 中的 secret 也不能进入 Git。

annotation 要短而确定

通知首屏包含:

what
user impact
scope
starts at
first safe action
runbook
dashboard

不要塞:

  • 全部诊断 SQL;
  • 数百行日志;
  • 未经验证的 root cause;
  • 变化的 error text;
  • 密码/token;
  • “请检查”这种无动作文本。

runbook 需要停止线

告警 runbook 不是:

1. 登录数据库
2. 看看
3. 重启

它至少包括:

  • entry condition;
  • identity/target;
  • authority;
  • safe read-only queries;
  • hypotheses;
  • first action;
  • stop condition;
  • escalation;
  • mutation approval;
  • verification;
  • evidence。

一个 page 若 runbook 第一条就是 destructive action,应退回评审。

dashboard 不负责结论

dashboard URL 负责定位:

  • SLI windows;
  • event count;
  • release marker;
  • entry/DB/host correlation;
  • metamonitoring。

runbook 负责:

  • 如何解释;
  • 如何复核;
  • 什么能做;
  • 什么不能做。

Grafana panel 不能表达完整权限和停止线。

alert review expiry

第 24 章 candidate 带 expires_for_review。规则应定期检查:

  • SLO target 是否变;
  • metric/version 是否变;
  • query cost;
  • false positive/negative;
  • page actionability;
  • route ownership;
  • runbook 是否可执行;
  • last drill;
  • label cardinality;
  • notification receipt。

“从未触发”可能是服务稳定,也可能是规则永远不匹配。VMAlert 有 never-firing 检测思路,但仍要用合成测试证明。

actionless instance-down 为什么被拒绝

候选:

PostgresInstanceDownWithoutAction
route page

缺少:

  • user impact;
  • owner;
  • runbook;
  • first safe action。

一个 replica 可能计划下线且服务正常。替代:

topology dashboard retains instance state
endpoint/user symptom drives page
imminent durability/HA loss may have separately governed alert

这不是“不监控实例”,而是不把所有 component state 都升级为打断。

25.4.4 用演练验证告警,而不是等生产事故

规则需要单元测试

测试文件指定:

rule_files:
  - recording-rules.yml
  - alert-rules.yml

evaluation_interval: 1m
group_eval_order:
  - recording groups...
  - alert groups...

然后注入:

input_series:
  - series: '...rate1h{service="pg36_shop",...}'
    values: "0.02x3 0x5"

检查:

1m   pending, no firing alert
3m   firing with exact labels/annotations
7m   recovered, no alert

为什么不直接在在线 VMAlert 测

向在线 datasource/Alertmanager 发合成数据会:

  • 污染 production-like time series;
  • 触发真实 route;
  • 创建 silence/incident confusion;
  • 和现有规则相互作用;
  • 难以证明完全清理。

本章使用 vmalert-tool

start isolated VictoriaMetrics
ingest synthetic series
evaluate rules
compare expected samples/alerts
exit and remove temp workspace

这是 L1 ephemeral,不是 live deployment。

测记录规则的算术

输入:

good counter  +100/min
bad counter   +2/min

期望:

2100+2=0.019607843... \frac{2}{100+2} = 0.019607843...

工具实际查询 recording result,防止:

  • selector 写反;
  • 分母只算 good;
  • label join 丢失;
  • window 名与内容不一致;
  • expression 语法只“看起来对”。

测 alert labels 和 annotations

测试不仅看 alertname,还看:

service
operation_class/read_path
environment
class
route
severity
objective_id
owner_function
runbook_id
governance_status
observation_dependency
first_safe_action
verification
dashboard

这样 candidate 被误改成 page,或 accepted rule 被误改成 proposed,会使测试或 validator 失败。

测 missing

输入:

expected_service_traffic = 1
no pg36_sli_sample_fresh series

期望:

PG36ShopSLIMissing
route=test
class=derived-missing
observation_dependency=true

不要用零 bad ratio 代替。

还应分别测试:

  • expected traffic 为零;
  • SLI fresh;
  • exporter down;
  • rule query error;
  • independent probe fails;
  • label rename。

测 route,不发送通知

Alertmanager sandbox receiver 只有名字:

receivers:
  - name: pg36-shop-page-sink
  - name: pg36-shop-ticket-sink
  - name: pg36-platform-page-sink
  - name: pg36-platform-ticket-sink
  - name: pg36-proposed-rule-sink
  - name: pg36-null-sink
  - name: pg36-default-sink

没有:

webhook_configs
email_configs
slack_configs
pagerduty_configs
credentials

amtool config routes test 根据标签离线解析 receiver。八个用例覆盖:

availability page
correctness page
service ticket
platform ticket
metamonitoring page
diagnostic null
proposed test
unknown default

测 inhibition 的正反例

五个用例:

fast inhibits same-objective slow       true
meta inhibits derived missing           true
meta inhibits independent user symptom  false
meta inhibits correctness               false
fast crosses service boundary           false

只测“应该抑制”不够;错误的 broad matcher 往往会吞掉最重要的告警。

测规则引擎本身

在线当前快照:

groups             17
alert rules         50
recording rules     698
group errors        0
rule errors         0
alert states        50 inactive
current alerts      0

这是已有 Pigsty 规则,不包括本章 31 条候选规则;本章从未部署它们。

metamonitoring 还应观察:

  • evaluation duration vs interval;
  • missed iteration;
  • datasource error;
  • remote write error;
  • notifier error;
  • state restore;
  • no-series/never-firing;
  • data delay;
  • result limit。

“当前无告警”不是通知测试

current alerts=0notification failures=0 不能证明:

  • route matcher 正确;
  • receiver secret 有效;
  • 外部服务接收;
  • 值班人员可达;
  • 升级路径工作。

真实 notification canary 要穿过:

probe
  -> ingestion
      -> rule
          -> VMAlert notifier
              -> Alertmanager grouping/route
                  -> external receiver
                      -> receipt acknowledgment

本章没有权限触达真实 receiver,覆盖矩阵保持 real_receipt_canary=false

变更后的最小测试矩阵

改动 必测
expression normal/threshold/below/above/missing
window onset、pending、firing、recovery
labels grouping、dedup、cardinality
route expected sink、default、no real integration
inhibition positive + must-not-inhibit
annotation owner/action/runbook/dashboard
recording exact arithmetic and labels
engine dry run、unit、error metrics
deployment canary、rollback、receipt;本章未执行

本节验收

你应当能说明:

  1. burn rate 如何由 objective target 计算;
  2. 14.4x 为什么只是 policy starting point;
  3. 两个窗口分别防什么问题;
  4. instance ratio 为什么不是 event ratio;
  5. latency rule 为什么只能走 proposed test;
  6. correctness 为什么没有 error budget;
  7. for 为什么不能修复错误 expression;
  8. recording 与 dependent alert 为什么要分 group;
  9. group_by 为什么不默认带 instance;
  10. fast/slow/ticket 应如何抑制;
  11. metamonitoring 只能抑制哪些派生告警;
  12. page 最低要绑定哪些动作字段;
  13. synthetic rule test 与 offline route test 各证明什么;
  14. 为什么它们仍不能证明真实 pager delivery。

上一节:SQL 可观测基线 · 返回本章目录 · 下一节:Pigsty 可观测体系 · 查看全书目录 · 查看索引中心

25.5 Pigsty 可观测体系

Pigsty 提供的是一套 PostgreSQL 可观测参考实现,而不是另一套数据库语义。

PostgreSQL system views
  -> pg_exporter metrics
      -> VictoriaMetrics time series
          -> recording/alert rules
              -> Grafana dashboard
                  -> Alertmanager route

PostgreSQL / PgBouncer / Patroni / pgBackRest / host logs
  -> Vector
      -> VictoriaLogs
          -> Grafana exploration

application traces, when instrumented
  -> VictoriaTraces
      -> Grafana exploration

每一层都可能:

  • 正常工作;
  • 延迟;
  • 丢数据;
  • 标签漂移;
  • 权限不足;
  • 版本升级;
  • 将一个原生值重新聚合;
  • 把缺失误写成零。

所以“Pigsty 面板显示”仍要回到 PostgreSQL 和应用合同复核。

当前官方入口:

25.5.1 采集、存储、规则、面板与通知链

metrics 采集

Pigsty PostgreSQL 监控主要汇集:

PostgreSQL   pg_exporter, usually 9630
PgBouncer    exporter, usually 9631
Patroni      REST/metrics, usually 8008 or HTTPS target
Node         node_exporter, usually 9100
HAProxy      exporter/stats
other infra  etcd, Vector, Grafana, Alertmanager, storage self metrics

实际端口、TLS 和访问控制以 inventory/rendered config 为准,不能把教学默认值 当网络策略。

沙箱 pg-test-1 的只读探测:

pg_exporter PostgreSQL   9630
pg_exporter PgBouncer    9631
Patroni API              8008
node_exporter            9100

探测 endpoint 可证明 process/HTTP 返回,不能证明:

  • 所有 SQL collector 成功;
  • 所有 database 被发现;
  • 权限足够;
  • series 新鲜;
  • rule 查询正确;
  • 用户服务健康。

target registration

Pigsty 文档中的 PostgreSQL target 文件沿用:

/etc/prometheus/targets/pgsql/

命名不意味着当前存储一定是旧版 Prometheus;Pigsty v4 的当前栈使用 VictoriaMetrics。target 记录:

labels:
  cls: pg-test
  ins: pg-test-1
  ip: 10.10.10.11
targets:
  - 10.10.10.11:9630
  - 10.10.10.11:9631
  - 10.10.10.11:8008

这个 external identity 会与 raw exporter metric-specific labels 合并。

raw exporter 与存储后的 label 不同

直接访问 :9630/metrics

pg_activity_count{datname="test",state="idle"} 1

写入 VictoriaMetrics 后还会带:

cls="pg-test"
ins="pg-test-1"
ip="10.10.10.11"
instance="10.10.10.11:9630"
job="pgsql"

因此调试 label 漂移要分别看:

  1. exporter raw output;
  2. target/relabel config;
  3. storage series;
  4. recording result。

只看一层可能误判 producer。

pg_exporter 是 SQL 到 metric 的编译层

它的配置定义:

query
minimum PostgreSQL version
timeout
cache
tag columns
metric columns
type
description
scale

同一个 view 可以变成多条 metric。升级 PostgreSQL/pg_exporter/Pigsty 后:

  • 列可能增加;
  • metric 名可能变化;
  • tag 可能变化;
  • 旧 dashboard/rule 可能失配;
  • 权限 schema 可能变化。

生产需要 versioned config 和 compatibility test。

exporter 内建最小信号

pg_exporter 即使不加载大量自定义 collector,也有基本自描述/连通信号,例如:

pg_up
pg_version
pg_in_recovery
pg_exporter_build_info

它们适合判断:

  • exporter process/target;
  • database connectivity;
  • server version;
  • recovery role。

pg_up=1 不证明所有高成本 collector、扩展 view 或 application database 可读。

management endpoint 要保护

pg_exporter 的管理/配置能力不应无条件暴露给业务网络。即使 /metrics 可被 监控系统读取,也要区分:

scrape read
health read
configuration reload
profiling/debug

使用:

  • 网络 ACL;
  • reverse proxy auth/TLS;
  • bind address;
  • least-privilege service;
  • 管理面关闭或隔离;
  • access log/audit。

不要把“exporter 没有业务写权限”等同于“管理 endpoint 无风险”。

metrics storage

Pigsty v4 使用 VictoriaMetrics 保存时序。它负责:

  • ingest;
  • time-series index;
  • MetricsQL/PromQL-compatible query;
  • retention;
  • recording result;
  • rule state相关读写;
  • API。

本章快照:

VictoriaMetrics version          1.148.0
total series                     44,842
total label-value pairs          387,724
distinct metric names            3,078

这些不是“监控容量还剩多少”的答案。还要看:

  • ingest rate;
  • data size;
  • retention;
  • series churn;
  • query latency;
  • cache;
  • disk;
  • backup;
  • self errors;
  • cardinality trend。

logs storage

Vector 收集:

/pg/log/postgres
/pg/log/pgbouncer
/pg/log/patroni
/pg/log/pgbackrest
host/service logs

发送到 VictoriaLogs。沙箱 health/version 证明:

VictoriaLogs v1.52.0 endpoint healthy

本章没有读取任何日志 body,因此没有证明:

  • 所有 source 被采集;
  • parser 正确;
  • redaction 正确;
  • retention 正确;
  • query role 正确;
  • incident window 有完整日志。

endpoint health 只是第一层。

traces storage

沙箱有:

VictoriaTraces v0.9.4 endpoint healthy

但没有声称 pg36_shop 发出 application span。三件事要分开:

trace backend exists
collector receives data
specific service has complete/useful instrumentation

没有第三项,不能在架构图上把 trace 当成已覆盖。

rule evaluation

VMAlert:

  • 读取规则;
  • 向 datasource 查询;
  • 执行 recording/alert;
  • remote-write recording/state;
  • 把 alert 发给 Alertmanager;
  • 暴露自监控 API/metric。

快照:

VMAlert version         1.148.0
groups                  17
alert rules             50
recording rules         698
group/rule errors       0
current firing alerts   0

这些是 Pigsty 已有规则。第 25 章的:

18 recording + 13 alert

只经过隔离工具测试,未放进在线 VMAlert。

dashboard

Grafana 将三类 data source 组织为:

PGSQL   PostgreSQL metrics
PGCAT   PostgreSQL catalog/direct datasource
PGLOG   PostgreSQL-related logs

当前 Pigsty 文档列出约 26 个 PostgreSQL dashboard,按 overview、cluster、 instance、database 等层级组织。

dashboard 的优势:

  • 导航与变量;
  • 版本化 panel;
  • 多层关联;
  • time range;
  • release annotation;
  • top-N;
  • drill-down。

它的边界:

  • panel query 可能与告警不同;
  • 变量默认值可能聚合错 scope;
  • time range/step 改变结果;
  • downsampling 隐藏 spike;
  • 颜色阈值不等于 SLO;
  • Grafana cache/query error;
  • 直接 PG datasource 可能与 metric snapshot 不同。

Alertmanager

Alertmanager 负责:

  • group;
  • dedup;
  • route;
  • inhibition;
  • silence;
  • receiver integration;
  • repeat。

它不负责决定 expression 是否有业务意义。VMAlert 能成功发送给 Alertmanager, 也不等于外部 receiver 已收到。

快照:

Alertmanager 0.33.1
current alerts 0
notification failure counters nonzero series 0

没有 receipt canary,真实 delivery 仍是盲区。

端到端链路的健康层

证据
producer PostgreSQL view/metric raw output
scrape target up、scrape duration/error
ingest newest sample time、storage health
query exact expression result、error
rule evaluation state/error/duration
notifier VMAlert notifier success/error
route Alertmanager matched receiver
integration external API accepted
receipt human/system acknowledgment

“面板有数据”通常证明到 query;“Alertmanager 无失败”最多证明部分 notifier/ integration 路径;receipt 需要独立 canary。

Pigsty 三种监控模式

当前文档:

模式 场景 可见能力
RDS / Basic 只有可连接 PGURL PG metrics,缺 host/pool/LB/log
Managed 现有数据库且可 SSH/sudo PG + node,其他可选
Full / Standard Pigsty 创建并管理 PG/pool/LB/node/log 完整参考栈

这直接影响诊断:

RDS mode has no node metrics
  -> cannot conclude host normal from absent panel

RDS mode has no local PG logs
  -> PGLOG absence is expected capability gap

Managed optional pool
  -> check inventory before diagnosing queue

dashboard 应根据 capability 隐藏/标记 unavailable,而不是显示绿色零。

平台版本是合同的一部分

本章快照:

Pigsty           v4.5.0
PostgreSQL       18.6
pg_exporter      v1.4.0
VictoriaMetrics  v1.148.0
VictoriaLogs     v1.52.0
VictoriaTraces   v0.9.4
Alertmanager     0.33.1

旧教程若仍写 Prometheus、Loki 等历史组件,不能直接套用 v4。规则语法具有兼容 目标,但 storage、API、自监控 metric 和运行特性必须按实际版本核对。

25.5.2 以指标语义定位集群、实例、数据库和查询

三个稳定基础身份

Pigsty 使用:

cls   cluster
ins   instance/member
ip    node address

同一个:

cls=pg-test
ins=pg-test-1
ip=10.10.10.11

可关联 PostgreSQL、PgBouncer、Patroni、node、HAProxy 与 logs。

它们解决的是平台身份,不自动解决业务身份:

service
environment
operation_class
objective_id

需要应用层单独提供。

为什么同时保留 clsinsip

label 用途 变化风险
cls 服务集群聚合 cluster rename/migration
ins 稳定成员角色 rebuild/replacement
ip node/网络关联 IP 重用/迁移

IP 不是唯一永久身份;instance 也可能重建。诊断包同时保存 topology event 和 captured_at。

instance 与 exporter endpoint

storage series 还有:

instance="10.10.10.11:9630"
job="pgsql"

这个 instance 是 scrape target endpoint,不一定等于 Pigsty ins。写 query 时明确:

sum by (cls, ins, ip) (...)

不要误用 Prometheus convention 的 instance 代替 Pigsty member identity。

cluster 级

常见问题:

  • 集群是否有 primary;
  • 多少 member exporter 可达;
  • transaction/WAL 总 workload;
  • aggregate capacity;
  • replication topology;
  • entrypoint health。

查询示意:

min by (cls) (pg_up)
sum by (cls) (rate(pg_db_xact_total[5m]))
max by (cls) (pg_lag)

第一条 min(pg_up) 只是“是否每个目标 up”,不是 service availability。

instance 级

常见:

role
activity/wait
checkpointer
I/O
WAL/archive
replication sender/receiver
autovacuum
host resource

本章现场:

pg_in_recovery{cls="pg-test"}

可区分:

pg-test-1 0 primary
pg-test-2 1 replica
pg-test-3 1 replica

但角色应与 Patroni、direct SQL 和 topology event 交叉,特别是在切换窗口。

database 级

raw exporter:

pg_activity_count{datname="test",state="idle"}
pg_db_deadlocks{datname="test"}
pg_db_temp_bytes{datname="test"}

storage 加平台身份后,可以:

sum by (cls, datname, state) (
  pg_activity_count
)

database label 是 logical database 名;多个 cluster 可能都有 postgrestest,不能丢 cls

query 级

现场 pg_query_calls

pg_query_calls{
  datname="postgres",
  query="-1567903771303523871"
} 214

这里 query 的值是 numeric queryid 字符串,不是 SQL text。语义:

metric label name  query
metric label value queryid
cardinality bound  pg_stat_statements.max per tracking dimensions

查询时:

topk(
  20,
  sum by (cls, datname, query) (
    rate(pg_query_exec_time[5m])
  )
)

具体 metric 名与单位由当前 pg_exporter.yml 定义,使用前查看 HELP 和 dashboard query;不要凭记忆假设 _exec_time 是 counter 还是 seconds。

query label 仍有 cardinality 成本

即使不是 raw text:

  • pg_stat_statements.max=10000
  • 多个 database/user/toplevel;
  • 多个 instance;
  • 旧 series retention;
  • query churn;

仍可产生大量 series。current snapshot 的 top label cardinality 中 recording 有 698 个值,也说明 rule identity 自身会形成规模。

需要:

  • 只保留需要的 query metric;
  • 记录 dealloc;
  • 控制 dynamic SQL shape;
  • top-N dashboard;
  • retention;
  • 不要把 queryid 复制到 page grouping。

exact metric names 来自当前 exporter

沙箱 pg_exporter v1.4.0 实际暴露:

pg_activity_count
pg_activity_max_conn_duration
pg_activity_max_duration
pg_activity_max_tx_duration

pg_archiver_failed_count / failed_time
pg_archiver_finish_count / finish_time

pg_checkpointer_timed / req / done
pg_checkpointer_write_time / sync_time / buffers_written

pg_db_numbackends / deadlocks / temp_bytes / xact_*

pg_io_read_bytes / write_bytes / extend_bytes
pg_io_read_time / write_time / fsyncs ...

pg_lag
pg_repl_*
pg_query_calls / exec_time / io_time / rows / blocks / wal_bytes
pg_table_age / n_dead_tup / size / bloat estimates

这是版本化现场证据,不是永久 API。升级时用:

curl --fail --silent http://TARGET:9630/metrics

仅在受控网络检查 # HELP# TYPE 与 label,不要把 endpoint 公网暴露。

pg_lagpg_repl_replay_diff

现场:

pg_lag                   all observed 0
pg_repl_replay_diff      two downstream series 0

前者可能是 time-like convenience metric,后者是 replay distance;具体实现要 查 exporter SQL。两者都不能代替 commit-correlated SLI。

archiver metric 命名的实现差异

native view:

archived_count
last_archived_time
failed_count
last_failed_time

exporter 观察名:

finish_count / finish_time
failed_count / failed_time

规则作者要确认:

  • finish_time 是 Unix seconds;
  • counter type;
  • primary/replica exposure;
  • reset;
  • last success after failure;
  • missing on replica。

不能把 native column 名直接猜成 metric 名。

label join 的显式性

组合两个指标:

A
and on (service, operation_class, environment)
B

必须显式声明 join key。默认所有共同 label 会参与匹配;一侧多一个 insjob,结果可能空。

调试步骤:

  1. 分别查询 A/B;
  2. 列 label sets; 3.确定语义上应该一对一、多对一还是聚合; 4.先 aggregate; 5.用 on/ignoring; 6.检查重复结果。

不要通过删除 identity 修复 join

错误:

sum(A) / sum(B)

它可能跨 cluster/environment 聚合,虽然“有数了”,语义已丢失。

正确做法先确定 service scope,再用相同 bounded dimensions。

scrape freshness

本章正式采集使用:

timestamp(pg_up)
timestamp(pg_exporter_up)
timestamp(vmalert_iteration_total)
timestamp(alertmanager_notifications_failed_total)

并计算 newest sample age,要求不超过 180 秒。up=1 但最后样本很旧,不应视为 健康;storage 里旧值可能仍可查询。

current app SLI absence

查询:

count({__name__=~"pg36_shop_.*"}) by (__name__)

结果 series 为 0。结论:

application SLI not implemented in live sandbox

不是:

zero errors
zero latency
100% availability

这一区分贯穿全章。

25.5.3 面板结论回到 SQL、日志与主机事实复核

dashboard 是索引,不是裁判

一个 panel 应能回答:

query expression
data source
scope variables
unit
legend identity
window/step
missing behavior
link to native evidence

看不到 query 的 panel 不适合支撑高风险动作。

先检查 dashboard scope

事故前先读变量:

cluster
instance
database
query
time range
timezone
refresh interval

常见错误:

  • 选了 pg-meta 而非 pg-test
  • instance 仍是旧 primary;
  • database 选 postgres 而用户在 test
  • time range 不包含 onset;
  • browser timezone 与事件 UTC 不同;
  • panel 聚合 all instance;
  • Grafana repeat panel 隐藏一个 member。

本章 SSH 还遇到一个真实访问路径陷阱:工作站对多个逻辑 IP 的 SSH port forward 落到同一元节点。只有进入元节点再连接真实沙箱网络,并验证 hostname + Patroni scope + cluster_name,才证明查询了 pg-test-1

从 availability panel 回到 event

panel:

availability bad ratio 1h / 5m

复核:

  1. source metric 是否存在;
  2. eligible/good selector;
  3. event count;
  4. low traffic;
  5. release/route;
  6. missing;
  7. reconciliation; 8.独立 probe。

如果 app metric series 根本不存在,panel 不应该显示绿色 0。

从 connection panel 回到 activity

panel 显示连接增加:

SELECT
  backend_type,
  datname,
  state,
  wait_event_type,
  count(*)
FROM pg_stat_activity
GROUP BY backend_type, datname, state, wait_event_type
ORDER BY backend_type, datname, state, wait_event_type;

再看:

  • PgBouncer client/server/wait;
  • HAProxy queue/session;
  • application pool config event;
  • connection churn log;
  • transaction age;
  • max connection headroom。

不要直接提高 max_connections

从 lock panel 回到 blocker graph

SELECT
  a.pid AS waiting_pid,
  a.datname,
  a.application_name,
  a.wait_event,
  clock_timestamp() - a.query_start AS wait_age,
  pg_blocking_pids(a.pid) AS blockers
FROM pg_stat_activity AS a
WHERE cardinality(pg_blocking_pids(a.pid)) > 0
ORDER BY a.query_start;

然后在受限会话按 queryid/application 找 owner。panel 的 lock count 不足以 授权 terminate。

从 I/O panel 回到两个系统

PostgreSQL:

SELECT backend_type, object, context,
       sum(read_bytes), sum(read_time),
       sum(write_bytes), sum(write_time)
FROM pg_stat_io
GROUP BY backend_type, object, context;

主机:

device latency/queue/throughput
filesystem capacity
memory/page cache pressure
other process activity

只有两层同窗,才能区分 PG workload 与 host path。

从 WAL/replication panel 回到原生位置

SELECT
  application_name,
  state,
  sync_state,
  pg_wal_lsn_diff(sent_lsn, replay_lsn) AS gap_bytes,
  write_lag,
  flush_lag,
  replay_lag
FROM pg_stat_replication;

再检查:

  • Patroni role/timeline;
  • receiver;
  • slot;
  • WAL generation rate;
  • read routing;
  • commit token probe。

不要仅凭 panel 的“lag 0”关闭 freshness incident。

从 archive panel 回到时间顺序

SELECT *
FROM pg_stat_archiver;

问:

new failure or historical?
last success after failure?
WAL currently generated?
archive queue progressing?
pgBackRest check?
restore evidence age?

沙箱就是 failed_count=21 但后来成功。panel 若只把 total failed 画红,会永久 误报。

从 slow query panel 回到 reset

先看:

SELECT *
FROM monitor.pg_stat_statements_info;

再看 queryid 聚合:

SELECT
  dbid, userid, queryid, toplevel,
  calls, total_exec_time, mean_exec_time,
  shared_blks_read, temp_blks_written, wal_bytes,
  stats_since, minmax_stats_since
FROM monitor.pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 50;

检查:

  • reset;
  • dealloc;
  • member/failover;
  • calls vs mean;
  • track planning/timing;
  • dashboard delta 算法;
  • query text 权限。

从 vacuum panel 回到对象与 blocker

SELECT
  schemaname,
  relname,
  n_live_tup,
  n_dead_tup,
  n_mod_since_analyze,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 50;

再查:

  • progress;
  • old transaction/xmin;
  • slot/feedback;
  • relation size;
  • freeze age;
  • autovacuum config;
  • host I/O。

不要因为 bloat panel 红就执行 rewrite。

从 log panel 回到 pipeline

先验证:

source file exists and advances
Vector source healthy
parse errors
VictoriaLogs ingest/query
time zone
retention
redaction

然后限定:

cluster / instance / database
severity or SQLSTATE
time window
row limit

本章不导出 body;如果必须查看,在受限界面完成。

面板与规则必须共享 contract

如果 alert:

bad_ratio 1h + 5m

dashboard 却画:

5m p99

值班无法复核 alert。至少提供:

  • exact long/short expression;
  • event count;
  • threshold;
  • pending/firing start;
  • missing/freshness;
  • release marker;
  • rule evaluation error。

用 source link,不复制 payload

alert 携带:

dashboard id
query template id
time range
service/environment

而不是把 metric dump、SQL text、log body 全塞进通知。诊断包按权限拉取。

三次复核法

高风险动作前至少三类独立证据:

service symptom
  app SLI or independent probe

PostgreSQL fact
  native SQL / log / topology

platform/host fact
  pool / host / rule engine / change event

不是机械凑三条,而是让每条能 falsify 竞争解释。

Pigsty dashboard 的正确使用路径

overview
  locate service/cluster and onset

cluster
  topology, workload, replication, capacity

instance
  role, activity, I/O, WAL, maintenance

database
  transactions, tables, query workload

query/catalog/log
  focused evidence

native SQL and host
  verify before mutation

下钻过程中始终保留 time range 和 identity。

平台升级验收

Pigsty/pg_exporter/PG major 升级后:

  1. inventory/target identity;
  2. raw exporter HELP/TYPE/labels;
  3. required metric presence;
  4. series cardinality;
  5. recording rule dry run/unit test;
  6. dashboard no-data/error;
  7. alert route test;
  8. native SQL parity;
  9. log parse/redaction;
  10. production canary/rollback。

不要以“Grafana 首页能打开”验收监控升级。

本节验收

你应当能解释:

  1. Pigsty v4 metrics、logs、traces、rules、dashboards、routes 各由谁负责;
  2. raw exporter label 与 storage external label 为什么不同;
  3. clsinsip 与 scrape instance 的区别;
  4. RDS/Managed/Full 三种模式会缺哪些信号;
  5. pg_query_*query label 为什么是 queryid 而非 text;
  6. exporter endpoint up 为什么不等于所有 collector 正常;
  7. VictoriaMetrics series count 为什么需要趋势而非单次上限;
  8. VMAlert 无 error 为什么不等于本章规则已部署;
  9. Alertmanager 无失败为什么不等于 receiver 已收到;
  10. trace backend healthy 为什么不等于应用有 span;
  11. 从 connection、I/O、replication、archive、query、vacuum panel 分别回到什么原生证据;
  12. 平台升级为什么必须做 metric/rule/dashboard/native parity。

上一节:把观察契约变成告警 · 返回本章目录 · 下一节:从告警到诊断包 · 查看全书目录 · 查看索引中心

25.6 从告警到诊断包

告警只告诉你一个合同条件成立。它不是事故报告,也不是根因分析。

值班最先丢失的往往不是 metric,而是上下文:

onset 前有哪些变更?
当时谁是 primary?
哪些入口受影响?
长窗和短窗的 event count 是多少?
锁图在三分钟后消失前是什么样?
统计是否刚 reset?
采集链有没有延迟?
日志保留窗口还在吗?
值班执行的第一条查询是否改变了状态?

诊断包的任务,是在不增加事故的前提下,自动保存一个有界、脱敏、带身份与 时间语义的起点:

alert
  -> manifest
      -> symptom windows
          -> topology and changes
              -> PostgreSQL aggregates
                  -> platform/host correlation
                      -> hypotheses and falsifiers

它不能承诺:

captured everything
proved root cause
safe to mutate
compliance achieved

机器可读合同: diagnostic-pack-contract.json

25.6.1 自动保存时间窗、拓扑、变更与关键查询

诊断包从 manifest 开始

任何包先记录:

{
  "run_id": "...",
  "captured_at": "...Z",
  "trigger_alert": "...",
  "service": "pg36_shop",
  "environment": "l2-sandbox",
  "targets": ["pg-test-1"],
  "tool_versions": {},
  "source_hashes": {},
  "risk": "L0",
  "mutation": "none"
}

没有 manifest 的截图无法回答:

  • 哪次事件;
  • 哪个环境;
  • 哪一版 query;
  • 谁采集;
  • 何时采集;
  • 是否做了 mutation;
  • 是否后来被改。

target 要通过多源确认

本章正式采集没有相信工作站 SSH alias,而是:

ProxyJump via pg-meta-1
  -> actual sandbox network 10.10.10.11
      -> hostname pg-test-1
      -> Patroni scope pg-test, name pg-test-1
      -> PostgreSQL cluster_name pg-test
      -> pg_is_in_recovery false
      -> two pg_stat_replication rows

如果其中冲突:

stop
classify identity mismatch
do not continue to mutation

连接成功不是身份验证。

时间窗要覆盖 onset 前后

本章合同默认:

before alert  30m
after alert   15m
clock         UTC

这不是 universal window。选择应考虑:

  • 最长 burn window;
  • scrape/evaluation/group delay;
  • release duration;
  • query/log retention;
  • incident severity;
  • 存储成本。

至少保存:

alert starts_at
rule evaluation time
collector time
database clock
change event time

如果 after window 尚未完成,可以:

  1. 先保存 initial pack;
  2. 15 分钟后补一个 immutable supplement;
  3. 不覆盖 initial;
  4. manifest 建立 parent/child。

保存 event count,不只保存 ratio

同样 10% bad ratio:

1 bad / 10 total
10,000 bad / 100,000 total

动作和置信度不同。SLO section 保存:

  • good/bad/eligible count 或 rate;
  • long/short window;
  • threshold;
  • burn;
  • low-traffic flag;
  • missing/freshness;
  • independent probe;
  • objective version。

不要只截图红线。

topology 是事件时刻拓扑

保存:

service entrypoints
HAProxy/PgBouncer path
cluster/member role
Patroni timeline
replication state
declared dependencies
read/write routing

不自动保存:

  • client address;
  • password;
  • connection URI;
  • inventory secret。

IP 是否允许进入私密包取决于政策;本章公开摘要只保留教学网络身份。

变更时间线

收集:

application release
schema migration
PostgreSQL parameter/config
pool size/mode
HAProxy route
failover/switchover
node restart
backup/restore
monitoring rule/dashboard
credential/security policy
capacity/retention

每个 event:

event id
actor role
target
requested/approved/executed times
result
rollback/roll-forward
evidence link

不要直接收集个人聊天全文作为唯一 change log。

PostgreSQL section

本章自动保存:

activity aggregate by client state/wait type
lock aggregate
replication identity/state/gap without client address
pg_stat_wal
pg_stat_checkpointer
pg_stat_archiver
nonzero pg_stat_io aggregate
database aggregate
maintenance aggregate
pg_stat_statements aggregate/reset, no query text

为什么 activity 只自动聚合:

  • 完整 query text 敏感;
  • PID 列表可能很大;
  • 一次 current sample 很快过时;
  • 自动证据的目标是安全起点。

若 incident 需要 blocker graph,runbook 再用受限角色采集二级包。

SQL query controls

采集连接先执行:

SET statement_timeout = '5s';
SET lock_timeout = '500ms';
SET default_transaction_read_only = on;
BEGIN READ ONLY;

并禁止:

EXPLAIN ANALYZE
pg_stat_reset*
pg_stat_statements_reset
VACUUM / ANALYZE
CHECKPOINT
cancel/terminate
DDL/DML
load generation
failover
restore
reload

READ ONLY 不是绝对安全证明:

  • SELECT 仍可很贵;
  • function 可能具有副作用,具体取决于实现与权限;
  • 系统视图查询可拿锁;
  • external extension 可能访问资源。

因此同时使用 allowlisted query、timeout 和 least privilege。

queryid-level top list

自动保存:

dbid
userid or role class
queryid
toplevel
calls
total/mean execution
rows
blocks/temp/WAL
stats_since/minmax_stats_since

限制:

top 50
no query text
no bind
no client address

top 50 不是完整 workload。manifest 要写:

sort key
limit
window/reset
source member

平台 section

保存:

  • exporter/target freshness;
  • VictoriaMetrics health/series;
  • VMAlert groups/rules/error/missed iteration;
  • Alertmanager failure counter;
  • node resource trend;
  • 日志 query count,不含 body;
  • component versions。

本章公开摘要包括:

VMAlert 17 groups
50 alert rules
698 recording rules
0 rule errors

它是采集时刻 baseline,不能事后覆盖。

文件布局

建议:

incident-<id>/
├── manifest.json
├── symptom.json
├── topology.json
├── changes.json
├── postgresql.json
├── platform.json
├── hypotheses.json
├── validation.json
└── hashes.json

正文和 body 类材料如确需保存,放在更严格的 evidence tier,不与常规诊断包 混合。

分区大小上限

本章每 section:

manifest        64 KiB
symptom         256 KiB
topology        256 KiB
changes         256 KiB
postgresql      1 MiB
platform        1 MiB
hypotheses      256 KiB

超限时:

  • 不应无限截断而不标记;
  • 记录 truncated=true
  • 保存 total count;
  • 使用 top-N;
  • 提供 restricted source link;
  • 按需发起二级采集。

初始包与后续包

T0 automatic
  cheap, bounded, no text

T1 operator
  focused blocker/query/log under incident authority

T2 mutation evidence
  before/after, approval, command, verification

T3 postmortem
  decisions, cause, counterfactual, actions

不要让 T0 自动化拥有 T2 权限。

source hash 与可重放

保存:

  • collector version;
  • query template hash;
  • rule/config hash;
  • upstream governance run id;
  • output hash;
  • capture parameters。

这样以后能回答:

same data, same query?
same rule version?
same target?

hash 证明 byte identity,不证明内容真实或政策正确;仍需身份和 source trust。

25.6.2 区分首发症状、伴随现象与根因证据

先写 timeline,不先写故事

事实表:

时间 来源 事实 置信度
10:01:30 release event version B 完成 high
10:03:00 user SLI latency short window 超阈值 high
10:04:00 PgBouncer metric wait queue 增长 medium/high
10:04:20 trace sample pool wait 占主要时间 sampled
10:05:00 PG activity active/wait 未显著变化 point sample

先不要写:

version B caused database slowdown

事实支持的初步说法:

latency symptom began after version B;
pool wait correlated;
current PostgreSQL execution evidence did not show the same growth.

四种状态标签

symptom
  contract violation directly observed

correlate
  same window changed, causal role unknown

hypothesis
  proposed mechanism with predicted evidence

cause
  mechanism corroborated, alternatives tested, repair verified

诊断包中的每条观察带 role,避免相关线自动升级成 root cause。

首发症状不是第一个 dashboard spike

“first observed” 受:

  • scrape interval;
  • evaluation interval;
  • threshold;
  • 日志延迟;
  • 时钟;
  • 采样;
  • dashboard refresh

影响。可以说:

first reliable observation available to this system

不要说“绝对第一个事件”,除非证据支持。

假设要写预测与反证

模板:

hypothesis: pool configuration reduced reusable server connections
mechanism: requests queue before PostgreSQL
predicts:
  - PgBouncer wait rises
  - edge latency rises
  - PostgreSQL active executions do not rise equally
  - host CPU/I/O remains stable
falsified_by:
  - no pool wait
  - server execution time explains latency
  - rollback does not change symptom
next_safe_query:
  - inspect pool wait and config event
mutation_required: false

这比“看起来像连接池”更可执行。

反例一:CPU 同时升高

availability fast burn
CPU high

可能机制:

  • 应用 retry storm 让 CPU 高;
  • slow query 让 CPU 高并导致错误;
  • batch 与 outage 巧合;
  • monitoring query 自身;
  • 另一 process。

需要:

queryid active work
wait state
request/transaction rate
process CPU
release/batch event
repair response

CPU 是 correlate,不是自动 cause。

反例二:replica gap 与 stale read

freshness probe bad
replica WAL gap high

强候选,但仍检查:

  • probe 确实走 replica;
  • token commit 已确认;
  • clock;
  • route;
  • cache;
  • transaction snapshot;
  • replica state/timeline。

如果切 primary path 后新 token 立即满足,而 gap 回落后 replica path 恢复, 机制更强。

反例三:归档失败计数

failed_count=21

时间线:

last failure 18:57
last success 22:27
current capture 22:48

结论:

historical failures occurred
archive later succeeded
no active failure is established by this counter alone

这是 falsification:反驳“非零 counter = 当前故障”。

反例四:锁已经消失

用户 latency 曾受 lock 影响,但初始包晚到:

current pg_locks no blocker
deadlock/lock-wait log in window
application timeout in window

不能因为 current view 空就否认历史;也不能因为日志有等待就证明当前仍阻塞。 timeline 必须保留各自时间语义。

修复验证要回到症状

如果动作是 cancel blocker:

blocker disappears

只是组件验证。还要:

  • user burn short/long window;
  • event outcome;
  • unknown writes reconciliation;
  • correctness;
  • pool/entry;
  • 副作用;
  • recurrence。

动作可能同时让 query 消失和用户失败增加。

recovery 不是瞬时绿

fast rule 的两个窗口恢复速度不同:

5m clears first
1h remains elevated

关闭事件政策可要求:

  • 短窗恢复;
  • 长窗下降趋势/低于阈值;
  • independent probe;
  • correctness check;
  • no new unknown outcome;
  • repair stable for observation period。

不要等所有长窗完全归零,也不要一个样本就关。

counterfactual

root cause review 要问:

如果没有这次变更,症状是否仍会发生?
如果只改变这个机制,症状是否恢复?
为什么监控提前没有发现?
哪个 guardrail 能阻止复发?

不能真实重放生产时,可以:

  • staging reproduction;
  • synthetic fixture;
  • plan/config diff;
  • canary;
  • independent domain evidence。

明确证据强度。

诊断包不是 postmortem

诊断包:

raw-ish bounded facts
initial hypotheses
capture boundary

postmortem:

impact
timeline
cause/contributing factors
detection/response
decisions
counterfactual
actions/owners/dates

不要在自动包里预填 root_cause=database

25.6.3 证据采集本身的负载和权限边界

observer effect

采集会:

  • 建立连接;
  • AccessShareLock
  • 读取 shared stats;
  • 执行 sort/aggregate;
  • 查询 storage;
  • 扫描日志;
  • 占网络与磁盘;
  • 出现在 pg_stat_activity/pg_stat_statements
  • 影响被观察系统。

目标是有界,不是声称零影响。

风险分级

采集 风险 默认
health/version/small metric L0 自动
bounded system-view aggregate L0 自动
top queryid without text L0 自动,有 timeout
blocker graph/PID detail L0/L1 data-sensitive incident role
bounded log body sensitive 二级授权
raw SQL/bind export high data risk 禁止自动
EXPLAIN low/moderate 评估 function/lock
EXPLAIN ANALYZE executes statement 禁止自动
statistics reset destructive observation 禁止
load/fault injection mutation 隔离演练

EXPLAIN ANALYZE 会执行

对 DML:

EXPLAIN ANALYZE DELETE ...

真的执行 DELETE,除非在可回滚事务中且没有外部副作用;即便 rollback,也会:

  • 运行 trigger/function;
  • 拿锁;
  • 产生 WAL/side effect;
  • 消耗资源;
  • 影响 sequence/external service 等。

诊断自动化不得把它当只读。

function 与 extension

SELECT 可以调用:

  • volatile function;
  • security definer;
  • external API;
  • advisory lock;
  • file/extension;
  • sleep。

allowlist system catalog/view query,不接受 incident 参数拼任意 SQL。

query timeout

本章:

statement_timeout  5s
lock_timeout       500ms
parallel capture   max 4
top query          50
log rows           1000

timeout 后:

  • 记录 section incomplete;
  • 保存 error class;
  • 不在 tight loop 重试;
  • 不自动提高 timeout;
  • 交给 operator 决定替代 source。

并发限制

对 100 个 database 同时执行 catalog query,单条很轻也会形成 load spike。

策略:

priority:
  affected service/database first

concurrency:
  fixed small pool

jitter:
  avoid synchronized collectors

deadline:
  stop when incident value decays

cache:
  reuse inventory/version

log query cost

无界:

all clusters
30 days
regex on full message
no limit

会拖慢 VictoriaLogs,也可能返回大量敏感正文。

有界:

exact cls/ins/database
45m window
severity/SQLSTATE structured filter
limit 1000
count/group first
body only under secondary authorization

metric query cost

危险模式:

  • regex 匹配所有 metric;
  • 高基数 query label 长窗口;
  • subquery 小 step;
  • topk 前未聚合;
  • cross join;
  • dashboard 多 panel 同时 refresh;
  • incident automation 重复相同查询。

记录:

  • expression hash;
  • start/end/step;
  • series count;
  • duration;
  • result size;
  • timeout/error。

权限

数据库:

machine exporter
  dedicated monitoring role, stable views

automatic diagnostic
  aggregate-only allowlist

interactive incident
  time-bounded pg_read_all_stats or narrower

mutation operator
  separate role/approval

监控平台:

metric read
log metadata read
log body read
rule edit
silence create
receiver secret
admin

不应是一个万能账号。

evidence 文件权限

本章 private bundle:

directory 0700
files     0600

reviewer 检查所有文件 mode。公共仓库只放 allowlist summary:

  • 版本;
  • 计数;
  • pass/fail;
  • declared gaps;
  • run id;
  • production gate。

evidence secret scan

自动扫描:

  • SCRAM verifier;
  • private key;
  • credential-bearing URI;
  • authorization header;
  • clear password JSON;
  • query/raw_sql/sql_text 字段。

扫描通过不证明绝对无秘密:

  • 未知格式;
  • encoded value;
  • 业务 PII;
  • hash 可重识别;
  • file name。

仍要 data classification 与人工 review。

retention

本章 private evidence 教学默认 30 天;生产需独立政策。决定:

  • 事件/合规需求;
  • 敏感程度;
  • 调查周期;
  • 删除;
  • legal hold;
  • backup;
  • 访问 audit;
  • hash/manifest。

不要因为是“监控证据”永久保存。

完整性与可更新性

原始 initial pack 不覆盖。修正错误时:

new supplement
references original run_id/hash
states correction reason
preserves original

公开摘要可以更新展示,但应保留 underlying immutable run reference。

failure-safe behavior

若 capture 失败:

do not report empty as healthy
mark section incomplete
record error without secret
continue independent low-cost sections
page metamonitoring if critical path
do not mutate database to make capture pass

本章 validator 对 application SLI absence 就是:

declared expected gap

而不是失败或绿色。

自动化权限不能随事故升级

事故 severity 变高,不代表 collector 自动获得:

  • superuser;
  • SSH root;
  • raw log body;
  • query text export;
  • failover;
  • terminate;
  • restore。

需要新 authority 的动作必须停下来进入 SOP/change plan。

诊断入口输出

给第 31 章的输入:

manifest identity/time/version
user symptom and windows
entrypoint/path
PostgreSQL state/counters/reset
top queryid aggregates
topology/change timeline
host/platform correlation
known gaps
hypotheses and falsifiers
permissions/risk

第 31 章再按症状分支:

slow query
lock/deadlock
connection/pool
replication/freshness
disk/capacity
vacuum/freeze

本节验收

你应当能说明:

  1. 诊断包为什么先有 manifest;
  2. target 为什么要多源确认;
  3. 为什么同时保存 alert、collector 和 database clock;
  4. ratio 为什么必须带 event count;
  5. topology 与 change event 为什么要保存事件时刻版本;
  6. automatic pack 为什么只保存 queryid 聚合;
  7. T0/T1/T2/T3 四层 evidence 权限如何分开;
  8. symptom/correlate/hypothesis/cause 的措辞差异;
  9. 假设为什么必须带预测与反证;
  10. repair 为什么要回到 user/control signal 验证;
  11. EXPLAIN ANALYZE 为什么不能自动运行;
  12. timeout/limit/concurrency 如何让采集 fail-safe;
  13. 0700/0600 与 public allowlist 各解决什么;
  14. secret scan 为什么仍不能替代数据分类;
  15. 采集失败为什么必须是 incomplete/unknown 而不是 healthy。

上一节:Pigsty 可观测体系 · 返回本章目录 · 下一节:实战:实现并演练观察契约 · 查看全书目录 · 查看索引中心

25.7 实战:实现并演练观察契约

本节把全章压成一条可重放的证据链:

bind ch24 observation contract
  -> validate signal and diagnostic-pack semantics
      -> compile 18 recording rules
          -> compile 13 alert rules
              -> preserve 7 accepted policies
                  -> quarantine 6 proposed policies
                      -> read live Pigsty/PostgreSQL baseline
                          -> verify target identity
                              -> run isolated VMAlert tests
                                  -> run offline Alertmanager routes
                                      -> test inhibition positive/negative
                                          -> reject 25 counterexamples
                                              -> publish 14-row coverage matrix
                                                  -> keep production gate pending

它刻意把两种动作分开:

online baseline
  L0, read-only, no mutation

isolated exercise
  L1-ephemeral, temporary loopback processes and files
  no online rule, no online alert, no real receiver

实验合同:

25.7.1 为延迟、错误、复制、备份和资源建立规则

文件布局

static/labs/ch25/
├── requirements.json
├── signal-contract.json
├── coverage-matrix.json
├── diagnostic-pack-contract.json
├── recording-rules.yml
├── alert-rules.yml
├── rule-tests.yml
├── alertmanager-sandbox.yml
├── route-tests.json
├── negative-cases.json
├── topology.mmd
├── lab-contract.md
├── capture.py
├── exercise.py
├── validate.py
├── review.py
├── task.sh
└── observability-run.json

职责:

文件 证明什么
requirements target、risk、上游、hard rejection、gate
signal contract source/type/label/reset/missing/cost
recording rules SLI window 与诊断聚合
alert rules accepted/proposed policy
rule tests synthetic arithmetic、pending、firing、recovery
AM config empty sink route 和 inhibition
route tests 八个 route、五个 inhibition 正反例
diagnostic pack 自动证据字段、limit、权限
coverage 已有信号、预期缺口、fallback
negative cases validator 不是只会通过
capture live L0 baseline
exercise remote /tmp isolated test
review file mode、secret、claim、count
public summary allowlist 结果,不含 private evidence

上游 binding

第 24 章四份输入按 hash 绑定:

observation-contract.json
alert-candidates.json
slo-policy.json
governance-run.json

并要求:

run_id              34909737-527a-460c-927c-d9d71c93aa13
production_ch24_gate pending

如果上游改了 target、alert policy 或 missing semantics,本章 capture 不能继续 假装基于旧合同。

记录规则:可用性

五个窗口:

rate5m
rate30m
rate1h
rate6h
rate3d

表达式:

sum by (service, operation_class, environment) (
  rate(pg36_shop_request_outcomes_total{
    eligible="true",
    outcome!="good"
  }[5m])
)
/
sum by (service, operation_class, environment) (
  rate(pg36_shop_request_outcomes_total{
    eligible="true"
  }[5m])
)

注意三个设计:

  1. 分子是 bad eligible event,不是 instance;
  2. 分母没有任意 clamp_min(...,1) 改变低流量比率;
  3. 缺失由独立 freshness/metamonitoring 处理,不补健康零。

instrumentation 必须预初始化 outcome category,或者用能区分“零 bad”与“bad series 未产生”的设计。

记录规则:延迟

1 -
sum by (service, operation_class, environment) (
  rate(pg36_shop_request_duration_seconds_bucket{
    eligible="true",
    le="0.25"
  }[5m])
)
/
sum by (service, operation_class, environment) (
  rate(pg36_shop_request_duration_seconds_count{
    eligible="true"
  }[5m])
)

产生 5m/1h bad ratio。

规则可计算,但 alert governance 尚未接受:

route: test
severity: candidate
governance_status: proposed-not-accepted

记录规则:新鲜度

sum by (service, read_path, environment) (
  rate(pg36_shop_commit_visibility_probes_total{
    eligible="true",
    within_bound="false"
  }[5m])
)
/
sum by (service, read_path, environment) (
  rate(pg36_shop_commit_visibility_probes_total{
    eligible="true"
  }[5m])
)

它不读取 pg_lag,因为用户 SLI 必须携带 commit token。

记录规则:PostgreSQL 诊断

pg36:replica_replay_distance_bytes:max
pg36:archive_failures:increase15m
pg36:longest_transaction_seconds:max
pg36:table_freeze_age:max
pg36:dead_tuples:sum
pg36:exporter_unavailable:max

这些是原因/容量输入,不自动 page。

记录规则:metamonitoring

pg36:vmalert_rule_errors:sum
pg36:vmalert_missed_iterations:increase15m
pg36:notification_failures:increase15m

一个重要限制:notification failure 为零仍不能证明 receiver receipt。还需要 外部 canary。

accepted 七条

validator 逐字段与第 24 章比对:

alert route for
availability fast page 2m
availability slow page 5m
availability budget ticket 15m
freshness fast page 2m
correctness mismatch page 0m
capacity horizon ticket 1h
monitoring path broken page 5m

比较:

  • route;
  • severity;
  • objective;
  • owner;
  • runbook;
  • first safe action;
  • governance status;
  • for
  • multiwindow token。

任意漂移使 lint 失败。

proposed 六条

alert 用途 为什么不直接上线
latency fast burn 用户延迟 ch24 尚未接受 page policy
restore evidence stale recovery control route/severity 待接受
archive failure active recovery risk threshold/pgBackRest 联动待评审
transaction age maintenance/capacity workload-specific policy
freeze age wraparound horizon 应从配置/rate 生成
SLI missing observation path expected traffic/fallback 待实现

统一:

route: test
severity: candidate
governance_status: proposed-not-accepted

validator 会拒绝 candidate 进入 page/ticket sink。

archive candidate 不比较历史总数

pg36:archive_failures:increase15m > 0
and on (cls, ins, ip)
(time() - pg_archiver_finish_time) > 900

同时要求:

  • 15 分钟出现新失败;
  • 成功推进已经停滞。

仍需 runbook 复核 pgBackRest 和 restore evidence。

transaction candidate

pg36:longest_transaction_seconds:max > 900

只走 test,因为:

  • batch 可能合法;
  • idle in transaction 与 active 不同;
  • cancel 权限与 unknown outcome;
  • 阈值依赖 workload;
  • user impact 可能不存在。

first action 是找 owner/queryid 与安全性,不是 terminate。

freeze candidate

pg36:table_freeze_age:max > 1000000000

固定值仅用于 synthetic test。生产应使用:

current age
current autovacuum_freeze_max_age
table override
consumption rate
vacuum throughput
blocker
safety margin

capacity accepted 规则仍需 reviewed input

pg36_shop_capacity_horizon_days < 14
and on (service, environment)
pg36_shop_capacity_forecast_reviewed == 1

accepted 是告警合同,未表示 forecast exporter 已实现。coverage 仍标记应用层 缺口。

rules 与 groups

三组 recording 和四组 alert 分开。原因是 VMAlert 的 recording result remote write 是异步的;同 group 依赖上一条结果可能读不到本轮。

25 个反例之一会把 alert 塞回 recording group,确认 validator 拒绝:

recording group contains an alert and creates rule chaining

当前 live baseline

capture.py 只读:

VictoriaMetrics   health/API/query
VictoriaLogs      health/version
VictoriaTraces    health/version
VMAlert           rules/alerts/self metrics
Alertmanager      health/alert count/self metrics
pg_exporter       raw HELP/name/label inventory
PgBouncer exporter response
Patroni           selected identity/state
PostgreSQL        bounded read-only SQL

不会读取 Alertmanager config,因为可能有 receiver secret;只保存 count 与 configuration_exported=false

PostgreSQL target identity

工作站配置:

10.10.10.10 -> local port 2222
10.10.10.11 -> local port 2200
...

现场发现多个转发落到元节点。正式路径改为:

ssh ProxyJump meta
  target HostName=10.10.10.11 inside sandbox

并验证:

hostname            pg-test-1
cluster_name        pg-test
Patroni             pg-test/pg-test-1
pg_is_in_recovery   false
replication rows    2

这一检查会拒绝“查询成功但目标错误”。

实际 PG observation settings

正式快照记录:

track_activities              on
track_counts                  on
track_io_timing               on
track_wal_io_timing           off
stats_fetch_consistency       cache

pg_stat_statements.track      all
track_planning                off
track_utility                 off

logging_collector             on
log_destination               csvlog
log_file_mode                 0640

auto_explain.log_min_duration 1000ms
auto_explain.log_analyze      on
auto_explain.log_timing       on
auto_explain.sample_rate      1

两个待评审:

auto_explain overhead reviewed  false
log group access reviewed       false

实验不修改配置。

25.7.2 注入可控症状,验证触发、路由、抑制和恢复

只做静态 lint

static/labs/ch25/task.sh lint

输出:

recording_rules=18
alert_rules=13
accepted_alerts=7
proposed_alerts=6
counterexamples=25-rejected
live_deployment=false
production_ch25_gate=pending

不访问远端。

正式完整运行

先创建一个新的私密路径;task.sh 也会拒绝覆盖非空目录:

export PG36_EVIDENCE_DIR=/absolute/private/new-empty/ch25-run
static/labs/ch25/task.sh all

动作:

capture
  L0 live read-only

exercise
  L1 remote temp / loopback only

verify
  positive + negative + source hash + live evidence

review
  private mode + secret scan + claim boundary

capture 的 SQL 安全边界

statement_timeout=5s
lock_timeout=500ms
default_transaction_read_only=on
BEGIN READ ONLY

query allowlist 不含:

query text
bind
client address
EXPLAIN ANALYZE
reset
vacuum/checkpoint
cancel/terminate
DDL/DML

即便如此,pg_locks 会看到采集查询自身的 relation lock;这是 observer effect,证据不会假装完全无影响。

isolated workspace

exercise.py

  1. 在元节点执行 mktemp -d /tmp/pg36-ch25.XXXXXXXX
  2. 验证路径符合严格 regex;
  3. 只上传四个无 secret 文件;
  4. 执行测试;
  5. rm -rf 只允许匹配该 prefix;
  6. 断言目录已经不存在;
  7. 输出 remote_cleanup=ok

如果 temp path 不匹配,宁可失败也不清理未知目录。

规则语法

vmalert -rule=recording-rules.yml
        -rule=alert-rules.yml
        -dryRun

证明 parser/规则结构可接受,不执行 live query。

合成时序

vmalert-tool

isolated VictoriaMetrics
simulated periodic ingestion
recording/alert evaluation
expected sample/alert comparison

availability recording 输入:

good +100/min
bad  +2/min

验证结果:

2102=0.019607843... \frac{2}{102}=0.019607843...

fast pending/firing/recovery

输入:

1h bad ratio  0.02 for 4 samples, then 0
5m bad ratio  0.02 for 4 samples, then 0
evaluation    1m
for           2m

期望:

1m  no firing alert
3m  exact alert labels/annotations
7m  no alert

这验证 expression、for、label identity 和 recovery。

其他合成用例

availability slow burn
availability budget ticket
freshness fast burn
correctness immediate
reviewed capacity ticket
monitoring path page
latency proposed route
expected traffic but SLI missing

correctness for=0m,capacity 需要 reviewed gauge,missing 不补零。

Alertmanager 配置检查

amtool check-config

发现:

global config
route
2 inhibition rules
7 receivers
0 templates

所有 receiver 只有 name,没有 integration。

八个 route

用例 期望 sink
availability page pg36-shop-page-sink
correctness page pg36-shop-page-sink
service ticket pg36-shop-ticket-sink
platform ticket pg36-platform-ticket-sink
metamonitoring page pg36-platform-page-sink
diagnostic pg36-null-sink
proposed pg36-proposed-rule-sink
unknown pg36-default-sink

使用:

amtool config routes test --config.file=...
  --verify.receivers=<expected>
  <labels...>

离线解析,不连接 live Alertmanager。

五个 inhibition

source → target 结果
fast → same service/objective slow inhibit
meta → derived missing inhibit
meta → independent user symptom do not
meta → correctness do not
fast → other service slow do not

validator 同时解析 config 和测试表;添加 broad correctness inhibition 会被 反例拒绝。

25 个对抗性变体

类别:

scope
  production data/approval promoted

online mutation
  live deploy/notification/Alertmanager

semantics
  missing healthy, timing disabled = zero

privacy
  tenant label, query text, public evidence

claims
  app metric/rules/pager claimed live

query safety
  EXPLAIN ANALYZE

alert governance
  accepted route/for drift, candidate page

time
  slow rule loses second window

component semantics
  archive historical counter

engine
  alert chained in recording group

routing
  real webhook, correctness inhibition, real receiver name

validator 不是检查“文件存在”,而是逐个 mutation 后要求出现预期 rejection。

正式运行结果

run_id      b876731e-5741-40e3-a03e-17cf7d20881b
captured    2026-07-29T22:48:21Z
target      pg36-l2-vagrant/pg-test
live risk   L0
exercise    L1-ephemeral

组件:

Pigsty            v4.5.0
PostgreSQL        18.6
pg_exporter       v1.4.0
VictoriaMetrics   v1.148.0
VictoriaLogs      v1.52.0
VictoriaTraces    v0.9.4
Alertmanager      0.33.1

在线:

VM series                  44,842
label-value pairs          387,724
VMAlert groups             17
live alert/recording       50 / 698
live rule errors           0
live current alerts        0
application SLI series     0

PostgreSQL:

primary                 pg-test-1
async replicas          2
observed WAL gap        0 / 0 bytes
pg_stat_statements rows 194
pgss calls              122,303
query text exported     false
archive failed_count    21
last success after fail true

演练:

18 recording rules       pass
13 alert rules           pass
8 routes                 pass
5 inhibition cases       pass
25 counterexamples       rejected
remote cleanup           pass
secret scan              pass

结论:

live chapter deployment  false
real receiver            false
production_ch25_gate     pending

evidence

private bundle:

observability-evidence.json
isolated-exercise.txt
validation-report.json
negative-report.json
review.txt

全部:

directory 0700
files     0600

公共摘要只包含 allowlist,不包含 source body、SQL text、log body 或 receiver config。

重验

export PG36_EVIDENCE_DIR=/absolute/private/existing/ch25-run
static/labs/ch25/task.sh verify
static/labs/ch25/task.sh review

如果 source file 在 capture 后改变,hash 校验失败。必须新跑,而不是手工改 report。

没有 reset action

本章:

  • 在线没有 mutation;
  • remote temp 自动清理;
  • isolated process 退出;
  • 没有 database fixture;
  • 没有 live rule。

所以没有数据库 reset。不要添加一个“为了形式完整”的 destructive reset。

25.7.3 输出告警覆盖表、盲区与 ch31 的诊断入口

14 行覆盖矩阵

coverage-matrix.json 包含:

5 user/control signals
6 PostgreSQL/platform observation areas
logs
traces
routing

每行:

question
source
live expected/status
rule status
fallback
owner
blind spot

五个应用缺口

coverage live fallback
availability event absent independent place-order probe
latency histogram absent end-to-end duration probe
commit freshness absent unique-token probe
correctness reconciliation absent signed reconciliation
restore evidence age absent immutable restore manifest

它们是 expected-gap,因为教学沙箱没有对应应用。

不得由:

pg_up
HAProxy up
pg_lag
backup success
zero DB errors

代填。

已有 PostgreSQL/platform 覆盖

exporter reachability
activity/transaction/wait/lock
I/O/WAL/checkpoint/archive/replication
maintenance/freeze/object
pg_stat_statements aggregate/reset
VMAlert/Alertmanager self monitoring
VictoriaLogs endpoint
VictoriaTraces endpoint

“已有”仍带盲区:

  • one sample misses transient wait;
  • PG I/O 不区分 OS cache/device;
  • dead tuple 不是 exact bloat;
  • queryid collision;
  • endpoint 不证明 body/parse;
  • backend 不证明 application spans;
  • notification failure 0 不证明 receipt。

配置观察到的待评审项

auto_explain

log_analyze   on
log_timing    on
sample_rate   1
threshold     1s
nested        on
parameter max -1

生产前:

  • 代表性 load test;
  • short statement overhead;
  • log volume;
  • parameter leakage;
  • log_timing=off 方案;
  • sampling;
  • rollback;
  • owner approval。

本章不直接说“必须关闭”,也不说“当前安全”。

log 0640

可能为 Vector collector group 提供 read。生产前:

  • actual owner/group;
  • group membership;
  • directory mode;
  • rotation;
  • central storage ACL;
  • retention;
  • redaction;
  • access audit。

alert coverage 不等于 implementation

七条 accepted alert 有规则,但 app source 不存在:

policy coverage   yes
synthetic test    yes
live data         no
live deploy       no
real route        no

因此不能说“availability monitoring complete”。

proposed queue

需要治理拍板:

  1. latency page route/severity;
  2. restore evidence stale 的 change gate;
  3. archive risk 的 threshold 与 pgBackRest 证据;
  4. long transaction workload class;
  5. freeze horizon based on config/rate;
  6. expected traffic/missing source;
  7. real receipt canary。

每项完成后:

  • 更新 ch24 governance;
  • hash binding 失效;
  • 重跑 ch25;
  • canary deploy;
  • 验证 route/receipt;
  • 保留 rollback。

生产上线前的门禁

service instrumentation
  event schema/version/cardinality

source validation
  producer/zero/missing/reset

rule validation
  synthetic + shadow/live query

governance
  owner/runbook/first action/severity

routing
  real receiver secret outside Git

delivery
  receipt canary

cost
  exporter/query/storage/log overhead

privacy
  labels/body/parameters/retention/access

deployment
  staged canary, rollback, change event

operations
  drill and evidence

完成之前:

production_ch25_gate=pending

ch31 诊断入口

本章输出按 symptom 路由给第 31 章:

慢查询

user latency windows
entry/pool wait
activity wait
pg_stat_statements reset + top queryid
I/O/temp/WAL
release/plan event
host evidence

锁与死锁

user outcome
current blocker graph
lock wait/deadlock log aggregate
transaction age/xmin
application owner
safe cancellation boundary

连接与池

edge queue
HAProxy/PgBouncer client/server/wait
PG active/idle/idle-in-xact
connection config change
max connection headroom

复制与新鲜度

commit token probe
read path
Patroni role/timeline
sender/receiver position
slot/WAL retention
route change

磁盘与容量

filesystem horizon
PG I/O bytes/time
device latency/queue
WAL/archive
object growth
rewrite/backup headroom

vacuum 与冻结

old xact/xmin
table estimates
progress
freeze age/config/rate
slot/feedback
host I/O

诊断入口的统一字段

run id
captured at
service/environment
target identity
symptom windows/count
topology
changes
PostgreSQL reset and aggregates
platform freshness
known gaps
hypotheses/falsifiers
risk/authority

第 31 章不需要从“打开所有 dashboard”重新开始。

完成标准

本章不是以“31 条规则存在”完成,而是:

semantics are explicit
accepted and proposed are separated
live and isolated are separated
missing is not healthy
rules are tested
routes are tested without delivery
inhibition has negative tests
native PG evidence is bounded
privacy and cost are explicit
blind spots are published
production gate remains honest

本节验收

你应当能复述:

  1. 18 条 recording rule 分哪三组;
  2. 13 条 alert 中哪七条 accepted、哪六条 proposed;
  3. archive rule 为什么同时看增量与 last success;
  4. target identity 为什么需要 ProxyJump 后三源校验;
  5. capture 与 exercise 的风险/权限边界;
  6. synthetic time series 如何验证 pending/firing/recovery;
  7. route test 为什么使用空 receiver;
  8. inhibition 为什么必须有 must-not-inhibit;
  9. 25 个反例覆盖哪些失败模式;
  10. private evidence 与 public summary 如何分离;
  11. 五个 application gap 为什么不能用 PG component metric 替代;
  12. auto_explain0640 为什么是待评审而非自动修复;
  13. 生产门禁还缺哪些步骤;
  14. 第 31 章如何使用本章诊断包。

上一节:从告警到诊断包 · 返回本章目录 · 下一章:胸有成竹:容量规划与压测基线 · 查看全书目录 · 查看索引中心