跳至内容

21.4 恢复流程与验证

恢复是一场受控的数据分叉:

select historical lineage
  -> copy base
      -> replay WAL
          -> stop at boundary
              -> possibly promote onto new timeline
                  -> decide what may consume this history

每一步都可能生成一个能启动、却不应交付的实例。因此流程必须先写停止条件, 再写命令。

21.4.1 选择备份集、目标时间和恢复位置

先冻结事故事实

开始恢复前,保留:

incident start and detection time in UTC
suspected destructive transaction/request
source cluster identity and current timeline
current repository catalog
archive maximum and gaps
relevant application/audit logs
last known correct business marker
authority and requested outcome

不要一边猜 target,一边让日志、WAL、对象 lifecycle 和源状态继续变化而没有 快照证据。

target 不是一句“十分钟前”

把 target 写成结构化决策:

kind: name
value: before_release_20260729
inclusive: not-applicable
timezone: UTC
source_evidence:
  - pg_create_restore_point result
  - release change record
expected:
  present:
    - schema version 41
    - business token A
  absent:
    - migration transaction B
approved_by:
  - incident commander
  - data owner

时间 target 应包含 offset:

2026-07-29 20:10:44.000+00

避免依赖 session/local timezone。

target 类型选择

known pre-created boundary
  name

known destructive transaction
  xid + inclusive=false, after verifying identity

known physical log point
  lsn

only trusted event time
  time + explicit timezone/inclusive

need earliest consistent database
  immediate

need latest independently archived state
  end of archive, with RPO evidence

XID 会 wrap;日志中同一个数字必须与正确 cluster/epoch/时间关联。LSN 精确但 不直接表达业务。time 易懂但可能有 clock/commit ambiguity。

选择能到达 target 的 backup

候选 base 必须:

finish before or otherwise be valid for target
belong to same physical lineage
have all dependent backup objects
have continuous required WAL to target
have reachable timeline history
have usable encryption key
fit target runtime/version

对 time target,pgBackRest 可以选择一份结束时间早于 target 的可用 backup。 对 name/XID,工具不能总是自动知道 target 落在哪份历史中;pgBackRest User Guide 明确提示 name/XID target 无法自动选择 backup。正式 run 因此用 --set=<exact-label> 固定刚创建的 full。

“最新备份”可能太新

误删发生在 10:00:

backup A ends 09:00
backup B starts 11:00

选择 B 无法回到 10:00 之前,因为其文件状态已经包含误删;需要 A 加 WAL。 “latest”不是无条件正确。

backup set 与 recovery target 分离

--set
  chooses base/dependency set

--type / --target
  chooses where WAL replay stops

一个是起点,一个是终点。混淆会导致:

  • 起点晚于目标;
  • 自动选择错误;
  • 恢复到 WAL 尾部而不是事故前;
  • 以为 backup label 就是业务时刻。

恢复位置

优先顺序:

new host/failure domain
  best isolation

same host, new path/process/socket/port
  useful lab compromise with explicit exception

overwrite original data directory
  destructive replacement only, separate authority

日常验证和误删取数应选择新环境。覆盖原集群会:

  • 消灭当前证据;
  • 影响服务;
  • 把 target 错误变成二次事故;
  • 与 Patroni/DCS 冲突;
  • 让失败回退更难。

空间预算

至少:

[ Space \ge RestoredData +WALWorkingSet +Temp +Logs +SafetyMargin ]

若保留源数据副本,可能需要两倍以上。tablespace、sparse file、reflink、 object cache 与 filesystem reserved blocks 都要计入。

恢复前:

df -h
df -i
findmnt
lsblk

确认 path 真正位于预期设备,而不是 root filesystem 上一个空目录。

权限与隔离

恢复目录:

owner postgres
mode 0700
not symlink
new and empty
not /pg/data
not a Patroni member path

接入:

listen_addresses=''
private Unix socket mode 0700
custom restrictive pg_hba
distinct port
no service registration
no DNS/VIP/proxy route

数据副本本身可能含生产敏感数据;“测试恢复”不能降低访问控制。

archive push 隔离

