# 第一原则：保护现场与可恢复性

LLMS 索引： [llms.txt](/llms.txt)

---

事故现场不是静止的。应用在重试，Patroni 在判断角色，WAL 在产生和回收，日志在轮转，
backup retention 在清理历史，运维人员的查询本身也会留下连接和日志。所谓“保护现场”
不是把所有东西停住，而是知道**谁还会改变什么**，有选择地围住最危险的状态转移，并
为恢复保留必要的 WAL、备份、拓扑和业务边界。

## 31.2.1 暂停自动化、危险变更与证据覆盖 {#item-31-2-1}

### 先列控制器，再决定是否暂停

Pigsty 集群中常见的主动控制器包括：

| 控制器 | 可能继续做什么 | 盲目停止的代价 |
|---|---|---|
| Patroni + DCS | 角色管理、重启、failover、配置协调 | 丧失自动保护；pause 也不是 fencing |
| HAProxy / VIP / pool | 根据健康检查改变流量去向 | 现有连接与新连接可能走不同节点 |
| 应用重试与 job | 继续 DML、DDL、回填、消费消息 | 错误或负载继续扩大 |
| WAL archiver / backup | 保存恢复链、执行保留策略 | 停 archive 可能填满 `pg_wal` 并缩短恢复窗口 |
| autovacuum / maintenance | 清理 tuple、冻结 XID、重建对象 | 全局停用会引入膨胀和 wraparound 风险 |
| 日志轮转与 telemetry retention | 覆盖旧日志、聚合或丢弃高基数字段 | 取证窗口变短；全部开启 debug 又可能泄密或打满盘 |

正确的“冻结记录”应逐项写：

```text
controller       refund-consumer / Patroni / backup retention
observed_state   running, config hash abc..., owner team-x
exact_scope      run_id=backfill-20260730 only
reason           stop new external side effects
expected_effect  queue remains retained; other consumers continue
resume_owner     business owner
resume_condition reconciliation manifest approved
```

优先围住制造新歧义的 writer、重试和错误 job。不要为了“干净现场”关闭 WAL 归档、删除
旧备份或把所有监控改成 debug；这些动作可能同时破坏恢复能力和磁盘余量。

### Patroni pause 不是“时间停止”

