跳转到主要内容

21 未雨绸缪:备份体系与恢复演练

pgbackrest info 显示 status: ok,不等于数据可恢复;每天都生成备份, 不等于误删后能回到正确时刻;有三台流复制副本,更不等于有一份独立备份。

备份体系真正要交付的是一个可证伪的命题:

当某个已声明的损失场景发生时,团队能否找到一条完整、可信、权限可用的 恢复链,在隔离环境中把 PostgreSQL 带到预定边界,验证数据库与业务不变量, 再以受控方式交付服务?

这句话里没有“备份成功率”这个单一答案。它至少包含:

scenario
  -> recovery point objective
      -> base backup lineage
          -> continuous WAL
              -> repository and key availability
                  -> target selection
                      -> isolated restore
                          -> PostgreSQL consistency
                              -> business validation
                                  -> controlled cutover

本章先从误删、介质丢失、区域故障和合规留存反推 RPO/RTO;再解释物理 基础备份、WAL、timeline 与归档链;随后把 full/diff/incr、过期、加密、 不可变和异地副本放进同一份仓库设计;最后用 Pigsty 与 pgBackRest 完成一次真实的命名恢复点演练。

本章不把成功说大:

fresh full backup                 通过
named-point physical PITR         通过
base + keep / no discard          通过
isolated Unix-socket postmaster   通过
new timeline and same lineage     通过
source cluster remains healthy    通过
arbitrary-time recovery           未测试
missing WAL / lost key            未注入
immutable off-site repository     未证明
production-sized RTO              未证明
regional disaster recovery        未测试
production approval               pending

本章目标

读完并完成实验后,你应当能够:

  1. 从损失场景与业务真相出发,而不是从“每天全备”出发设计恢复;
  2. 区分 RPO、RTO、恢复粒度、保留周期、历史版本数和法律留存;
  3. 说明逻辑备份、物理备份、存储快照、流复制与 CDC 各自能恢复什么;
  4. 解释基础备份为何必须配合一条连续 WAL 链;
  5. backup_label、LSN、WAL 文件名与 timeline history 判断物理血缘;
  6. 设计幂等且不会覆盖不同内容的归档路径,并理解归档积压为何会填满 pg_wal
  7. 正确比较全量、差异、增量的依赖、恢复复杂度与过期语义;
  8. 把加密密钥、不可变、独立凭据、异地副本与恢复权限纳入仓库合同;
  9. 区分“仓库可读”“文件恢复完成”“只读可用”“提升完成”“业务可用”;
  10. 正确选择 time、name、XID、LSN、inclusive/exclusive 与 timeline;
  11. 在 Pigsty 中声明、观察和操作 pgBackRest,而不把平台包装当成原理;
  12. 在不覆盖原集群的前提下完成一次可重放、可审计的隔离恢复;
  13. 输出测量口径、证据、反例、例外与生产准入差距;
  14. 知道成功恢复一次之后,下一次应该故意测试哪些失败路径。

前置与后续

前置:

后续:

  • 第 22 章 服务接入、连接池与路由 处理恢复后如何 把客户端安全带到正确角色;
  • 第 23 章深入身份、传输、凭据与密钥;
  • 后续容量、监控、变更与事故章节会把恢复证据纳入生产治理;
  • 区域级 DR 和真正的 destructive replacement 必须在独立授权的演练中 完成,不由本章沙箱命令暗中代替。

学习路径

business loss scenario
  -> authoritative truth + tolerated loss
      -> RPO / RTO / granularity / retention
          -> logical vs physical vs snapshot vs replica
              -> base backup + WAL continuity + timeline
                  -> repository dependency graph
                      -> select backup and target
                          -> restore into isolation
                              -> wait through promotion
                                  -> database + business proof
                                      -> gaps and next drill

这条路径故意不从复制 pgbackrest restore 命令开始。恢复命令是一个高风险 状态迁移;没有目标语义、血缘、WAL、隔离和验收标准时,命令执行得越顺利, 越可能迅速得到一个“能启动但不该交付”的数据库。

四层恢复证明

把“可恢复”拆成四层,能避免指标替代:

层次 最低问题 常见证据 尚不能推出
仓库层 备份与 WAL 对象能否读取 catalog、checksum、archive range PostgreSQL 能启动
引擎层 能否恢复到一致状态 recovery log、timeline、pg_is_in_recovery() 目标数据正确
数据层 预期事实是否存在/不存在 token、行数、约束、聚合、对账 应用依赖可用
服务层 应用能否安全接入 routing、权限、smoke、backlog 长期 SLO 已满足

本章正式实验走到数据层,并用 rollback-only write probe 证明提升后可写; 它不切换生产路由,因此没有宣称服务层 cutover 通过。

正式实验拓扑

target           pg36-l2-vagrant/pg-test
Pigsty           v4.5.0
PostgreSQL       18.6
pgBackRest       2.59.0
source           pg-test-1, live primary, timeline 7
repository       S3-compatible MinIO, AES-256-CBC, one sandbox target
restore host     pg-test-3
live member      Patroni replica on 5432, unchanged
isolated copy    fresh path + private Unix socket + port 55432
archive push     off
recovery target  named point
target action    promote
target timeline  latest

实验业务边界:

base      committed before fresh full backup       must exist
keep      committed after backup, before target    must exist
target    named restore point
discard   committed after target                    must not exist

正式观测:

backup label                        20260729-201041F
backup command                         2.086 s
pgBackRest check                       0.598 s
logical backup bytes                   36,121,841
repository delta bytes                  4,539,288
restore copy                            2.758 s
start -> first connection               0.963 s
first connection state                  recovery=true, read_only=true
start -> promoted and writable          1.319 s
read-only -> promoted                   0.356 s
source timeline -> restored timeline    7 -> 8
system identifier relation              matches source
source replica lag after drill          0 bytes
counterexamples rejected                14

这些时间是 36 MB 级合成沙箱的一次观测,不是生产 RTO。它们最有价值的 发现反而是:

pg_ctl -w start 返回时,实例可能刚进入 hot standby 的只读可用阶段, recovery_target_action=promote 尚未完成。

所以正式脚本没有把“第一条 SELECT 成功”当成恢复完成,而是继续等待 pg_is_in_recovery() = false,再做一次回滚写入。

十项例外

沿用第 19 章六项:

EX19-SHARED-HYPERVISOR
EX19-SINGLE-ETCD
EX19-SINGLE-BACKUP-TARGET
EX19-VIRTUAL-STORAGE
EX19-INVENTORY-SECRETS
EX19-LAB-RESOURCE-FLOOR

本章新增四项:

EX21-SHARED-RESTORE-HOST
  恢复进程、目录与网络隔离,但与 live replica 共用 guest/hypervisor;
  不能声称主机、内核、设备和故障域隔离。

EX21-REPOSITORY-NOT-IMMUTABLE
  只有本地单 MinIO;未证明 object lock、独立凭据或跨区域副本。

EX21-SMALL-SYNTHETIC-DATA
  数据量很小;不能拿时间结果做生产容量规划。

EX21-NAMED-POINT-ONLY
  只验证一个命名点,并主动切 WAL 后检查;不能代表任意时间点或最坏
  归档间隙 RPO。

例外不是装饰性免责声明。每项都对应一个被禁止的推论,机器验收要求它们 完整保留。

本章目录

21.1 从恢复场景设计备份

21.2 物理备份与 WAL 连续性

21.3 备份仓库与保留策略

21.4 恢复流程与验证

21.5 用 pgBackRest 交付备份策略

21.6 实战:完成一次隔离恢复演练

实验入口

动作语义:

capture / verify / review / all
  L0 read-only

drill:pitr
  guarded local sandbox mutation
  insert markers + full backup + fresh isolated restore + stopped retention

reset:fixture
  destructive and separate
  delete exactly one reviewed run only

all 只重验已有证据,不会为了演示方便再做一份备份或再启动一次恢复。

权威资料

原理优先以当前 PostgreSQL 18 文档为准:

实现与平台入口:

版本相关命令在使用前应回到对应版本文档核对。本章 formal evidence 固定 在 Pigsty v4.5.0、PostgreSQL 18.6 与 pgBackRest 2.59.0;“当前文档入口” 不是“历史版本命令完全相同”的承诺。

本章最重要的判断

backup command succeeded        != recoverable
catalog status ok               != WAL chain complete for every target
replica healthy                 != independent backup
physical restore started        != recovery complete
read-only query succeeded       != promotion complete
PostgreSQL consistent           != business truth correct
one fast sandbox restore        != production RTO
encrypted repository            != ransomware resistance
retained for 14 days            != 14 days of arbitrary PITR
restore directory retained      != service approved

真正的完成条件是:

declared scenario
+ selected truth boundary
+ complete lineage and WAL
+ isolated executable restore
+ engine and business proof
+ controlled service decision
+ explicit residual risk

下一节从第一项开始:先定义究竟要从什么损失中恢复。


上一章:狡兔三窟:高可用拓扑与容灾目标 · 返回下卷导读 · 下一章:四通八达:服务接入、连接池与路由 · 查看全书目录 · 查看索引中心

21.1 从恢复场景设计备份

备份设计最容易犯的错误,是从一个漂亮的日历开始:

周日全备
周一到周六增量
保留 14 天

它回答了“工具何时运行”,却没有回答:

什么会丢?
丢了什么算业务损失?
要回到哪个边界?
由谁判断边界正确?
依赖也丢失时怎么办?
多快恢复到什么能力?

正确顺序是从场景与真相出发,再选择恢复机制、保留和调度。

21.1.1 误删、介质故障、区域故障与合规留存

先定义“损失”

数据库恢复不是把字节放回磁盘,而是重新建立被业务接受的事实。至少区分:

logical loss
  数据库仍运行,但某些正确事实被删除、覆盖或错误变更

physical loss
  数据文件、WAL、设备或整个集群不可用/不可信

site loss
  数据库、控制面、接入层和本地备份一起不可用

historical obligation
  必须证明某个历史版本可恢复、可查询或已按政策删除

不同损失的 truth source 不同。误删一行时,最可信的边界可能来自审计事件; 存储毁坏时,最可信的边界是仓库中最后完整基础备份与连续 WAL;区域故障时, 本地仓库本身不再是可用前提。

场景一:误删与逻辑破坏

典型事件:

DELETE FROM orders;
UPDATE account SET balance = 0;
DROP TABLE customer;
deploy buggy migration;
application writes wrong currency or tenant id;

主库、流复制副本和同步副本都会忠实重放这些操作。HA 可能保持服务在线, 却更快地把错误复制到每份在线副本。

误删恢复要先问:

  1. 错误事务的开始、提交和影响范围是什么?
  2. 目标是整个集群回退,还是只取回一张表/一批行?
  3. 目标时刻之后有哪些正确交易不能丢?
  4. 如何把旧事实与当前事实合并?
  5. 序列、外键、触发器、审计与外部系统如何一致?

常见安全路径不是“原地 PITR 回退生产”,而是:

restore historical cluster in isolation
  -> validate target boundary
      -> export affected logical set
          -> compare with current production
              -> reviewed merge or repair

原地回退会同时删除误操作之后的所有正确提交,通常扩大损失。

场景二:介质、文件系统或集群丢失

典型事件:

data volume lost
filesystem corruption
all HA members share failed storage
operator removes whole cluster
encryption layer/key unavailable
malware changes data and online copies

这时要恢复的是整个物理集群。所需链条通常为:

compatible PostgreSQL runtime
+ one usable base backup
+ every required WAL segment
+ timeline history
+ tablespace/path mapping
+ repository credential and encryption key
+ configuration and identity material

注意最后两项不一定在 PostgreSQL 物理备份内。官方连续归档文档明确提醒, postgresql.confpg_hba.confpg_ident.conf 的手工修改不由 WAL 恢复。平台声明、证书、服务发现、KMS/IAM 与 DNS 也需要独立保存。

场景三:区域或控制域故障

区域故障不是“把同一恢复命令放到另一台机器”:

source region unavailable
local object store unavailable
local DNS/control plane unavailable
operator identity federation degraded
secrets/KMS endpoint unavailable
network dependency changed
capacity not pre-provisioned

如果数据库与备份仓库在同一机房、云账户、管理员凭据或密钥域,所谓 “远程对象存储”仍可能是共同故障。

区域场景的前提矩阵至少包括:

依赖 源域丢失时是否仍可用 证据
基础备份 是/否 异地对象清单、定期读取
WAL 是/否 最大已复制 WAL、延迟
解密密钥 是/否 独立保管与 break-glass 测试
PostgreSQL 包/镜像 是/否 固定版本仓库
Pigsty 声明 是/否 版本化、离站
DNS/证书 是/否 独立控制面
应用依赖 是/否 DR dependency map
人员权限 是/否 异地登录演练

只有“对象在另一个 bucket”远远不够。

场景四:合规留存与法律冻结

合规问题不等于“把备份留久一点”。需要先区分:

operational recovery retention
  为误删、故障与日常恢复保留

historical record retention
  为审计、诉讼、监管或业务档案保留

legal hold
  正常过期规则暂时不能删除特定材料

right-to-delete / minimization
  到期后必须删除或不可再识别

物理备份以整个 cluster 为粒度,单个数据主体的删除很难立刻传播到所有历史 备份。合规方案可能需要:

  • 受控的短期物理恢复窗口;
  • 更长期、字段经过选择与脱敏的逻辑档案;
  • 按租户/数据域分离;
  • 密钥销毁策略;
  • legal hold 与正常过期的冲突处理;
  • 恢复访问的审批、审计和二次删除。

不要让 DBA 独自解释法律语义。数据 owner、安全、法务与平台团队要共同签署 retention class。

场景卡,而不是一句“灾备”

为每个场景写同一张卡:

id: accidental-delete-orders
trigger: confirmed destructive transaction against orders
authoritative_boundary:
  source: audit event + restore point
  timezone: UTC
scope: selected tenant and time range
maximum_tolerated_loss:
  committed_correct_orders: zero
service_target:
  historical cluster queryable: 60 minutes
  repaired rows in production: 4 hours
dependencies:
  - base backup and continuous WAL
  - audit event identity
  - isolated restore capacity
  - reviewed logical merge procedure
stop_conditions:
  - target transaction ambiguous
  - WAL gap
  - post-target correct orders cannot be reconstructed
proof:
  - restored marker boundary
  - row and amount reconciliation
  - approved repair change
owner: data-platform + orders-domain

recovery-scenarios.json 提供本章四类机器可读样例。没有 stop_conditions 的 runbook 很容易在压力下把“不知道”伪装成“应该没问题”。

同一个事故可能需要两条恢复路径

例如误删订单:

path A: service continuity
  current production remains online

path B: historical reconstruction
  isolated PITR -> export -> compare -> repair

例如区域故障:

path A: promote designed DR replica
  lower RTO, possibly different RPO/consistency contract

path B: restore off-site backup
  slower, but independent historical recovery

HA、DR 与 backup 可以组合,但不能互相替名。

21.1.2 RPO、RTO、恢复粒度与保留周期

RPO:允许回退多远

令:

t_loss     事故发生或最后可信事实时间
t_recover  实际可恢复边界

时间口径可写为:

RPOactual=tlosstrecover RPO_{actual}=t_{loss}-t_{recover}

但它不是简单的 backup interval:

daily base backup + continuous WAL
  RPO 主要由 WAL 最后耐久位置决定,不是 24 小时

daily logical dump only
  RPO 可能接近 24 小时

streaming replica
  对主机故障可能接近复制延迟
  对误删可能是 0 秒地复制了错误,无法提供历史点

生产 RPO 必须带场景:

primary process loss RPO
single-AZ loss RPO
repository outage RPO
operator logical error RPO
region loss RPO

“RPO = 5 分钟”却不说明事故类型,是不完整的合同。

WAL archive 的 RPO

连续归档场景中,粗略地:

RPOarchivetfailuretlast durable independent WAL RPO_{archive} \approx t_{failure}-t_{last\ durable\ independent\ WAL}

需要强调 durableindependent

  • WAL 在主库 pg_wal 中,不代表主机丢失后仍可取;
  • archive command 返回成功,不代表对象存储副本已跨域;
  • 对象写入完成,不代表 KMS/credential 在事故中可用;
  • pg_stat_archiver 的累计失败数不等于“当前失败”,要看最近成功/失败时序 和 backlog。

低流量系统还会遇到 segment 未填满。archive_timeout 或显式 pg_switch_wal() 可以缩短时间窗口,但过短会制造大量完整长度的归档对象。

RTO:恢复到哪种能力

完整恢复时间不是 restore 命令运行时间:

RTObusinessTdetect+Tdecide+Tprovision+Tretrieve+Treplay+Tvalidate+Tcutover+Tdependency RTO_{\text{business}} \ge T_{\text{detect}} +T_{\text{decide}} +T_{\text{provision}} +T_{\text{retrieve}} +T_{\text{replay}} +T_{\text{validate}} +T_{\text{cutover}} +T_{\text{dependency}}

可以同时定义多个里程碑:

里程碑 完成条件
repository selected 备份、WAL、key 和 target 已审核
bytes restored pgBackRest 文件阶段完成
consistent PostgreSQL 到达一致恢复点
read-only 可以接受只读查询
promoted pg_is_in_recovery() = false
data validated 引擎与业务不变量通过
service cut over 受控端点连接到正确角色
backlog cleared 积压与补偿完成

本章正式实验测到:

restore copy                  2.758 s
start -> first read-only      0.963 s
start -> promoted             1.319 s

若只记录前两个数字,会漏掉验证、路由、应用依赖和积压,也会把只读窗口误报为 “恢复完成”。

恢复粒度

至少有四种粒度:

cluster
  物理备份/PITR 常见粒度;包含 cluster 内所有数据库

database/schema/table
  逻辑导出或从隔离物理恢复中再导出

row/business object
  比较、审计、补偿与应用语义

service capability
  read-only、read-write、reporting、limited tenant 等

PostgreSQL 物理 WAL 是 cluster 级历史。不能告诉恢复引擎“只重放某张表的 正确事务”。pgBackRest 的 database include/排除能力也不是任意表级 PITR; 选择性恢复的语义必须认真核对限制。

target 精度不等于 truth 精度

PostgreSQL 可按:

time
transaction ID
LSN
named restore point
immediate consistency
end of available WAL

停止恢复。但技术 target 再精确,如果业务事件不明确,也不能证明正确。

例如:

10:00:00 application request sent
10:00:01 database transaction committed
10:00:02 external payment confirmed
10:00:03 audit pipeline wrote event

“恢复到 10:00:02”到底包含什么,取决于时区、提交时间、inclusive 语义与 外部系统。命名点可以降低实验歧义,但生产误删通常只能事后从日志、XID、 LSN 或审计重建边界。

保留周期不是 PITR 窗口的同义词

假设“保留 14 天全备”,仍不能自动推出:

过去 14 天每一秒都可恢复

还需要:

  • 至少一份在目标之前结束的可用基础备份;
  • 从该备份起到目标的连续 WAL;
  • timeline history;
  • 未丢失的依赖备份;
  • 可用的解密密钥和仓库权限;
  • 与目标 PostgreSQL 版本兼容的恢复环境。

真实 PITR window 是这些集合的交集。

恢复点频度、版本数与日历跨度

三个常被混淆的量:

capture cadence
  多久产生一个新逻辑/物理基线

version count
  保留多少组 backup

calendar coverage
  最旧可恢复事实距现在多远

retention_full=2 按 count 解释,与按 time 解释完全不同;差异/增量依赖还会 影响一组备份何时能安全过期。应把政策写成可测试的例子:

at 2026-08-01 12:00 UTC
  must restore any named daily checkpoint since 2026-07-18
  must restore any WAL time since 2026-07-25
  must retain month-end logical archive for 7 years

然后定期真的选最旧目标恢复。

分级服务目标

不是所有数据库都应买同一种恢复成本:

tier 场景示例 RPO RTO 机制
critical 支付主账 秒/分钟级 分钟级 HA + 连续 WAL + 异地仓库 + 高频演练
important 订单业务 分钟级 小时级 连续 WAL + 物理备份 + 选择性恢复
standard 内部应用 小时级 当日 日备 + 合理 WAL/逻辑导出
reconstructable 派生分析 可重算 天级 上游真相 + schema/code + optional backup

分级的结果不是降低严谨性,而是让成本与真实损失匹配。

目标反推设计

对每个 tier 反推:

RPO
  -> WAL archive latency / dump cadence / replication mode

RTO
  -> restore bandwidth / provisioned capacity / runbook automation

granularity
  -> physical, logical, audit or application repair path

retention
  -> backup dependency graph + WAL policy + legal hold

failure domain
  -> repository placement + credential + key independence

proof
  -> drill frequency + oldest target + business invariant

这比先决定“全备还是增量”更接近工程问题。

21.1.3 逻辑、物理、快照和副本的职责边界

没有一种副本解决全部恢复问题

机制 主要内容 典型粒度 优势 主要盲区
pg_dump SQL/归档格式逻辑对象 database/object 可选择、可迁移、可检查 慢;非 cluster 物理 PITR
pg_dumpall 全局对象与多个库的 SQL cluster logical roles/tablespaces 辅助 大规模恢复慢;无 WAL
physical base backup 数据文件物理状态 cluster 快、完整、可配 WAL 版本/平台耦合;粗粒度
WAL archive 变更历史 cluster PITR、连续恢复 必须连续;依赖 base
storage snapshot block/volume volume 快速 capture/clone 一致性、跨卷、WAL 与快照语义
streaming replica 在线物理历史尾部 cluster 低 RTO、读扩展 复制误删;非独立历史
logical replication/CDC 选择性变更流 table/event 异构、下游重建 DDL/序列/删除/slot 等边界

设计的关键是组合,而不是选一个“最好”的工具。

逻辑备份适合什么

pg_dump 在一个一致性快照中读取数据库,可以:

  • 选择 schema/table;
  • 以 custom/directory 格式并行恢复;
  • 跨部分 PostgreSQL 版本迁移;
  • 检视 DDL 与数据;
  • 从隔离 PITR 中导出被误删的对象。

但逻辑备份:

  • 不是数据目录文件副本;
  • 不含用于物理重放的控制信息;
  • 不能接到 WAL archive 上继续重放;
  • 通常不覆盖所有 cluster-wide 配置与运行状态;
  • 大库导出/导入和索引重建可能远慢于物理恢复;
  • 恢复顺序、owner、extension、privilege 与 external dependency 仍要处理。

PostgreSQL 官方文档明确说明 pg_dump/pg_dumpall 不能作为连续归档物理 恢复链的一部分。

物理备份适合什么

物理备份复制 cluster 数据文件,并依靠 WAL 把不一致的文件时间切片恢复到 一致点。它保留:

all databases in cluster
catalogs and physical relation state
transaction status
extension physical objects inside cluster
system identifier lineage

它适合:

  • 大型数据库快速整库恢复;
  • PITR;
  • 创建 standby/clone;
  • 保留完整引擎语义。

但通常要求同一大版本与兼容架构/页格式,并以 cluster 为恢复单位。物理 恢复后再做逻辑选择,是误删恢复常见组合。

PostgreSQL 18 原生增量不要和工具增量混为一谈

PostgreSQL 18 的 pg_basebackup --incremental 依赖:

earlier backup manifest
WAL summaries covering the required LSN interval
pg_combinebackup at restore preparation
all earlier dependent backups
normal WAL recovery requirements

这是一套 PostgreSQL 原生机制。pgBackRest 的 full/diff/incr、block incremental、bundle 与 repository metadata 是另一套实现和依赖图。

讨论“我们做增量备份”时,必须说清:

which tool
which version
dependency chain
restore assembly step
expiration owner

不能因为术语相同就互换 runbook。