PITR promote 会产生新 timeline。验证实例若继续向 source repository archive,可能污染共享历史。pgBackRest 官方指南建议,对会 promote 但不成为 新 primary 的 reporting/testing cluster 使用:

--archive-mode=off

正式实验既在 restore option 中设置,又查询有效:

SHOW archive_mode;  -- off

仅修改 archive_command='' 会让 WAL 积在本地,并不等同于清晰的验证实例 policy。

authority

恢复至少分三种授权:

read-only catalog/capture
  no database or repository mutation

isolated restore drill
  synthetic source writes + new backup/path/postmaster

production replacement/cutover
  stops/overwrites/reroutes real service

本章 formal 只有第二种沙箱授权。不能拿它的确认 token 去生产。

21.4.2 启动前核对时间线与 WAL 完整性

preflight 清单

postgres 启动前确认:

exact source/stanza/system lineage
exact backup label and type
backup status and dependency completeness
backup start/stop WAL
target kind/value/inclusive
target WAL covered by archive
timeline history available
restore_command configured
recovery.signal present
standby.signal absent unless desired
tablespace path and ownership
PostgreSQL major/binaries/extensions
recovery-critical max settings
network/HBA/archive isolation
log destination writable

把它写入 evidence,不要只在人的终端滚过。

repository catalog

pgBackRest:

sudo -iu postgres \
  pgbackrest --stanza=pg-test --repo=1 --output=json info

检查:

stanza status code
database/repository id
PostgreSQL version
backup label/type/error
start/stop timestamp
start/stop archive
logical/repository bytes
archive min/max by archive id
locks

不要把输出中的 repository secret 或 raw system identifier复制进公开 evidence。

WAL 文件名比较的限制

同一 archive ID、固定 segment size 和 timeline 语境中,固定长度 WAL filename 可帮助判断 target segment 是否不晚于 max。更完整的验证需要:

  • 确认中间没有 gap;
  • 确认 history file;
  • 让 restore path 实际逐段读取;
  • 对 object checksum;
  • 记录 archive ID 与 lineage。

只有 max >= target 不能证明中间全在。正式实验同时跑 pgbackrest check 和真实 restore;真实 replay 是最终 gap detector。

restore_command

恢复过程请求 WAL:

restore_command = 'pgbackrest ... archive-get %f "%p"'

返回合同与 archive push 类似:

0
  requested file delivered

nonzero
  file unavailable; PostgreSQL may try pg_wal/stream/next source as applicable

不要让 command 把其他 cluster 同名 WAL 放入 %p

signal file 与 generated settings

pgBackRest restore 会生成 postgresql.auto.conf recovery settings,并创建 recovery.signal。检查:

test -f "$PGDATA/recovery.signal"
test ! -f "$PGDATA/standby.signal"   # for this promoted PITR design
sed -n '1,200p' "$PGDATA/postgresql.auto.conf"

输出可能包含路径或凭据参数,证据应做 secret-safe 投影,而不是无脑上传。

同一 system identifier

物理 backup 和 source 应同一 lineage:

SELECT system_identifier
FROM pg_control_system();

比较关系:

source == restored

如果不同:

  • backup 来自另一 cluster;
  • stanza/path 混淆;
  • evidence 目标错误。

不要“修” system identifier 让它相等;停止并调查。

timeline 必须可达

记:

backup timeline = Tb
target timeline = Tt

要求存在从 (T_b) 到 (T_t) 的合法 history path。PITR promote 后的新 timeline (T_n) 应满足:

[ T_n > T_t ]

本章:

source/target timeline 7
restored promoted timeline 8

如果 promotion 后仍报告 7,要确认观察的是 current WAL timeline、checkpoint timeline 还是 recovery 尚未完成。

checkpoint timeline 会滞后

pg_control_checkpoint() 报最近 checkpoint。一个 replica 可能已在接收/重放 更新 timeline,但其 checkpoint control info 仍较旧。第 20 章已遇到这种 情况。

因此:

primary current WAL filename timeline
Patroni TL
replica receive/replay evidence
checkpoint timeline

要标注 observation semantics,不能强行要求所有数字在任意瞬间相等。

recovery-critical 参数