Patroni 的 pause mode 适合大版本升级、损坏恢复等需要暂时脱离自动管理的特殊操作，但
其[官方语义](https://patroni.readthedocs.io/en/latest/pause.html)很具体：

- member key 和 primary 的 leader lock 仍会更新；
- Patroni 仍可能对运行中的成员做只读查询；
- 人工 restart、failover/switchover 和 reinitialize 仍可执行；
- 发现 parallel primaries 时只告警，并不会自动 demote 无 leader lock 的 primary；
- PostgreSQL 停止后不会被自动拉起。

所以 `patronictl pause` 既不是写围栏，也不是禁止所有人工动作。使用前必须先记录成员、
leader、timeline、路由和现有 pause 状态，明确谁负责 `resume`，并验证旧 writer 已在
网络、存储或服务层被独立围住。单纯停止 Patroni 进程更不是维护模式。

### 暂停要有过期条件

每个临时控制都要有：

```text
start UTC
owner
exact target
automatic expiry or review time
health signal while paused
resume command and verifier
```

没有恢复条件的临时限流会变成永久容量损失；遗忘的 Patroni pause 会让后续故障不再
自动恢复；遗忘的 archive/retention 变更会在几小时后制造第二次事故。事件结束条件中
必须包含“所有临时控制已复位或进入有 owner 的计划变更”。

## 31.2.2 记录时间、拓扑、版本、告警与最近变更 {#item-31-2-2}

### 建立 incident envelope

第一份现场包不需要“把服务器全抄走”，但必须能回答：

```text
incident id / collector / UTC / clock source
environment / cluster / service endpoint
PostgreSQL version / system identifier / timeline / LSN
primary and replica observations / Patroni and DCS state
HAProxy / pool / VIP route
backup stanza / latest backup / archive boundary
recent deploy / DDL / config / secret / storage / network change
active user impact and business marker
every artifact's source, collection command, hash and access class
```

`system_identifier` 区分不同 PostgreSQL 集群，timeline 识别 failover 后的历史分支，LSN
描述同一 timeline 上的位置；三者不能互相替代。连接到“一个叫 production 的端点”
并不能证明取到了预期数据副本。

运行中的 PostgreSQL 可以在短超时只读事务中取 control identity：

```sql
\set ON_ERROR_STOP on
SET statement_timeout = '5s';
SET lock_timeout = '500ms';
SET default_transaction_read_only = on;

BEGIN READ ONLY;
SELECT
  clock_timestamp() AS observed_at,
  current_setting('cluster_name') AS cluster_name,
  current_setting('server_version') AS server_version,
  pg_is_in_recovery() AS in_recovery,
  s.system_identifier,
  c.timeline_id,
  c.checkpoint_lsn,
  c.redo_lsn,
  c.checkpoint_time
FROM pg_control_system() AS s
CROSS JOIN pg_control_checkpoint() AS c;
COMMIT;
```

离线数据目录或只读取证副本可以使用
[`pg_controldata`](https://www.postgresql.org/docs/18/app-pgcontroldata.html)，但报告中
必须写清楚读取的是哪个路径、当时是否运行、工具版本和文件来源，不能把两个节点的
输出拼成一条时间线。

### 动态状态、累计统计和日志不是同一种证据

PostgreSQL
[`pg_stat_activity`](https://www.postgresql.org/docs/18/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW)
描述当前 backend；`pg_stat_replication` 描述当前 walsender；`pg_stat_database`、
`pg_stat_archiver` 等是累计计数。官方文档指出累计统计不会瞬时更新，并且默认在同一
事务内缓存；clean shutdown 可以保存统计，而 crash、base backup 启动和 PITR 会重置
累计计数
（[Cumulative Statistics System](https://www.postgresql.org/docs/18/monitoring-stats.html)）。

因此：

- `failed_count = 21` 不等于当前 archive 正在失败，要比较 reset、last failure、
  last success 和当前 WAL；
- `deadlocks = 0` 必须同时记录 `stats_reset`；
- 一张事务内的静态快照适合关联字段，连续变化率则要用多个有时间的样本；
- 采集活动时默认按 state、wait 和 age 聚合，原始 SQL、bind value、client address
  只在必要且获授权时进入受控证据。

PostgreSQL 日志还有轮转与覆盖策略；`log_truncate_on_rotation` 在特定文件命名下可以
周期性覆盖旧内容
（[PostgreSQL Logging](https://www.postgresql.org/docs/18/runtime-config-logging.html)）。
应先复制事故时间窗内的精确文件并计算散列，而不是先改 logging 配置或执行一次会刷屏
的诊断。

### 在 Pigsty 中跨层对齐

Pigsty 的 `FULL/L3` 监控把 PostgreSQL、PgBouncer、Patroni、HAProxy、主机与日志用
`cls`、`ins`、`ip` 等标签关联。事故时可以从 PGSQL Alert → Cluster → Service /
Patroni / Replication / Persist → Instance / Session / Query 逐层缩小，而不是只看
一张总览
（[Pigsty Dashboard](https://pigsty.io/docs/pgsql/dashboard/)）。

命令行可以用：

```bash
pig context -o json
```

获取主机、PostgreSQL、Patroni、pgBackRest 与扩展上下文
（[pig context](https://pigsty.io/docs/pig/cmd/#pig-context)）。把它当采集起点而非唯一
真相：还要用 SQL role、Patroni REST、DCS 和客户端端点交叉验证。配置和 secret 文件
通常只保存版本号、权限和 SHA-256，不把内容直接放进普通事件频道。

### 证据清单本身也要可审计

```text
artifact       patroni-members.json
observed_at    2026-07-30T02:05:03.441Z
collector      oncall-a
source         three declared REST endpoints
scope          state / role / version / timeline only
sha256         ...
redaction      connection_url and tags removed
access         incident-restricted
```

NIST SP 800-61r3 要求保留 incident data 的完整性和 provenance，同时保护响应记录的
机密性。散列只能证明“以后看到的 bytes 没变”，不能证明采集命令正确、时钟准确或源
本身可信；这些信息要由 provenance 补齐。

## 31.2.3 先克隆、隔离或只读，再做破坏性尝试 {#item-31-2-3}

### 保存原件，实验只在工作副本

可能改写数据页、WAL、catalog 或时间线的动作，先问：

```text
原始状态是否已有独立副本？
这个副本在什么一致性边界创建？
数据目录、tablespace 和 WAL 是否属于同一快照？
工作副本是否与生产网络和路由隔离？
若动作失败，原始副本和恢复链是否仍可用？
```

推荐分成三份：

| 副本 | 目的 | 允许动作 |
|---|---|---|
| 原始证据 | 保留最早可得状态 | 不直接启动或修复；受控访问 |
| 工作副本 | 执行 `amcheck`、WAL 分析、恢复和修复试验 | 可销毁、可重新生成 |
| 恢复候选 | 通过验证后准备回灌或接管 | 只执行 runbook 声明动作 |

“把目录复制了一份”并不自动得到有效 physical backup。运行中对 PGDATA 做普通文件复制
可能跨越不同页和 WAL 时刻；带 tablespace 的集群还可能跨多个文件系统。使用
pgBackRest、PostgreSQL backup API 或由存储平台保证一致性的原子快照，并记录其
crash-consistent / application-consistent 语义。

### 副本不等于历史

物理 replica 会重放主库已经提交的错误 `DELETE`，不能当成误操作前的时间胶囊；
共享故障域中的存储损坏也可能影响多个副本。每份候选都要独立标注：

```text
system identifier
timeline and fork point
last replayed / checkpoint LSN
capture time and clock
checksum state
backup and archive ancestry
business manifest
```

只有这些边界与恢复目标匹配，副本才有资格成为数据来源。

### “只读”有多个层次

- SQL `BEGIN READ ONLY` 阻止普通数据库写事务，但不冻结其他会话、WAL replay 或后台
  状态变化；
- 只读业务路由只约束经过该端点的应用，不约束 owner、scheduler 或直连；
- 文件系统只读保护 bytes，却可能使 PostgreSQL 无法完成 crash recovery，不能在
  原始取证挂载上强行启动；
- snapshot clone 通常是独立可写工作副本，但 CoW 底层和保留策略仍要记录。

因此应保存不可变原件，再从它派生可写工作副本，而不是把“只读”当作万能开关。

[`pg_waldump`](https://www.postgresql.org/docs/18/pgwaldump.html)主要用于 debug 和教学，
官方还提醒在 server 运行时可能给出错误结果。对事故 WAL 的分析应固定工具版本、
timeline 和 segment 范围，优先在复制出的 WAL 上完成；不要为了让工具读取而随意改名
或移动现役 `pg_wal` 文件。

### 破坏性尝试必须留下退出线

下列动作默认只在工作副本：

```text
zero_damaged_pages
pg_resetwal
手工 page/file copy
强制 catalog 修改
无来源约束的 REINDEX / VACUUM FULL
覆盖式 PITR
失败后继续使用同一个 pg_rewind target
```

即便工作副本“修好了”，也要再回答：丢了哪些 tuple、违反了哪些业务不变量、能否从
WAL/备份/其他副本补齐、修复步骤能否重复、生产切换后如何对账。能启动只是一条证据，
不是完整性结论。

---

[上一节：事件分级与响应目标](../01/) · [返回本章目录](../) · [下一节：从症状路由而不是猜根因](../03/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