快照要回答一致性

单卷快照可能在 block 层原子,但数据库语义还要问:

  • PostgreSQL 是否运行中?
  • 数据、WAL、tablespace 是否跨多个卷?
  • 各卷快照是否同一 consistency group?
  • 是否调用 pg_backup_start/stop 或工具协议?
  • 快照克隆后如何恢复与识别 timeline?
  • 快照控制面与主存储是否共同故障?
  • 是否定期挂载并启动验证?

“云盘快照成功”不是数据库一致性的证据。一个 crash-consistent snapshot 也许能通过 crash recovery,但不能自动提供任意 PITR 或跨卷一致性。

Replica 为什么不是 backup

流复制的目标是把当前历史低延迟复制到其他实例:

primary DELETE wrong rows
  -> WAL
      -> replica replays same DELETE

它可以:

  • 降低实例/主机故障的 RTO;
  • 在同步策略下改善某些故障的 RPO;
  • 分担读;
  • 作为备份来源,降低主库读取压力。

它不能单独:

  • 保留误操作之前的历史点;
  • 对抗影响全部成员的错误配置;
  • 对抗同一账户/区域/恶意管理员;
  • 证明长期 retention;
  • 替代隔离恢复。

延迟副本可增加逻辑错误反应窗口,但依然需要:

delay guarantee
pause/stop authority
monitoring that does not auto-heal away the delay
protection from direct reads/writes
independent backup for other failures

CDC 不是免费备份

事件流或 logical replication 能帮助重建某些数据,但要验证:

  • 初始快照从哪里来;
  • DDL 是否同步;
  • TRUNCATE/DELETE 是否保留足够语义;
  • sequence、large object 与 extension state;
  • slot 丢失或 lag;
  • exactly-once 是否只是消费端幂等;
  • 下游是否共享同一个逻辑错误。

它更适合作为另一条可重建历史,而不是未经证明的整库恢复替代品。

一份常见的组合

HA replicas
  handle selected availability failures

physical full/diff/incr + WAL
  handle cluster restore and PITR

logical export
  handle object-level portability and long-term selected records

audit/event history
  identify business boundary and support repair

off-site immutable copy
  handle control-domain and destructive repository loss

restore drills
  prove that all of the above compose

“多种机制”不等于重复浪费;前提是每种都有明确场景和 owner。

选择问题

面对一个需求,按顺序问:

1. loss is logical, physical, site-wide, or compliance?
2. recover whole cluster or selected facts?
3. need historical point or latest state?
4. how independent must the copy be?
5. what PostgreSQL/version compatibility exists?
6. what target precision can be observed?
7. how will business correctness be proven?
8. what is the controlled return-to-service path?

可能的答案:

selected rows from yesterday
  isolated physical PITR -> logical export -> reviewed merge

whole cluster after storage loss
  physical backup + WAL -> replacement infrastructure

cross-major migration
  logical dump/restore or logical replication, not physical replay

low-RTO host failure
  HA promotion, while independent backup remains separate

seven-year selected archive
  policy-specific logical record, not indefinite operational WAL by default

本节验收问题

在进入物理原理前,应能回答:

  1. 你的前三个恢复场景是什么?
  2. 每个场景的 authoritative boundary 来自哪里?
  3. RPO/RTO 的起止点和完成能力是什么?
  4. 哪些损失会被 replica 同步复制?
  5. 当前 retention 是否真的形成连续 PITR window?
  6. 配置、密钥、声明和应用依赖存放在哪里?
  7. 哪些结论只有实际 restore 才能证明?

如果答案仍是“每天有备份”,还没有完成设计。

小结

恢复体系从业务损失开始:

scenario defines truth
truth defines target
target defines mechanism
mechanism defines dependencies
dependencies define retention and placement
proof defines the drill

下一节进入物理链条:一份在线基础备份为什么能够是不一致的文件切片, WAL 又如何把它变成一个可选择时间点的 PostgreSQL 历史。


返回本章目录 · 下一节:物理备份与 WAL 连续性 · 查看全书目录 · 查看索引中心

21.2 物理备份与 WAL 连续性

物理 PITR 可以写成一条依赖式:

Recoverable(t)=BaseBackup(b)ContinuousWAL(bstart,t)ReachableTimeline(t)RuntimeCompatibility Recoverable(t)= BaseBackup(b) \land ContinuousWAL(b_{start},t) \land ReachableTimeline(t) \land RuntimeCompatibility

其中任何一项缺失,都不是“少恢复一点”,而可能是整条恢复链不可执行。

21.2.1 基础备份、检查点与一致性起点

为什么运行中的文件可以备份

在线复制数据目录时,文件并不处在同一个瞬间:

relation A copied at 10:00:01
relation B page modified at 10:00:02
relation B copied at 10:00:03
catalog copied at 10:00:04

单看文件副本,它可能不一致。PostgreSQL 的保证来自:

  1. 备份在一个已知 checkpoint 边界开始;
  2. 保存恢复所需的 backup metadata;
  3. 保存备份期间及之后所需的 WAL;
  4. 恢复时从 redo 起点重放到一致状态。

所以物理备份不是“恰好一致的一篮子文件”,而是:

recoverable file image + exact WAL obligations

checkpoint 在做什么

checkpoint 把一个恢复起点写入控制信息,并推动 dirty buffer 写盘。它不是 “把所有事务历史压成备份”,也不是备份完成点。

在线备份通常在 checkpoint 起点建立 redo requirement:

checkpoint redo LSN
  <= backup start LSN
      <= copied data interval
          <= backup stop LSN
              <= selected recovery target

恢复必须拥有从要求的 redo/WAL 起点到目标的连续记录。

pg_backup_start / pg_backup_stop

低层协议的核心顺序:

SELECT pg_backup_start(label => 'reviewed-label', fast => false);
-- keep this session alive while the file copy runs
SELECT * FROM pg_backup_stop(wait_for_archive => true);

关键语义:

  • 调用 pg_backup_start 的连接必须保持;
  • fast=true 请求立即 checkpoint,可能增加 I/O;
  • 文件复制期间数据库可以继续写;
  • pg_backup_stop 产生重要的 backup_label / tablespace_map 内容;
  • primary 上默认等待所需 WAL 归档;
  • 自制工具必须逐步验证,不能只在最后看 exit code。

正式系统通常使用 pg_basebackup 或 pgBackRest,而不是重新实现低层协议。 理解协议是为了判断工具证据。

backup_label 不是备注

backup_label 告诉恢复过程:

which backup session
start WAL file
start LSN
checkpoint location
start time

它和 tablespace mapping 是恢复输入,不是方便人看的注释。随意修改、漏拷或 把另一份备份的 label 混入,都会破坏血缘。

PostgreSQL 12 以后在线备份 label 通常作为 pg_backup_stop 输出交给备份工具, 而不是永久留在运行中 primary 的数据目录。不要照搬旧版“拷走 backup_label 文件”的手册。

pg_basebackup

pg_basebackup 通过复制协议取得运行中 cluster 的物理基础备份。常见能力:

plain or tar output
server/client compression
backup manifest
WAL streaming or fetch
rate limiting
tablespace mapping
standby signal/config generation
checkpoint mode
progress reporting

它需要具备 REPLICATION 权限或 superuser,并满足 pg_hba.confmax_wal_senders。它只备份整个 cluster,不能只备份一个 database。

示意:

pg_basebackup \
  --host=source \
  --pgdata=/safe/new/path \
  --format=plain \
  --wal-method=stream \
  --checkpoint=fast \
  --progress \
  --verbose

这不是本章 formal executor;正式实验使用 Pigsty 已交付的 pgBackRest。

--wal-method

概念上:

none
  backup output 不带 WAL;必须另有完整 archive

fetch
  在备份末尾从 source pg_wal 取所需 WAL;
  必须确保 WAL 未被回收

stream
  另开 replication connection 同步流式接收;
  需要额外 wal sender

即使 backup 包含让自身达到一致点的 WAL,若要恢复到更晚时间,仍需要后续 连续 archive。

backup manifest 与校验

manifest 记录文件、大小、checksum、WAL range 等,可用 pg_verifybackup 验证一份 pg_basebackup

pg_verifybackup /path/to/basebackup

它能证明文件与 manifest 一致、所需 WAL 结构满足其检查,却仍不能证明:

  • repository credential 在事故时可用;
  • PostgreSQL 能在目标环境启动;
  • target 之后/之前的业务边界正确;
  • extension/OS/配置依赖齐全;
  • 服务能够切换。

校验是 restore proof 的一层,不是替代。

data checksum 的边界

PostgreSQL data checksum 可以在读页时发现某些 page corruption。备份工具也 可对仓库对象做 checksum。两者都重要,但:

checksum matches
  means bytes match expected checksum

checksum matches
  does not mean business values are semantically correct

错误事务生成的页面 checksum 完全正确。

tablespace 与外部路径

物理备份必须覆盖 tablespace。恢复环境要检查:

  • pg_tblspc symlink;
  • target path 是否存在、为空、owner 正确;
  • 不同主机路径是否需要 remap;
  • mount 是否真的是预期设备;
  • tablespace 与数据目录是否落在同一快照 consistency group。

忽略 tablespace 常导致“主数据目录恢复成功,启动时才发现一半对象缺失”。

从 standby 备份

从 replica 取备份可降低 primary I/O,但带来不同限制:

  • 备份期间 standby 不能被 promote;
  • backup 与 primary timeline/WAL archive 仍要协调;
  • restartpoint 不等于 primary checkpoint;
  • 低活动时某些增量条件可能不成立;
  • replication lag 影响备份包含的历史;
  • 工具必须知道如何从正确节点取得并验证 WAL。

“从副本备份”是容量/可用性选择,不自动增加备份独立性。

PostgreSQL 18 原生增量

PostgreSQL 18 支持:

pg_basebackup --incremental=/path/to/prior/backup_manifest ...

服务端依据 pg_wal/summaries 中的 WAL summary 判断改变的 block。恢复前用 pg_combinebackup 把 full 与后续 incremental 合成为可启动的 synthetic full。

依赖链:

prior full
  -> prior/current manifests
      -> every required intermediate incremental
          -> WAL summaries at backup time
              -> pg_combinebackup
                  -> normal WAL recovery

PostgreSQL 不会替你管理哪些旧备份仍被新 incremental 依赖。过期策略删除 一个祖先,就可能让后代全部不可恢复。

版本兼容

物理备份通常用于同一 PostgreSQL 大版本。恢复环境还要匹配:

CPU architecture and page format expectations
PostgreSQL major
extension shared libraries
collation/locale providers
tablespace layout
configuration parameters needed during recovery

本章开发实验故意把 max_connections 从 source 的 500 降到 20, PostgreSQL 18 拒绝恢复:

recovery aborted because of insufficient parameter settings
max_connections = 20 is lower than on the primary, where it was 500

这不是性能调优问题,而是 recovery safety check。正式 runner 携带 source 的:

max_connections
max_worker_processes
max_wal_senders
max_prepared_transactions
max_locks_per_transaction

不能因为“验证实例很小”就任意降低 WAL 所要求的上限。

21.2.2 归档、timeline history 与恢复链

WAL 是有序的物理历史

WAL record 在数据页落盘前耐久化,支持 crash recovery、streaming replication 与 archive recovery。LSN 是逻辑日志位置,例如:

0/200002D0

通常每个 segment 16 MiB,但 segment size 可在 initdb 时选择。WAL 文件名 编码 timeline、log 与 segment,不应靠截字符串之外的自造规则做跨配置运算; 使用 PostgreSQL 函数:

SELECT pg_current_wal_lsn();
SELECT pg_walfile_name(pg_current_wal_lsn());
SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), '0/20000000');
SELECT pg_switch_wal();

连续性要求

一份 base backup 要恢复到目标 tt,必须有:

[LSNrequired start,LSNt] [LSN_{required\ start}, LSN_t]

上的每一段 WAL,以及中途 timeline 切换需要的 history。