WAL 记录某些 source 参数。恢复实例不能把它们设得更低。重点:

max_connections
max_worker_processes
max_wal_senders
max_prepared_transactions
max_locks_per_transaction

正式脚本先查询 source,再在 isolated start 中显式携带。第一次开发启动因 max_connections=20 < 500 被 PostgreSQL 正确拒绝,未修改源集群。

这类失败的 SOP:

stop
read PostgreSQL FATAL/DETAIL/HINT
compare with captured source settings
correct isolated runtime
start as a new reviewed attempt
never weaken source or edit WAL/control data

extension 与 preload

物理 backup catalog 可能引用 extension。恢复环境若缺 .so

  • startup 可能因 shared_preload_libraries 失败;
  • 查询对象可能缺函数/type;
  • background worker 可能连接外部系统。

验证实例需要决定:

install exact extension packages
or explicitly disable reviewed preload components

本章 synthetic fixture 不依赖 preload,因此 isolated instance 设:

shared_preload_libraries = ''

这不能证明生产 extension 全部可用;属于本章小数据边界。

不让恢复实例自动加入平台

同一 cluster_name 不是 Patroni membership,但为避免混淆,本章使用:

cluster_name = 'pg36-ch21-restore'

同时:

no Patroni process
no DCS registration
no HAProxy service
no exporter discovery
no archive push

如果目标是正式替换 cluster,加入平台是另一个经过审查的阶段。

日志是证据

关注顺序:

starting backup recovery
restored history/WAL from archive
starting point-in-time recovery to ...
redo starts
consistent recovery state reached
ready to accept read-only connections
recovery stopping at target
selected new timeline
archive recovery complete
end-of-recovery checkpoint
ready to accept connections

错误:

requested WAL not found
recovery ended before configured target
invalid checkpoint record
insufficient parameter settings
could not load library
tablespace path failure
permission denied

不要只保存最后一行 database system is ready

两阶段 readiness

正式 runner:

  1. pg_ctl -w 等到第一条连接;
  2. 立即记录 pg_is_in_recovery()transaction_read_only
  3. 继续轮询到 recovery=false;
  4. 查询 effective isolation;
  5. 做 rollback-only write;
  6. 才声明 promotion complete。

观测:

first connection:
  in_recovery=true
  transaction_read_only=true

355.896 ms later:
  in_recovery=false
  transaction_read_only=false

这不是理论角落,而是本章真实运行结果。

21.4.3 数据库一致不等于业务数据正确

引擎一致性

PostgreSQL recovery complete 能说明:

  • WAL record 可重放到一致点;
  • control/catalog/transaction state 满足引擎;
  • 数据库可按当前模式接受连接;
  • promotion 时建立了新 timeline。

不能说明:

  • target 是事故前正确边界;
  • 所有正确交易都在;
  • 错误交易都不在;
  • 外部系统一致;
  • 应用 schema/code 匹配;
  • 秘密与权限适合交付;
  • 报表金额正确。

业务不变量

为每个服务维护可执行 invariant:

orders
  no orphan order lines
  order total = sum(lines)
  accepted payment token unique
  known checkpoint token present
  destructive release token absent

ledger
  debits = credits per journal
  immutable entries not missing
  sequence/event continuity

tenant SaaS
  no row crosses tenant boundary
  tenant counts match reference
  row-level security policies installed

SQL 示例:

SELECT order_id
FROM order_line l
LEFT JOIN orders o USING (order_id)
WHERE o.order_id IS NULL
LIMIT 1;

SELECT journal_id
FROM ledger_entry
GROUP BY journal_id
HAVING sum(debit) <> sum(credit)
LIMIT 1;

零行是某项证据,不是万能健康。

正向与反向标记

一个强 boundary test 同时要求:

before target marker present
at/allowed target marker present
after target marker absent

本章:

base     present
keep     present
discard  absent

只验证 keep 存在,可能实际恢复到了 WAL 尾部,discard 也在;反向断言能 发现 overshoot。

token 要不可混淆

正式 marker:

run_id + stage
unique token
database commit timestamp
primary key(run_id, stage)
unique(token)

恢复后按 exact run ID 查询,避免把旧演练行当成本轮成功。

验证 schema 与语义

除了行:

  • schema version;
  • extension version;
  • constraint/index validity;
  • owner/privilege;
  • RLS policy;
  • sequence;
  • collation;
  • function/trigger;
  • materialized view freshness;
  • partition attachment;
  • large object。

有些对象在物理 backup 中存在,但应用新版本可能期待更晚 schema。历史 database 与当前 application code 不能直接组合。

外部系统

数据库可能回到 10:00,消息队列、对象存储、支付平台仍在 10:30:

database says payment pending
payment provider says captured

database row absent
object file already created

database event offset rewound
consumer has processed later events

恢复计划必须决定:

  • 外部系统也回退?
  • 数据库追赶?
  • 做补偿/对账?
  • 暂停哪些写入?
  • replay event 是否幂等?

PITR 不能跨系统自动保持分布式一致。

误删取数的安全合并

推荐路径:

isolated historical restore
  -> validate historical target
      -> export only affected data
          -> normalize identifiers/format
              -> compare against current
                  -> reviewed repair transaction
                      -> audit and reconcile

不要把历史 cluster 暴露给普通应用写入;它的 sequence、outbox 与定时任务 可能重新发出旧动作。

可在隔离实例中:

disable external network
disable schedulers/background workers
use read-only role for analysts
export with COPY/pg_dump
record checksum/count

本章为了验证 promotion 做一次 temporary table 回滚写,不产生持久业务写。

service cutover 是另一个 gate

在真正 replacement 场景:

freeze or fence old writer
validate candidate
configure identity/secrets
register monitoring
update routing
drain/reconnect clients
run smoke/invariants
reconcile unknown outcomes
monitor backlog
retain old evidence

本章不执行 routing change,所以:

database/data proof = accepted with exceptions
service cutover = not run

第 22 章继续连接与服务合同。

验证矩阵

维度检查失败动作
lineagesame system ID relationstop
timelinenew child after promotestop
targetexpected present/absentstop/reselect
enginerecovery=false, writableinspect logs
isolationno TCP, archive offstop immediately
schemaversion/extensions/constraintsrepair environment
businessdomain invariantsreject candidate
externalreconciliationkeep isolated
sourceoriginal cluster unchangedincident
shutdownprivate postmaster stoppedforce safe stop

证据包

至少包含 secret-safe:

authority and exact target
source preflight
repository/catalog projection
backup label and WAL range
target decision
restore options and path
recovery phase timestamps
effective settings
lineage relation and timeline
business queries/results
source postflight
shutdown state
exceptions
counterexamples
review hashes

不要包含:

repository key
cipher pass
database password
raw secret inventory
unnecessary raw system identifier

失败也是证据

本章有两次开发/正式前失败:

development:
  max_connections lower than source
  PostgreSQL rejects recovery start

first formal attempt:
  pgBackRest check given unsupported --repo=1
  pgBackRest exits 31 before restore

两次都没有覆盖 source,也没有留下运行中的 isolated postmaster。我们修复 runbook,而不是删除失败痕迹后假装一次成功。

成熟流程会统计:

failed drills
failure phase
time to diagnose
unsafe side effects
runbook correction
repeat proof

完成定义

restore command exit 0                      insufficient
PostgreSQL accepts read-only                insufficient
PostgreSQL promoted                         engine milestone
business boundary passes                    data milestone
source remains healthy                      safety milestone
isolated instance stopped                   containment milestone
service controlled and reconciled           service milestone

本章正式到 containment milestone,服务 cutover 保持未运行。

小结

恢复的核心动作不是“启动一个旧数据库”,而是连续回答:

which history?
which target?
which evidence?
which isolation?
which completion state?
which business truth?
which authority to expose it?

下一节把流程映射到 Pigsty 和 pgBackRest:仓库、调度、凭据、观察入口以及 为什么平台自动化应承载合同,而不是掩盖 PostgreSQL 原生证据。


上一节:备份仓库与保留策略 · 返回本章目录 · 下一节:用 pgBackRest 交付备份策略 · 查看全书目录 · 查看索引中心

最后更新于