缺一段不会得到“少几秒数据”;恢复通常停在 gap:

base -> WAL A -> WAL B -> [missing C] -> WAL D -> target

即使 D 在仓库里,也不能跨过 C。

archive_modearchive_commandarchive_library

归档要求:

wal_level >= replica
archive_mode = on
archive_command or archive_library configured

shell archive command 中:

%p  source path relative to data directory
%f  WAL filename

最重要的返回值合同:

exit 0
  PostgreSQL believes WAL is durably archived and may recycle local file

nonzero
  PostgreSQL retries

错误地返回 0 是数据丢失风险;持续返回非零则形成 backlog 与磁盘风险。

归档必须幂等,但不能覆盖不同内容

crash 后 PostgreSQL 可能再次提交同一个 WAL 文件。正确 archive sink 应:

target absent
  atomically write and durably persist

target exists with identical content
  return success

target exists with different content
  return failure and alert

绝不能无条件覆盖。两个不同 system identifier 的 cluster 若误用同一 archive namespace,可能产生相同 WAL 文件名却内容不同。

stanza、repository path、cluster identity 和权限隔离是防碰撞设计的一部分。

timeline 为什么存在

一次恢复或 promote 会从旧历史分叉:

timeline 7: A -> B -> C -> D
                         \
timeline 8:              C' -> E -> F

新 timeline 保留父 timeline 与分叉 LSN 的 history。这样旧历史不会被 “回到过去再向前写”覆盖。

需要区分:

system identifier
  initdb 生成的物理 cluster 血缘标识

timeline
  同一物理血缘中的历史分支

checkpoint timeline
  最近 checkpoint 记录的 timeline

current WAL timeline
  当前写入 WAL filename 所在 timeline

恢复出来的物理副本应与 source system identifier 相同;完成 promotion 后 应进入一个新 timeline。本章 evidence 只记录关系:

system identifier relation = matches source
timeline 7 -> 8

不公开原始 identifier。

recovery 输入

现代 PostgreSQL 通过 signal file 进入恢复:

recovery.signal
  archive recovery / PITR

standby.signal
  standby mode, can continue waiting for WAL/stream

常见设置:

restore_command = 'fetch %f into %p'
recovery_target_time = '...+00'
recovery_target_name = '...'
recovery_target_xid = '...'
recovery_target_lsn = '...'
recovery_target_inclusive = on|off
recovery_target_timeline = 'latest'
recovery_target_action = 'pause'|'promote'|'shutdown'

同一轮只能选择一种 target kind。没有 target 时通常重放到可用 WAL 尾部。

target form

target 优势 风险/前提
time 人和事故日志易理解 时区、提交时间、精度
XID 对目标事务明确 wraparound/识别来源;inclusive
LSN 物理边界精确 业务语义难读
name 预先设置,教学/发布边界清晰 事故后无法补建
immediate 最快到一致点 不是最新业务状态
end of WAL 尽量最新 可能包含逻辑错误

命名点:

SELECT pg_create_restore_point('before_risky_change');

只在 primary 上有意义,并写入 WAL。它不是 backup;没有 base 与 archive, 名字本身什么也恢复不了。

inclusive / exclusive

恢复到 time、XID、LSN 时,要明确目标 record/transaction 是否包含。误删场景 常想恢复到 destructive transaction 之前

target transaction known
  recovery_target_xid = destructive xid
  recovery_target_inclusive = false

但必须在实验中验证具体 target type 的语义。不要只凭自然语言“到某时刻”。

target timeline

恢复链可能包含多次 promote。latest 让恢复沿 archive 中可达的最新 timeline 继续;current/具体数字会限制选择。

危险例子:

choose an old backup
target time belongs to a child timeline
force target_timeline=current

结果可能根本到不了目标,或走错历史。选择前应画出 lineage:

backup timeline
  -> history file
      -> parent fork LSN
          -> target timeline

恢复到只读与 promotion

hot standby 到达 consistent state 后可先接受只读连接,然后才到 target 并 执行 action。本章实际日志顺序:

consistent recovery state reached
database system is ready to accept read-only connections
recovery stopping at restore point
selected new timeline ID: 8
archive recovery complete
end-of-recovery checkpoint
database system is ready to accept connections

因此完成条件应为:

SELECT pg_is_in_recovery();          -- must be false
SHOW transaction_read_only;          -- must be off
BEGIN;
CREATE TEMP TABLE write_probe(x int);
INSERT INTO write_probe VALUES (1);
ROLLBACK;

只读 SELECT 成功不是 promotion proof。

配置文件不在 WAL 历史里

恢复到过去不会自动恢复:

postgresql.conf external changes
pg_hba.conf
pg_ident.conf
Patroni YAML/DCS policy
TLS certificates
Pigsty inventory
DNS/proxy config
application secrets

这些必须由 versioned declaration、配置备份与平台自动化重建。把配置也塞进 data directory 不是充分答案,因为事故可能同时损坏或需要在新环境重写。

21.2.3 复制槽、归档失败与 WAL 保留者

谁让 pg_wal 不能回收

WAL retention 的常见“持有人”:

checkpoint/recovery requirement
archive not yet successful
physical replication slot
logical replication slot
wal_keep_size
backup in progress
standby/restartpoint needs

它们并不是同一种保护,也不能相互替代。

archive backlog

当 archive command 失败:

completed segment remains needed
  -> retry
      -> pg_wal grows
          -> filesystem full
              -> PostgreSQL PANIC/offline

数据库继续运行一段时间不代表故障无害。官方文档指出,pg_wal 所在文件系统 填满会导致 PANIC;事务不会因此神奇地归档到远端。

监控:

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 = 21
last successful archive is after last failure

可能表示历史上失败过、当前已经恢复。不能仅因累计 count > 0 就报“当前失败”。 反之,长时间没有新 WAL 的系统,last_archived_time 老也未必故障。结合:

  • current WAL segment;
  • archive queue/backlog;
  • WAL generation rate;
  • repository maximum;
  • disk free;
  • recent command errors。

archive lag 与数据风险

两个间隙:

operational backlog
  current WAL - last archived WAL

disaster data gap
  source lost时,last business truth - last independently durable WAL

可用 byte 估计:

SELECT pg_wal_lsn_diff(
         pg_current_wal_lsn(),
         'last known archived LSN'
       );

但 repository 通常按 segment 报告,边界内还有 partially filled segment。 生产 RPO 应用时间和业务 token 做补充。

为什么低流量也有 archive delay

archive command 通常对完成的 segment 工作。若写入很少:

important transaction commits
segment remains open
no archive object yet

选择:

  • archive_timeout 定期强制 switch;
  • 关键变更后 pg_switch_wal()
  • pgBackRest check 触发/验证;
  • streaming WAL 到独立系统。

代价是更多 segment object 和带宽。策略应由 RPO 与成本反推。

physical replication slot

physical slot 保护某个 consumer 尚未接收的 WAL:

SELECT slot_name,
       slot_type,
       active,
       restart_lsn,
       wal_status,
       safe_wal_size,
       inactive_since
FROM pg_replication_slots;

如果 consumer 永久消失而 slot 保留,WAL 可无限增长,除非 max_slot_wal_keep_size 设限。设限后 slot 也可能变为不可继续,需重建 replica。

槽保护 streaming consumer,不等于 archive 成功:

slot retained WAL on primary disk
  != independent disaster copy

logical slot 更容易被忽视

logical slot 还关联 catalog horizon:

  • 保留 WAL;
  • 可能保留 dead tuples/catalog rows;
  • consumer lag 影响磁盘与 vacuum;
  • failover/同步 slot 有版本与配置前提。

监控 slot 要看:

active
restart_lsn
confirmed_flush_lsn
wal_status
safe_wal_size
xmin/catalog_xmin
owner and consumer

没有 owner 的 slot 是容量事故候选。

wal_keep_size

wal_keep_size 是最近 WAL 的最低保留量,帮助无 slot standby 应对短暂断开。 它:

  • 不是硬上限;
  • 不保证某个 consumer 的准确位置;
  • 不写远端仓库;
  • 不提供长期 PITR;
  • 不替代 slot 或 archive。

多个保留者叠加

最终 pg_wal 需要保留到最老需求:

LSNrecycle frontier=min(LSNcheckpoint,LSNarchive,LSNslots,LSNbackup,LSNstandby) LSN_{recycle\ frontier} =\min( LSN_{checkpoint}, LSN_{archive}, LSN_{slots}, LSN_{backup}, LSN_{standby} )

概念上谁最老,谁控制回收边界。因此磁盘告警时不要只看“archive 正常”, 还要看所有 slot、backup lock 与 standby 状态。

本章沙箱事实

正式演练前后观察到:

archive_mode=on
archive command configured
repository stanza status=ok
one S3-compatible repository
target restore-point segment <= repository maximum segment
source timeline remains 7
restore timeline becomes 8

累计 archiver 历史包含早期失败,但最近成功晚于最后失败;本章没有把累计 失败数误判为当前中断。

故障处理顺序

archive backlog:

1. stop unsafe cleanup/expiration
2. measure pg_wal free space and growth rate
3. identify exact archive error and repository availability
4. preserve source WAL
5. restore archive path/credential/capacity
6. confirm backlog drains and repository maximum advances
7. prove a restore target, not just count successes
8. write incident and prevention

不要先:

delete pg_wal files
drop unknown slots
reset stanza
expire repository aggressively

这些动作可能把可恢复性问题变成不可恢复。

本节原生检查单

SHOW wal_level;
SHOW archive_mode;
SHOW archive_command;       -- do not copy secrets into tickets/evidence
SHOW archive_timeout;
SHOW wal_keep_size;
SHOW max_slot_wal_keep_size;

SELECT * FROM pg_stat_archiver;

SELECT slot_name, slot_type, active, restart_lsn,
       confirmed_flush_lsn, wal_status, safe_wal_size
FROM pg_replication_slots;

SELECT pg_current_wal_lsn(),
       pg_walfile_name(pg_current_wal_lsn());

SELECT system_identifier
FROM pg_control_system();    -- compare securely; do not publish raw id

SELECT timeline_id, redo_lsn, checkpoint_lsn
FROM pg_control_checkpoint();

在 replica 上不能调用 primary-only current WAL 函数;使用 replay/receive 位置并标注 observation point。

小结

物理 recoverability 的核心不是“有一个 tar 包”,而是:

one reviewed physical lineage
+ complete base image
+ exact backup metadata
+ continuous WAL
+ reachable timeline history
+ compatible recovery runtime

下一节把这些对象放进仓库依赖图:full、diff、incr 应如何保留,怎样防止 正确的自动过期删除仍被后代依赖的祖先,以及为什么加密不等于不可变。


上一节:从恢复场景设计备份 · 返回本章目录 · 下一节:备份仓库与保留策略 · 查看全书目录 · 查看索引中心

21.3 备份仓库与保留策略

仓库不是“放备份的目录”,而是一张有依赖、权限、密钥、过期与故障域的 历史图。一个对象存在,不代表它独立可恢复;一个对象过期,也可能让一串 后代失去意义。

21.3.1 全量、差异、增量与过期

三种备份的依赖

以 pgBackRest 术语:

full
  不依赖同仓库的更早 backup set

diff
  依赖最近 full,保存相对 full 的变化

incr
  依赖最近一次可作为父级的 backup,保存相对父级的变化

示意:

F1
├── D1
│   ├── I1
│   └── I2
├── I3
└── D2
    └── I4

F2
└── I5

恢复 I4 需要 F1 + D2 + I4;恢复 I2 需要对应链。工具 catalog 负责 依赖,但 retention policy 必须与这张图一致。

“增量”有多个层面

不要只看备份类型标签:

logical incremental
  应用/CDC 按变化导出

file-level incremental
  只复制 mtime/size/checksum 判定变化的文件

block incremental
  只保存改变的数据块

PostgreSQL 18 native incremental
  WAL summaries + manifests + pg_combinebackup

pgBackRest incr
  pgBackRest repository dependency and restore semantics

同一个 incr 单词不保证恢复步骤相同。

full 的价值

full 优点:

  • 依赖图短;
  • 恢复选择更直接;
  • 祖先损坏影响范围小;
  • 容易做离线复制或长期固定点。

代价:

  • 读取和传输量大;
  • checkpoint/I/O 影响;
  • repository 增长;
  • 大库窗口可能长。

但开启 block incremental、bundle 或 dedup 后,“full backup label”不一定 意味着仓库再次保存一份完整未去重字节。要区分:

logical database size
backup delta read
repository delta written
repository total referenced size

本章 formal full:

logical size             36,121,841 bytes
repository delta          4,539,288 bytes

这反映当前 pgBackRest block/bundle/compression 配置,不代表任何生产数据的 压缩率。

differential 的恢复深度

diff 只依赖 full,所以常用于:

weekly full
daily diff
intra-day incr

恢复最近日点可能只需 full + diff,而不是一长串 daily incremental。代价是 diff 随 full 之后的累计变化变大。

incremental 的恢复深度

incr 降低单次备份量,但:

  • chain 更深;
  • 任一依赖损坏影响后代;
  • restore 需要更多 metadata/object;
  • catalog 与过期规则更关键;
  • 小对象延迟可能抵消节省;
  • 高频变化数据未必节省很多。

优化 backup window 不能以恢复路径不可测为代价。

backup cadence 与 WAL replay

备份越旧,恢复到“现在”通常需要重放更多 WAL:

TrecoveryTretrieve backup+Tassemble+Treplay WAL+Tcheckpoint T_{recovery} \approx T_{retrieve\ backup} +T_{assemble} +T_{replay\ WAL} +T_{checkpoint}

因此:

  • 更频繁 base/diff 可缩短 replay;
  • 更频繁备份增加 source/repository 工作;
  • WAL 生成率、CPU、storage latency 影响 replay;
  • parallel restore 不等于 parallel WAL replay 无限扩展。

通过生产规模演练测量,而不是从 backup duration 推算 restore duration。

count 与 time retention

pgBackRest repo1-retention-full-type 决定 repo1-retention-full 的解释:

count
  保留多少 full backup set

time
  以天为阈值保留 full

按 count=2 时,新 backup 要先成功,之后才可能 expire 最旧,因此瞬时可见 三份 full。按 time=20 时,需要存在至少一份达到对应年龄的 full 才形成过期 条件。不要把配置数字直接写成没有验证的“覆盖天数”。

differential retention

repo1-retention-diff 按数量控制 diff。依赖被过期时,相应 incremental 也要一起处理。

政策样例:

full: weekly, retain by time 35 days
diff: daily, retain 7
incr: every 6 hours
WAL: follow oldest retained recovery anchor
month-end logical archive: separate 13 months

这只是示例。真正参数要由场景、数据变化、仓库容量和 restore test 得出。

WAL expiration

PITR 需要连续 WAL。pgBackRest 可随 backup expiration 删除不再被保留 backup 所需的 archive。更激进的 archive retention 能省空间,却可能缩短 PITR window。

原则:

expire backup dependency graph first
derive which WAL is no longer useful
dry-run aggressive archive expiration
never delete WAL merely because it is "old"

一段 WAL 只有相对于某个可用 base 与目标才“有用”。孤立 WAL 不能独立恢复, 但错误删除 bridge segment 会破坏整个窗口。

自动过期的安全条件

在启用 expire 前,至少验证:

  1. 目标 retention 用例已写成例子;
  2. catalog 能画出 full/diff/incr dependency;
  3. newest backup 成功后才触发过期;
  4. repository 容量允许一次失败重试与过渡峰值;
  5. legal hold 不会被普通规则删除;
  6. off-site/immutable copy 的过期独立受控;
  7. 定期恢复最旧目标;
  8. dry-run 输出有人审阅;
  9. 时钟、时区与对象 lifecycle policy 一致;
  10. 删除权限与写入/恢复权限分离。

对象存储 lifecycle 的隐形删除

即使 pgBackRest retention 正确,bucket lifecycle 也可能:

  • 提前删除 object/version;
  • 转冷存储导致 RTO 激增;
  • 删除 multipart/metadata;
  • 与 legal hold 冲突;
  • 在 repository catalog 不知情时改变可用性。

基础设施 lifecycle 必须成为同一份恢复设计的受控输入。

最旧目标演练

只恢复 latest backup 不能验证 retention:

choose oldest required target
  -> identify required backup chain
      -> retrieve cold/off-site objects
          -> obtain historical key
              -> replay full WAL span
                  -> validate business marker

生产演练轮换:

latest target
oldest target
random time target
target across timeline switch
target just before destructive transaction
target whose objects are in cold tier

21.3.2 校验、加密、不可变与异地副本

四个不同属性

integrity
  bytes 未损坏/未被替换

confidentiality
  未授权者不能读取

immutability
  在保留窗口内,包括高权限主体也不能轻易删除/改写

availability
  事故时对象、密钥、网络和权限仍可用

checksum 提供部分 integrity;加密提供 confidentiality;它们都不自动提供 immutability 或 availability。

校验的层次

层次 例子 能发现 不能发现
transport TLS/object ETag 传输破坏的一部分 业务错误
repository pgBackRest checksum object/file mismatch key 丢失
PostgreSQL page data checksum page corruption 正确写入的错值
backup manifest pg_verifybackup 文件/WAL manifest mismatch target 选择错误
restore start recovery log 链条可重放 业务不变量
business token/对账 目标事实差异 所有未来依赖

每层都必要,但结论边界不同。

pgbackrest check

check 用于验证 stanza 配置与 archive path。典型:

sudo -iu postgres \
  pgbackrest --stanza=pg-test --log-level-console=info check

注意 pgBackRest 2.59.0 的 check 不接受 --repo=1;本章第一次 formal 尝试正因把 backup/info 的 repo selector 机械复制给 check,以 exit 31 在恢复前安全失败。这个失败保留了两个教学点:

same tool != every command accepts same option
successful check != successful restore

正式成功运行修正命令后继续。

加密在哪里

可能的层次:

client-side/repository encryption by backup tool
object storage server-side encryption
disk/volume encryption
transport TLS
application/column encryption

每层保护不同攻击面。pgBackRest repository cipher 需要 cipher_pass;如果 配置、备份与密码一起丢失,加密会非常成功地阻止所有人恢复。

密钥生命周期

备份 retention 往往比当前应用 key 生命周期长。密钥设计要回答:

  • 谁能加密、谁能解密、谁能删除?
  • rotation 后旧备份如何恢复?
  • key version 与 backup label 如何关联?
  • KMS/secret store 在区域事故中是否独立?
  • break-glass 如何审批、审计和定期测试?
  • 员工离职、账户冻结和组织恢复如何处理?
  • legal deletion 是否通过 key destruction 实现,证据是什么?

不要把 cipher pass 复制到 evidence、工单或书中。本章 evidence 只记录 cipher=aes-256-cbc,不导出密码。

不可变不是只读 ACL

普通权限:

backup writer can put
restore reader can get
operator can delete

若同一 credential 能写、覆盖、expire 与删除,攻击者拿到它就能破坏历史。

更强设计可能包括:

  • object lock / WORM retention;
  • versioning;
  • 独立账户/项目;
  • MFA-delete 或审批;
  • writer 无 delete;
  • lifecycle role 与 restore role 分离;
  • retention policy 受治理;
  • audit log 写入另一安全域;
  • 定期从不可变副本恢复。

“S3-compatible”不等于实现并启用了这些特性。

不可变也会制造治理问题

设置错误的长期 object lock 会:

  • 无法删除敏感数据;
  • 产生不可控成本;
  • 阻塞环境清理;
  • 与法律删除义务冲突。

因此需要:

retention class
legal hold process
minimum/maximum lock
authorized bypass
evidence and audit
test bucket before production

不可变是政策与控制面组合,不是一个布尔开关。

异地副本的独立性

“异地”至少检查:

physical region
cloud/account/project
identity provider
KMS/key
network/control plane
operator role
automation blast radius
object lifecycle
billing/organization dependency

两 bucket 位于不同 region,但由同一高权限脚本执行 recursive delete,仍有 共同控制域。

3-2-1 只是启发式

常见经验:

3 copies
2 media/system types
1 off-site

它提醒独立性,但不能替代场景合同。三份都被同一 credential 删除,数量没有 意义;一份真正不可变、离站且可恢复的副本,可能比十份同域复制更有价值。

repository namespace

避免不同 cluster 冲突:

repository
  -> stanza
      -> database/system-id lineage
          -> archive id
              -> timeline/WAL

不要把另一个 initdb 出来的 cluster 伪装成旧 cluster 继续向同一 archive namespace 写入。pgBackRest stanza metadata 与 system ID 检查是保护层; 权限与路径隔离仍要做。

沙箱边界

本章仓库:

one local MinIO
S3-compatible
AES-256-CBC repository cipher
shared laptop/hypervisor context
object lock not validated
cross-region copy not validated
independent credential domain not validated

所以正式结论带:

EX21-REPOSITORY-NOT-IMMUTABLE

能够真实恢复,并不抹掉仓库共同故障。

21.3.3 容量预算、失败告警与责任人

容量不是数据库大小乘份数

粗略预算:

Capacity=Bfull+Bdiff+Bincr+WALwindow+Metadata+Versions+SafetyMargin Capacity = B_{full} +\sum B_{diff} +\sum B_{incr} +WAL_{window} +Metadata +Versions +SafetyMargin

还受:

database growth
change rate and full-page images
compression/dedup
block incremental
bundle
index churn
vacuum/rewrite
WAL generated by bulk load/DDL
object versioning
failed/in-progress backup
multipart residue
cold tier overhead

用历史指标和压力场景建模,不只用当前 pg_database_size

两个增长率

data growth rate
  determines future full/restore size

WAL generation rate
  determines archive bandwidth, RPO exposure and replay work

一个 1 TB 数据库每天只改 1%,与每天 rewrite 500 GB 的数据库,备份策略不同。

原生观测:

SELECT now(),
       pg_current_wal_lsn(),
       pg_walfile_name(pg_current_wal_lsn());

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

配合定时 LSN sample 估算 WAL rate。

带宽预算

需要同时考虑:

source read throughput
source network egress
repository write/read throughput
archive sustained throughput
restore download throughput
WAL replay CPU/storage
shared production contention

backup 能在 4 小时窗口内完成,不代表事故时 restore 也能在 4 小时内完成: 方向、并发、cold retrieval 和 target infrastructure 都不同。

容量水位

至少建立:

repository used / total
growth per day/week
forecast days to full
oldest/newest backup
oldest/newest recoverable target
WAL archive backlog
pg_wal filesystem free
backup duration and bytes
restore duration by representative size
object lifecycle transitions

阈值应给行动时间:

warning when forecast leaves enough time to provision
critical before next backup/archive can exhaust capacity

固定 80%/90% 可能对高速增长系统太晚。

备份失败不是一个布尔告警

分类:

scheduler did not start
backup started but failed
backup completed but required WAL missing
repository inaccessible
archive delayed
checksum/integrity failure
retention did not expire
retention expired too much
credential/key near expiry
restore drill failed
business validation failed

每种 owner 与紧急程度不同。

freshness SLI

示意:

age_of_latest_successful_base
age_of_last_independently_archived_wal
duration_of_archive_backlog
days_since_last_successful_restore
age_of_oldest_proven_restore_target

比“backup job success rate 99%”更接近 recoverability。

一次失败后先保护什么

当 backup job 失败:

  1. 确认 archive 仍连续;
  2. 确认 pg_wal 与 repository 容量;
  3. 保存错误上下文与 catalog;
  4. 判断现有 restore window 是否仍满足;
  5. 修复依赖后重试;
  6. 不要先 expire “腾空间”;
  7. 若 RPO 已越线,升级 incident。

当 archive 失败:

  1. 以磁盘耗尽预测为首要风险;
  2. 保护未归档 WAL;
  3. 恢复 repository path;
  4. 确认最大 WAL 前进;
  5. 安排隔离 restore 验证链条。

RACI

一份实际 owner map:

能力 Accountable Responsible Consulted
业务 RPO/RTO 业务 owner SRE/平台 DBA、安全
PostgreSQL backup config 平台 owner DBA/平台 SRE
repository capacity 存储/平台 owner 平台 DBA
credential/key 安全 owner 安全/平台 DBA
retention/legal hold 数据治理 平台/法务 业务
restore runbook 平台 owner DBA/SRE 应用
business validation 业务 owner 应用团队 DBA
production cutover incident/change owner SRE/平台 全方

如果 backup 告警只有“DBA 群”负责,区域、密钥、应用验证很可能无人负责。

每日、每周、每季

示意节奏:

continuous
  WAL/archive/capacity alert

daily
  latest backup, duration, bytes, catalog status

weekly
  dependency/retention review, sampled checksum

monthly
  automated isolated latest/selected restore

quarterly
  representative business validation and oldest target

annually or major change
  regional/break-glass/cutover exercise

频率按 tier 调整,但“从不恢复,只看 job”不属于任何成熟 tier。

可审计政策模板

service: orders
tier: critical
scenarios:
  logical_error:
    rpo: named/audited transaction boundary
    rto_historical_query: 60m
  region_loss:
    rpo: 5m
    rto_read_write: 4h
backup:
  implementation: pgBackRest
  full: weekly
  diff: daily
  incr: 6h
  wal_archive: continuous
repository:
  primary: object-store-a
  immutable_copy: object-store-b
  failure_domain: separate account and region
  encryption_key: independent-dr-key
retention:
  operational_pitr: 35d
  monthly_logical: 13m
validation:
  latest_restore: monthly
  oldest_target: quarterly
  regional_cutover: annual
owners:
  platform: team-db
  business_validation: team-orders
  key: team-security

每个字段都应能找到 evidence,而不只是配置愿望。

小结

仓库的工程对象是:

backup dependency graph
+ continuous WAL window
+ integrity
+ confidentiality
+ immutability
+ independent availability
+ capacity
+ ownership

下一节进入恢复动作本身:怎样从候选 backup 中选择真正能到达 target 的一份, 如何在启动前验证 lineage/WAL,以及为什么 PostgreSQL “一致”仍不足以交付。


上一节:物理备份与 WAL 连续性 · 返回本章目录 · 下一节:恢复流程与验证 · 查看全书目录 · 查看索引中心

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 冲突;
  • 让失败回退更难。

空间预算

至少:

SpaceRestoredData+WALWorkingSet+Temp+Logs+SafetyMargin 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

要求存在从 TbT_bTtT_t 的合法 history path。PITR promote 后的新 timeline TnT_n 应满足:

Tn>Tt 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 章继续连接与服务合同。

验证矩阵

维度 检查 失败动作
lineage same system ID relation stop
timeline new child after promote stop
target expected present/absent stop/reselect
engine recovery=false, writable inspect logs
isolation no TCP, archive off stop immediately
schema version/extensions/constraints repair environment
business domain invariants reject candidate
external reconciliation keep isolated
source original cluster unchanged incident
shutdown private postmaster stopped force 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 交付备份策略 · 查看全书目录 · 查看索引中心

21.5 用 pgBackRest 交付备份策略

Pigsty 负责把 PostgreSQL、pgBackRest、仓库、归档、调度、日志和监控组合成 可交付基线;pgBackRest 负责 backup/archive/restore 机制;PostgreSQL 仍然 定义 WAL、recovery、timeline 与业务数据语义。

三层不要混淆:

PostgreSQL
  physical history and recovery semantics

pgBackRest
  repository, backup sets, archive transport, restore orchestration

Pigsty
  declarative delivery, scheduling, service integration and observability

21.5.1 仓库、策略、调度与凭据

从 Pigsty 声明开始

当前 Pigsty 的核心入口包括:

pgbackrest_enabled: true
pgbackrest_method: minio       # or local/custom
pgbackrest_repo:
  minio:
    type: s3
    s3_endpoint: sss.pigsty
    s3_region: us-east-1
    s3_bucket: pgsql
    path: /pgbackrest
    storage_port: 9000
    block: y
    bundle: y
    bundle_limit: 20MiB
    bundle_size: 128MiB
    cipher_type: aes-256-cbc
    retention_full_type: time
    retention_full: 14

这里只展示非 secret 结构。s3_key_secretcipher_pass 必须来自受控 secret source,不能提交到公开仓库、截图或 evidence。

参数随 Pigsty 版本演进。使用时以目标 tag 的参数参考为准,不能拿当前网页 替历史安装保证。

local 与 MinIO

Pigsty 文档给出的常见方式:

local
  local POSIX repository, default path /pg/backup

minio
  S3-compatible repository, optional MinIO service

custom
  pgBackRest supports other repository backends/config

职责差异:

方式 优势 必须显式接受的风险
local same host/storage 简单、低延迟 主机/磁盘共同故障
shared local/NFS 集中 mount/锁/网络/同域
MinIO/S3-compatible object API、独立部署可能性 endpoint、credential、object semantics
cloud object store 跨域与 durability 选择 账户/KMS/费用/egress/lifecycle

pgbackrest_method=minio 不自动证明 MinIO 位于独立故障域。

stanza

pgBackRest stanza 把一个 PostgreSQL cluster 与 repository metadata 关联:

pgbackrest --stanza=pg-test stanza-create
pgbackrest --stanza=pg-test check
pgbackrest --stanza=pg-test info

Pigsty 在启用备份组件时交付配置/stanza。current 文档给出:

./pgsql.yml -t pg_backup

用于对已存在 cluster 启用相关 subtask;真正执行前:

  • 固定 Pigsty repo/tag;
  • 使用正确 inventory 与 limit;
  • 先 diff/plan 变更;
  • 确认不会删除/重建现有 stanza;
  • 保护 secret output。

删除 cluster 与 stanza

Pigsty 文档提示,移除 primary 时可能删除 pgBackRest stanza,可用相应参数保留 backup。删除 cluster 的动作与删除 backup 历史是两个授权:

remove database service
  != authorize deleting recovery history

所有 pgsql-rm.yml / pg_rm_backup 动作都应进入 destructive review。本章 不执行。

backup 命令

Pigsty 提供 /pg/bin/pg-backup

pg-backup          # documented default/incremental behavior
pg-backup full
pg-backup diff
pg-backup incr

原生 pgBackRest:

sudo -iu postgres \
  pgbackrest --stanza=pg-test --repo=1 \
             --type=full --log-level-console=info backup

使用 wrapper 的好处是平台约定一致;使用原生命令的好处是 exact option 清晰。本章 formal 固定原生命令并把版本、label、duration 与 source hash 写入 evidence。

调度

Pigsty 用 pg_crontab 声明:

pg_crontab:
  - '00 01 * * 1 /pg/bin/pg-backup full'
  - '00 01 * * 2-7 /pg/bin/pg-backup'

本章沙箱实际观察:

Monday 01:00 full
other days 01:00 default backup

调度要回答:

  • 时区;
  • primary role 切换后谁执行;
  • concurrent/overlap lock;
  • missed schedule;
  • retry/backoff;
  • load window;
  • full/diff/incr policy;
  • output/log rotation;
  • alert source。

cron entry 存在不是 job 成功证据。

primary 与 role change

HA cluster 中 backup job 不应因为原 primary 降为 replica 就盲目继续。平台 wrapper/配置应识别角色与 pgBackRest topology。演练切换后检查:

which node owns cron
which node is repository stanza's database endpoint
archive push source
backup-from-standby policy

第 20 章切回原基线后,本章在 pg-test-1 执行 formal full。

async archive 与 spool

pgBackRest 可用 async archive push/get:

PostgreSQL archive command
  -> pgBackRest spool queue
      -> repository worker

好处是降低 postmaster archive command 的同步等待;代价是多一层队列状态。

观察:

spool path
archive-push async log
queue size/age
repository max WAL
pg_stat_archiver
pg_wal free

验证 restore 时应使用独立 spool path,避免测试实例与 live archive worker 共享状态。本章:

/data/pg36-ch21-restore/<run>/spool

凭据

典型 secret:

S3 access key/secret
repository cipher pass
repository TLS client material
remote host key
database backup/replication credential
KMS token

配置文件最低要求:

  • owner/group 与 mode;
  • 不被普通 exporter/log collector 读取;
  • command line/log 自动 redaction 验证;
  • rotation;
  • break-glass restore;
  • 不与 source destruction role 共用;
  • 不进入 pgbackrest info 的公开 projection。

本章 remote pgBackRest console 会把 secret 显示为 <redacted>,但 formal evidence 根本不保存原始 console,只保存结构化无 secret 结果。

仓库 TLS 与 CA

S3-compatible endpoint 应验证:

TLS enabled
expected CA
hostname/SAN
certificate rotation
clock
no silent downgrade

本章沙箱配置有 repository CA path,但未把完整 transport security 作为 生产验收;第 23 章继续。

声明与有效状态

三份证据:

inventory declaration
generated /etc/pgbackrest config
effective command/catalog behavior

不能只看其中一个。生成配置可能漂移,声明可能未应用,运行命令也可能被环境 变量或额外 conf.d 覆盖。

安全投影应保留:

stanza
repo type/path class
endpoint class
cipher type
retention type/value
block/bundle
archive async
spool path

去掉所有 secret value。

21.5.2 备份状态、归档状态与容量观察

pgbackrest info

人读:

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

机器读:

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

关注:

stanza status code/message
backup/restore locks
repository id/cipher
database version and repository key
archive id/min/max
backup label/type/error
timestamp start/stop
archive start/stop
logical and repository bytes
prior/reference dependency

JSON 仍可能含 raw system ID;公开 evidence 应转成 equality relation。

label 解读

示例:

20260729-201041F

常见 suffix:

F full
D differential
I incremental

不要只解析名字推断成功;同时看 catalog error=false、status、archive 和 实际恢复。

check

sudo -iu postgres \
  pgbackrest --stanza=pg-test --log-level-console=info check

它能触发/检查 archive path 与配置。不同 command 的 options 不完全相同:

info/backup/restore accept --repo=1
check in pgBackRest 2.59.0 rejects --repo=1

把一串“通用参数”复制给所有命令是危险习惯。

PostgreSQL 原生交叉验证

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

SELECT pg_current_wal_lsn(),
       pg_walfile_name(pg_current_wal_lsn());

对照:

PostgreSQL last archived
pgBackRest archive max
current WAL
spool queue

不同 observation 可能短暂错开,要用时间戳和 queue 解释。

累计失败数

本章沙箱:

archived_count > 0
failed_count = 21
last successful time > last failed time

这说明过去有失败,当前最近归档成功。正确告警不是:

failed_count > 0 forever critical

而是:

failure counter increased recently
and/or last failure later than last success
and/or archive maximum stops advancing while WAL advances
and/or spool/backlog/free-space breaches

backup freshness

计算:

now - latest successful backup stop

还要按类型:

latest full age
latest diff age
latest any backup age
oldest retained full

一份最新 incremental 可能依赖过旧 full;只看 latest label 隐藏 chain 风险。

archive freshness

低流量时:

last archive time old
current WAL same segment

不一定 backlog。高流量时:

last archive time recent
but repository is many segments behind

仍可能越过 RPO。结合 rate 和 segment distance。

容量

源端:

df -h /pg/data /pg
du -sh /pg/data/pg_wal

repository:

used bytes
object count
growth forecast
version/lock overhead
cold tier
retention expiry

restore target:

df -h /data
df -i /data

本章 .13/data 有充足空间,正式 restore 只有约 36 MB;这不能证明 生产大库空间规划。

日志

Pigsty 常见 pgBackRest log path:

/pg/log/pgbackrest/

分类:

backup
expire
archive-push-async
archive-get-async
restore

日志要集中,但 redaction 后再进入平台。错误排查保留 exec-id、command version、stanza 与 phase。

dashboard 与 alert

Pigsty 提供 PGSQL PITR 等 dashboard/monitoring 入口。图表用于趋势与定位, 机器验收仍回到:

  • PostgreSQL catalog;
  • pgBackRest JSON;
  • repository/storage fact;
  • real restore evidence。

看板绿不等于 restore proof。

观察矩阵

信号 warning critical/action
latest full age 接近 policy 越过恢复合同
archive max lag 上升 RPO 越线/磁盘风险
pg_wal free forecast shrink 可能 PANIC
repository free forecast shrink 下一备份不可完成
backup duration trend regression window/RTO risk
failed count delta new failure inspect/retry
restore drill age 接近周期 recoverability unproven
key expiry rotation window future restore blocked

本章 evidence capture

capture.py 默认只读:

PG36_EVIDENCE_DIR=/new/path \
  static/labs/ch21/task.sh capture

它采集:

Patroni topology
PostgreSQL archive/current WAL/settings
sanitized pgBackRest catalog
no raw system ID
no credential

它明确输出:

recoverability = not-proven-by-capture

因为 capture 没有执行 restore。

21.5.3 在隔离目标而不是原集群上恢复

Pigsty 的恢复入口

当前 Pigsty 文档提供:

manual pg-pitr prompt/script path
pgsql-pitr.yml playbook path

文档说明,熟悉配置时可使用自动 playbook,否则建议逐步手工方式。其本质 原因是恢复 target、source 与 destructive action 需要人理解,不应让 automation 隐藏。

示意:

./pgsql-pitr.yml \
  -e '{"pg_pitr": {"time": "2026-07-29 20:10:44+00"}}'

这类 playbook 可能停止、清空、重建目标 instance。不要在 live cluster 上 为了练习运行。

当前文档 target forms

Pigsty pg_pitr 映射:

pg_pitr: {}
pg_pitr: { time: "2026-07-29 20:10:44+00" }
pg_pitr: { lsn: "0/200002D0" }
pg_pitr: { xid: "250000", exclusive: true }
pg_pitr: { name: "before_release" }
pg_pitr: { type: "immediate" }

使用前核对目标 Pigsty tag 的 exact behavior,以及 exclusiverecovery_target_inclusive 的映射。

为什么本章不直接调用 destructive playbook

本章目标是验证备份,而不是替换服务。正式设计:

source cluster remains running
restore under a brand-new path
no Patroni
no DCS
no TCP
no service endpoint
archive_mode=off
stop after proof
retain directory

Pigsty 提供配置、pgBackRest 与 repository;本章用底层 pgBackRest 对 fresh path 恢复,以精确控制隔离边界。

formal restore command

结构化示意:

sudo -iu postgres pgbackrest \
  --stanza=pg-test \
  --repo=1 \
  --set=20260729-201041F \
  --type=name \
  --target=pg36_ch21_<run>_keep \
  --target-action=promote \
  --target-timeline=latest \
  --archive-mode=off \
  --pg1-path=/data/pg36-ch21-restore/<run>/data \
  --spool-path=/data/pg36-ch21-restore/<run>/spool \
  --log-path=/data/pg36-ch21-restore/<run>/log \
  restore

不要直接复制 placeholder。正式 runner 生成并验证 exact run ID/path,拒绝 已存在目录。

启动覆盖

live Pigsty PostgreSQL config 含:

hba_file=/pg/data/pg_hba.conf
ident_file=/pg/data/pg_ident.conf
log path under /pg/log/postgres
TLS path under /pg/cert
listen_addresses=0.0.0.0
port=5432
standby primary_conninfo/slot

若直接启动 restored config,会碰 live path、网络与复制配置。正式 runner 显式覆盖:

listen_addresses = ''
port = 55432
unix_socket_directories = '<private>/socket'
unix_socket_permissions = 0700
hba_file = '<private>/pg_hba.restore.conf'
ident_file = '<restored>/pg_ident.conf'
ssl = off
archive_mode = off
primary_conninfo = ''
primary_slot_name = ''
shared_preload_libraries = ''
logging_collector = off
cluster_name = 'pg36-ch21-restore'

同时携带 source recovery-critical maxima。

same-host isolation 的边界

formal restore 放在 pg-test-3,而 .13 同时继续运行 live replica:

live:
  /pg/data
  port 5432
  Patroni member pg-test-3

isolated:
  /data/pg36-ch21-restore/<run>/data
  Unix socket only
  internal port 55432
  no Patroni

这是 process/path/network isolation,不是 host/device/failure-domain isolation,形成 EX21-SHARED-RESTORE-HOST。生产 restore 应使用独立主机。

启动完成不能只看 pg_ctl

pg_ctl -D "$restore/data" -w start

可能在只读 consistent state 返回。正式脚本随后:

SELECT pg_is_in_recovery(),
       current_setting('transaction_read_only'),
       current_setting('archive_mode'),
       current_setting('listen_addresses');

等待:

recovery=false
transaction_read_only=false
archive_mode=off
listen_addresses=''

再做 rollback write。

停止并保留

验证后:

pg_ctl -D "$restore/data" -w -m fast stop

检查:

postmaster.pid absent
Unix socket absent
TCP listener absent
restore directory present
live replica still streaming on 5432

保留目录支持审计,但不是长期服务。删除由单独 reset:fixture 授权。

防误操作设计

task.sh

capture|verify|review|all
  no mutation

drill:pitr
  requires exact target + four nonproduction guards + confirmation

reset:fixture
  separate destructive token + exact run ID

all 永远不调用 drill:pitr。这避免 CI/读者为了“跑全套检查”意外再做备份 或启动恢复。

版本迁移

迁移到新版本时,工时不只改命令:

review PostgreSQL recovery setting changes
review pgBackRest command options/output schema
review Pigsty variables/playbooks
recreate sanitized baseline
run negative guards
perform fresh restore
compare readiness phases
update exceptions and evidence

migration-effort.json 列出 production 仍需补的 evidence,不把沙箱通过升级为生产。

小结

Pigsty 与 pgBackRest 的价值,是把可恢复性机制交付成一致、可观察、可自动化 的系统。正确使用方式是:

declare with Pigsty
inspect with pgBackRest + PostgreSQL
prove with isolated restore
govern with explicit authority

下一节执行完整实验,并逐项解释 formal output、两个安全失败、十四个反例和 生产 gate。


上一节:恢复流程与验证 · 返回本章目录 · 下一节:实战:完成一次隔离恢复演练 · 查看全书目录 · 查看索引中心

21.6 实战:完成一次隔离恢复演练

这是一次真实运行过的物理恢复,不是伪造的示例输出。

实验完成:

fresh synthetic marker
fresh full pgBackRest backup
post-backup marker
named restore point
post-target marker
forced WAL switch + pgBackRest check
exact-label restore to a fresh isolated path
read-only and promotion phase measurement
positive/negative business boundary
lineage and timeline proof
isolated shutdown
source postflight
14 adversarial counterexamples

安全边界:

local nonproduction sandbox only
production data/traffic forbidden
source cluster not stopped
live /pg/data not touched
no DCS/Patroni membership for restored copy
no TCP listener
archive push disabled
no routing change
no cleanup during acceptance
production approval pending

21.6.1 创建已知业务检查点并执行备份

先读合同

四个入口:

exact target:

pg36-l2-vagrant/pg-test
Pigsty v4.5.0
PostgreSQL 18.6
pgBackRest 2.59.0
pg-test-1 primary
pg-test-2/3 streaming replicas

任何 version/member/role drift 都先停止。

风险分级

action risk 行为
capture L0 只读采集 source/repository
verify L0 重验已有 evidence
review L0 hash、边界与 secret review
all L0 verify + review
drill:pitr L2 marker、full backup、fresh isolated restore
reset:fixture L3 删除一个 run 的 rows 与 retained directory

普通 all 不包含 drill 或 reset。

无授权必须拒绝

PG36_EVIDENCE_DIR=/new/empty/path \
  static/labs/ch21/task.sh drill:pitr

结果:

exit=77
refusing PITR drill: exact target, nonproduction, data, traffic,
and confirmation guards are required

拒绝发生在任何 marker/backup/restore 前。

chapter-19 gate

formal wrapper 先运行:

PG36_CH19_INVENTORY=/absolute/mode-0600/reviewed.yml \
PG36_EVIDENCE_DIR="$run/preflight-ch19" \
  static/labs/ch19/task.sh all

要求:

hosts=4-distinct
pg-meta one primary
pg-test one primary + two streaming replicas
secret values redacted
sandbox_l2=accepted-with-exceptions
production_ch19_gate=pending

private inventory 不复制到 evidence。它用于验证 exact declaration,不应使用 公开 placeholder 去做真实部署。

source preflight

在 DDL 前采集:

Patroni exact member set
pg-test-1 only leader/running
pg-test-2/3 replica/streaming
replay lag <= 1 MiB
source in_recovery=false
archive_mode=on
current WAL segment/timeline
recovery-critical settings
sanitized pgBackRest catalog

source system identifier 只保存在 runner 内存用于 equality,evidence 不导出 raw value。

fixture

setup.sql

CREATE SCHEMA IF NOT EXISTS pg36_ch21;

CREATE TABLE IF NOT EXISTS pg36_ch21.recovery_probe (
    run_id       text        NOT NULL,
    stage        text        NOT NULL
                             CHECK (stage IN ('base','keep','discard')),
    token        text        NOT NULL UNIQUE,
    committed_at timestamptz NOT NULL DEFAULT clock_timestamp(),
    PRIMARY KEY (run_id, stage)
);

每轮 run ID:

run_YYYYMMDDTHHMMSSZ_<8 hex>

路径与 SQL token 只接受这个 allowlist,避免 path/command injection。

三阶段顺序

insert base
  -> take full backup
      -> insert keep
          -> pg_create_restore_point(name)
              -> insert discard
                  -> pg_switch_wal
                      -> pgBackRest check

顺序的逻辑:

stage 文件基础备份 后续 WAL target 结果
base 已包含或由 backup 所需 WAL 一致化 present
keep backup 后 target 前 present
discard backup 后 target 后 absent

marker 不是 sleep

我们不靠:

sleep 2

猜测时间边界,而是保存:

run ID
unique token
database commit timestamp
restore-point LSN
target WAL filename

因此验证的是 transaction/WAL 边界。

fresh full

formal command:

sudo -iu postgres pgbackrest \
  --stanza=pg-test \
  --repo=1 \
  --type=full \
  --log-level-console=info \
  backup

runner 在前后读取 JSON catalog,要求恰好新增一份

type=full
error=false
stanza status code=0

正式结果:

label                 20260729-201041F
backup command        2085.526 ms
logical bytes         36,121,841
repository delta       4,539,288
backup archive        timeline 7

不公开 repository key/cipher pass。

restore point

runner:

SELECT pg_create_restore_point(
  'pg36_ch21_run_20260729T201040Z_961665aa_keep'
);

具体名称由 run 生成,不应手工照抄。evidence 保存:

name
LSN
WAL segment
created_at UTC

强制并检查 archive

SELECT pg_switch_wal();

然后:

sudo -iu postgres \
  pgbackrest --stanza=pg-test --log-level-console=info check

注意没有 --repo=1。正式成功:

check duration          598.471 ms
target WAL              ...000020
repository max WAL      ...000021
target_wal_covered      true

公开结果可以省略完整 WAL 名;完整 secret-safe evidence 保留以审计。

第一次 formal 尝试为何失败

最初 runner 对 check 传了:

--repo=1

pgBackRest 2.59.0 返回:

ERROR [031]: option 'repo' not valid for command 'check'

失败发生在 restore path 创建前:

backup succeeded
markers retained
no isolated directory
no postmaster
source healthy

我们没有删除失败 backup/marker 来伪装“一次通过”,而是修正 command-specific option,再做一轮 fresh formal evidence。

formal 入口

只在 exact local sandbox:

export PG36_CH19_INVENTORY=/absolute/mode-0600/reviewed.yml
export PG36_EVIDENCE_DIR=/absolute/new-empty/ch21-run

export PG36_CH21_TARGET=pg36-l2-vagrant/pg-test
export PG36_CH21_NONPRODUCTION=true
export PG36_CH21_PRODUCTION_DATA=false
export PG36_CH21_PRODUCTION_TRAFFIC=false
export PG36_CH21_CONFIRM=BACKUP_AND_ISOLATED_PITR_CH21

static/labs/ch21/task.sh drill:pitr

evidence dir 非空则拒绝覆盖。

21.6.2 恢复到隔离集群,核对数据库与业务不变量

restore root

formal:

/data/pg36-ch21-restore/
  run_20260729T201040Z_961665aa/
    data/
    socket/
    log/
    spool/
    pg_hba.restore.conf

preflight:

root exact allowlist
root does not exist
not symlink
port 55432 unused
owner postgres
mode 0700

已存在 path 不做 --delta 覆盖,直接拒绝。

exact backup 与 target

formal restore 绑定:

--set=20260729-201041F
--type=name
--target=<this run keep restore point>
--target-action=promote
--target-timeline=latest
--archive-mode=off

这同时固定:

which base
where replay stops
what happens at target
which timeline path
whether recovered fork can archive

文件恢复

结果:

restore_copy_ms = 2758.206
files/bytes      from exact backup catalog
recovery.signal  present
standby.signal   absent

copy 完成不算 PostgreSQL 恢复完成。

隔离启动

runner 用 exact PostgreSQL 18 pg_ctl,覆盖:

listen_addresses=''
port=55432
private Unix socket mode=0700
private HBA
ssl=off
archive_mode=off
primary_conninfo=''
primary_slot_name=''
shared_preload_libraries=''
logging_collector=off
cluster_name=pg36-ch21-restore

live replica:

/pg/data
port 5432
cluster_name=pg-test
Patroni streaming

两者路径与控制面不相交。

recovery-critical maxima

source 观测:

max_connections=500
max_worker_processes=24
max_wal_senders=50
max_prepared_transactions=0
max_locks_per_transaction=500

formal restore 携带这些值。

开发时曾尝试:

max_connections=20

PostgreSQL 在重放前 FATAL 拒绝。修复方法不是降低 source 或编辑控制文件, 而是让隔离 runtime 满足 source WAL 记录的要求。

两个 readiness 时刻

第一时刻:

start -> first connection = 962.980 ms
pg_is_in_recovery()       = true
transaction_read_only     = true

第二时刻:

start -> promoted         = 1318.876 ms
pg_is_in_recovery()       = false
transaction_read_only     = false

差:

355.896 ms

所以:

pg_ctl -w returned
  != target-action promotion complete

rollback write probe

promotion 后:

BEGIN;
CREATE TEMPORARY TABLE pg36_ch21_writable_probe(value integer);
INSERT INTO pg36_ch21_writable_probe VALUES (1);
ROLLBACK;

结果:

rollback-write-ok

它证明当前连接可写,不给历史副本增加持久业务事实。

business boundary

exact run:

base     present  true
keep     present  true
discard  present  false
unexpected stages 0

如果只看 row count=2,仍可能误拿其他 run;runner 对 exact token 比较。

lineage

system identifier relation = matches source
raw identifier recorded     = false
source timeline             = 7
restored timeline           = 8
timeline increment          = 1

这是同一物理血缘的一次合法 PITR fork。

isolation proof

运行中:

TCP listener        false
Unix socket         true
socket mode         0700
socket owner        postgres:postgres
postmaster.pid      true
archive_mode        off
Patroni managed     false

仅把 port 改成 55432 不算隔离;若 listen_addresses=0.0.0.0,仍会向网络暴露。

停止

pg_ctl -D <exact-data> -w -t 30 -m fast stop

停止后:

postmaster.pid              false
Unix socket                 false
TCP listener                false
restore directory retained  true

目录保留供审查,未自动删除。

source postflight

恢复后重新执行 chapter-19 gate,并在 drill 内检查 .13 live instance:

pg-test-1 leader/running
pg-test-2 replica/streaming
pg-test-3 replica/streaming
all Patroni timeline 7
source system ID unchanged
pg-test-3 in_recovery=true
pg-test-3 replay paused=false
pg-test-3 port=5432
replica lag=0 bytes

isolated timeline 8 不会加入 source Patroni cluster。

不做什么

no pgsql-pitr destructive replacement
no Patroni reinit
no failover/switchover
no /pg/data write
no service routing
no repository expire
no restore directory deletion

这使实验回答“backup 能否恢复”,而不是把多个高风险动作揉成一个结果。

21.6.3 输出 RPO/RTO 实测、证据链和失败处理 SOP

reference result

restore-run.json

{
  "backup_command_ms": 2085.525583,
  "restore_copy_ms": 2758.205792,
  "start_to_first_connection_ms": 962.980167,
  "first_connection_in_recovery": true,
  "start_to_promoted_ms": 1318.876208,
  "first_connection_to_promoted_ms": 355.896041,
  "base_present": true,
  "keep_present": true,
  "discard_present": false,
  "source_timeline": 7,
  "restored_timeline": 8,
  "isolated_postmaster_stopped": true,
  "production_ch21_gate": "pending"
}

它不含 credential 或 raw system ID。

这次 RPO 证明了什么

证明:

fresh full ended before keep
keep WAL and named point were archived
restore stopped at named point
post-target discard did not replay

可以说:

named-point selection for this run passed

不能说:

production RPO = 0
any time in 14 days is recoverable
worst-case archive delay is 0
region loss loses no data

因为实验主动 pg_switch_wal()check,没有模拟 source 在 open segment 尚未归档时突然毁坏。

这次 RTO 测量了什么

已测:

backup command
repository check
file restore copy
start to first read-only
start to promotion

未测:

incident detection
human decision/approval
new host provisioning
cold object retrieval
large production bytes
full business validation
application configuration
DNS/proxy/client cutover
unknown transaction reconciliation
backlog clearing

所以:

2.758s+1.319sRTOproduction 2.758s + 1.319s \not= RTO_{\text{production}}

甚至简单相加也不完整,因为时钟阶段与执行方式要明确。

四类时钟

建议输出:

T_backup
T_restore_copy
T_recovery_consistent/read_only
T_recovery_promoted
T_database_validation
T_business_validation
T_service_cutover
T_backlog_clear

本章前四项中的三个有正式观测,后四项保持未测。

evidence tree

<run>/
├── preflight-ch19/
├── drill/
│   ├── drill-manifest.json
│   ├── source-before.json
│   ├── fixture.json
│   ├── backup.json
│   ├── recovery.json
│   ├── isolated-shutdown.json
│   ├── source-after.json
│   ├── validation-report.json
│   └── negative-report.json
├── postflight-ch19/
└── review.txt

manifest 记录所有 source input SHA-256。restore-run.jsonmigration-effort.json 是 outcome,不参与输入 hash,但 reviewer 会把 published outcome 与 validation report 对齐。

十四个反例

negative-cases.json 要求拒绝:

反例 policy code
从沙箱声称生产通过 E_PRODUCTION_CLAIM
恢复到未审目标 E_TARGET
仓库状态失败 E_REPOSITORY
backup 标记失败 E_BACKUP
target WAL 未覆盖 E_ARCHIVE
recovered archive push 开启 E_ISOLATION
restore 有 TCP listener E_ISOLATION
只读阶段当 promotion 完成 E_PROMOTION
keep 缺失 E_BOUNDARY
discard 存在 E_BOUNDARY
system lineage 不同 E_LINEAGE
timeline 未前进 E_TIMELINE
isolated postmaster 未停止 E_SHUTDOWN
source cluster 退化 E_SOURCE_HEALTH

formal:

case_count=14
actual_code == expected_code for every case
status=ok

为什么测反例

一个 validator 若只接受正确 evidence,可能只是检查文件存在。让每个关键 断言被单独破坏并得到预期错误码,证明 decision rule 真正约束:

positive proof
+ negative falsification

这比“脚本 exit 0”更有审计价值。

read-only 重验

formal 结束后:

PG36_EVIDENCE_DIR=/absolute/completed/ch21-run \
  static/labs/ch21/task.sh all

输出:

status=validation-ok
sandbox_named_pitr=accepted-with-exceptions
counterexamples=14-rejected
status=review-ok
secret_values_exported=0
raw_system_identifiers_exported=0
isolated_postmaster=stopped
restore_directory=retained
production_ch21_gate=pending
mutation=none

它不会重跑 backup/restore。

失败 SOP:backup

backup command fails
  -> preserve console/log exec-id
      -> inspect stanza/repository/archive
          -> check source and disk health
              -> do not select partial label
                  -> retry only after cause and policy review

若已插入 base,它只是 synthetic evidence;不要为“干净”自动删。

失败 SOP:archive gap

target segment > repository max or actual restore requests missing WAL
  -> stop
      -> preserve source pg_wal
          -> inspect spool/archive errors/capacity
              -> recover missing WAL if possible
                  -> choose earlier target only with business approval

不能跳过 WAL 或把“最新可到达点”擅自当成批准 target。

失败 SOP:restore/start

restore fails
  -> retain exact directory/log
      -> ensure no postmaster remains
          -> inspect permission/tablespace/key/WAL
              -> create a new run/path for retry

不要用 --delta 在不清楚内容的目录上反复覆盖。

失败 SOP:target mismatch

base/keep missing or discard present
  -> reject candidate
      -> do not expose service
          -> review target/backup/inclusive/timeline
              -> select a new target with data owner
                  -> restore to another fresh path

PostgreSQL 能启动不降低严重性。

失败 SOP:isolation

若发现:

TCP listener
archive_mode=on
wrong HBA
Patroni/DCS membership
path overlaps /pg/data

立即停止 isolated instance,保留证据,确认 source repository 和 routing 未污染,再调查。不要“验证完再关”。

reset

删除不是 acceptance 的一部分。单独入口要求:

exact target
nonproduction guards
exact run ID
PG36_CH21_RESET_CONFIRM=DELETE_ONE_CH21_SANDBOX_RUN
no postmaster.pid
no Unix socket
path exact allowlist
not symlink

然后只删除一个 retained directory 与对应 run rows。本文 formal 没有执行 reset。

生产 gate

沙箱通过后,生产仍需:

  1. 代表性生产规模与 change/WAL rate;
  2. 独立 production-class restore infrastructure;
  3. immutable、off-site、独立 credential/key 证明;
  4. 任意 time/XID/LSN 与 inclusive 语义;
  5. 最旧 retention target;
  6. missing WAL、lost key、repository outage;
  7. extension/tablespace/config/PKI 完整性;
  8. 应用与外部系统不变量;
  9. service cutover、client outcome 与 backlog;
  10. region loss 与 break-glass access;
  11. 多次样本分布,而不是一次秒数;
  12. 生产 change/incident authority。

migration-effort.json 把这些保持为 required_next_evidence

本章最终判定

sandbox named PITR
  accepted with ten explicit exceptions

PostgreSQL physical lineage
  matched

target boundary
  base + keep, no discard

isolation
  path/process/socket/archive isolated
  shared restore host exception remains

source safety
  healthy before and after

production
  pending

小结

一次可信恢复演练同时具备:

known business boundary
+ fresh or exactly selected backup
+ continuous WAL evidence
+ isolated target
+ two-phase readiness
+ lineage/timeline proof
+ positive and negative invariants
+ source postflight
+ shutdown
+ residual-risk ledger

这才把“我们有备份”升级为“我们曾在明确边界内证明它可以恢复”。

下一章继续服务面:当一个 PostgreSQL 实例真的可写之后,连接池、代理、 路由、会话与客户端重试如何决定用户何时恢复。


上一节:用 pgBackRest 交付备份策略 · 返回本章目录 · 下一章:四通八达:服务接入、连接池与路由 · 查看全书目录 · 查看索引中心