这是本节的多页打印视图。 .
备份恢复
Pigsty 使用 pgBackRest 管理 PostgreSQL 备份 —— 这可能是 PostgreSQL 生态中最强大的开源备份工具, 支持增量备份、并行处理、加密、Silo/S3 对象存储等众多特性。 每个 PGSQL 集群默认都预配置了备份与 WAL 归档,开箱即用。
本章是备份恢复的 实操手册:配置方法、管理命令、恢复操作与演练教程。 设计理念与心智模型(为什么、如何权衡)请参阅概念层文档 时间点恢复。
所有的备份恢复操作,最终都会落实为 pgBackRest 命令。Pigsty 在它之上提供了三层封装,按需选用:
| 层次 | 接口 | 形态 | 适用场景 |
|---|---|---|---|
| 集群编排 | pg_pitr 参数 + pgsql-pitr.yml 剧本 |
Ansible 剧本 | 生产集群恢复:编排 HA、etcd 与多节点 |
| 实例编排 | pig pitr |
命令行工具 | 单节点恢复:在数据库节点上直接编排执行 |
| 命令原语 | pig pb / pb 别名 / pg-backup 脚本 |
pgBackRest 封装 | 备份、查询、清理,以及非托管实例的裸恢复 |
| 底层引擎 | pgbackrest |
原生命令 | 一切封装的最终执行者 |
| 章节 | 内容 |
|---|---|
| 机制 | pgBackRest 核心概念(stanza / 仓库 / 保留 / 时间线)与 Pigsty 的封装映射 |
| 策略 | 设计备份策略:调度计划、恢复窗口与磁盘空间规划 |
| 仓库 | 配置备份仓库:本地、Silo、外部 S3,加密、版本控制与锁定 |
| 管理 | 备份管理命令手册:启停、手动备份、查看、清理、Stanza 管理 |
| 恢复 | 执行时间点恢复:恢复目标、分步执行、参数参考 |
| 克隆 | 用 PITR 克隆数据库集群:找回数据、恢复演练 |
| 示例 | 沙箱教程:使用 pgBackRest 原语手工执行恢复 |
Pigsty 尽最大努力提供可靠的 PITR 解决方案,但我们不对 PITR 操作导致的数据丢失承担任何责任,使用需自担风险。如需专业支持,请考虑我们的 专业服务。
执行 PITR 前必须先核对 pig pg list <目标集群> 与 pig pb info,确认近期可用备份和恢复窗口,
由操作者复述精确的目标集群与恢复点,再执行目标限定的 ./pgsql-pitr.yml -l <目标集群> ...。
pgsql-pitr.yml 只打印计划,不会暂停等待确认;生产恢复还需要维护窗口和独立验证过的备份。
快速上手
- 设计备份策略:用
pg_crontab声明定时备份计划,用pgbackrest_repo选择备份仓库 - 管理备份:用
pg-backup手动备份,用pb info查看备份状态 - 执行恢复:用
pg_pitr参数声明恢复目标,运行pgsql-pitr.yml剧本
1 - 备份机制
Pigsty 的备份恢复功能,最终都会落实为 pgBackRest 命令的执行。 所以要真正掌握这套系统,需要理解两件事:pgBackRest 本身的概念体系(stanza、仓库、备份链、保留、时间线), 以及 Pigsty 各层封装如何映射到它(参数如何变成命令行选项)。本页依次讲清这两部分。
pgBackRest 核心概念
Stanza:集群的备份身份
Stanza(节)是 pgBackRest 中一套 PostgreSQL 集群备份配置的名称,也是仓库内隔离不同集群的命名空间。
Pigsty 将 stanza 直接映射为集群名 pg_cluster:
集群 pg-meta 的备份就存储在仓库的 backup/pg-meta/ 与 archive/pg-meta/ 目录下,
多套集群可以安全共享同一个仓库。
Stanza 记录着源集群的 system-id 与主版本号,写入备份前会核对身份 —— 这正是
克隆集群 后需要 stanza-upgrade 善后的原因。
Stanza 由 stanza-create 创建(Pigsty 在集群初始化时自动完成),
版本升级后用 stanza-upgrade 更新。
仓库:备份存放在哪里
仓库(Repository)是备份与 WAL 归档的存储后端,在配置中以 repo1-* 系列选项定义:
repo1-type 决定类型(POSIX 文件系统、S3、Azure、GCS、SFTP),repo1-path 决定路径,
repo1-cipher-* 决定加密,repo1-retention-* 决定保留策略。
Pigsty 通过 pgbackrest_repo 参数生成这些配置,详见 备份仓库。
备份链与备份标签
pgBackRest 支持三种备份类型,后两种依赖前面的备份构成 备份链:
| 类型 | 内容 | 标签后缀 |
|---|---|---|
| 全量备份(full) | 完整复制数据库集群 | F |
| 差异备份(diff) | 相对最近一次 全量 的变化 | D |
| 增量备份(incr) | 相对最近一次 任意备份 的变化 | I |
每个备份都有唯一的 备份标签(label),命名规则本身就描述了备份链:
20250715-013657F 是一个全量备份,20250715-013657F_20250715-013724D 是基于它的差异备份,
20250715-013657F_20250715-013730I 是增量备份 —— 下划线前的部分标识它所依附的全量备份。
恢复时用 --set 指定从哪个备份集开始还原,默认自动选择目标时间点前最近的一个。
保留策略:仓库如何不被撑爆
保留策略(Retention)决定旧备份何时被清除。repo1-retention-full 配合
repo1-retention-full-type(count 按份数 / time 按天数)控制全量备份的保留;
全量备份过期时,依附它的差异/增量备份与对应的 WAL 归档一并清除。
Pigsty 默认启用 expire-auto,每次备份完成后自动执行过期清理,也可用
expire 命令手动触发。
time 表示 最短时间窗口,不是“只保留最近 N 天内的全量备份”。只有在仓库中还存在另一份年龄达到 N 天的全量备份时,
更老的全量备份才会过期。因此 retention_full: 14 配合每周全备,稳态会保留三条全量备份链,恢复窗口约为 14~21 天。
WAL 归档:保留下来的历史
PostgreSQL 的 archive_command 在每个 WAL 段写满(或 archive_timeout 超时)后触发,
Pigsty 将其配置为 archive-push,把 WAL 推入仓库;
恢复时则由 restore_command 调用 archive-get 按需拉取。
Pigsty 启用了异步归档(archive-async=y),经由 /pg/spool 假脱机目录批量推送,避免归档拖慢主库。
时间线:分叉的历史
每次恢复提升(或故障切换)都会开启一条新的 时间线(Timeline),旧时间线的 WAL 仍保留在仓库中。
恢复时可用 --target-timeline 指定沿哪条时间线重放(默认 latest),
这使得"恢复错了再恢复一次"成为可能。完整心智模型见概念层 工作原理。
恢复机制:restore 命令做了什么
理解 restore 命令的行为,就理解了 PITR 的执行过程。它做两件事:
- 重建数据目录:从备份集还原数据文件。带
--delta选项(Pigsty 默认启用)时执行增量还原 —— 校验现有文件,只重写与备份不一致的部分,大幅缩短大库的还原时间。 - 写入恢复配置:生成
recovery.signal标记与恢复参数(restore_command、recovery_target_*), 使 PostgreSQL 下次启动时进入恢复模式,从仓库拉取 WAL 重放至目标点。
因此 restore 命令返回成功只是完成了一半:真正的恢复发生在 PostgreSQL 启动之后的 WAL 重放阶段。
重放到达目标后的行为由 --target-action 决定:pause 暂停等待检查、promote 提升开启新时间线、shutdown 停机。
恢复目标的参数组合是固定句式:--type 指定目标类型,--target 给出目标值,
可选的 --target-exclusive 控制边界、--target-timeline 选择时间线、--set 指定起点备份:
实际观察
您可以使用 pg_dbsu 用户(默认 postgres)直接执行 pgbackrest 命令,
观察上述概念的实际形态 —— 注意备份标签的命名、备份大小的差异,以及 info 输出中的备份链引用关系:
备份命令
Pigsty 的封装层次
在原生命令之上,Pigsty 提供了逐层升高的封装,每一层都只是对下一层的参数化调用,没有黑魔法:
| 层次 | 接口 | 它实际做什么 |
|---|---|---|
| 集群编排 | pg_pitr + pgsql-pitr.yml |
编排整个集群的恢复:暂停 HA → 停库 → 渲染配置并调用 pgbackrest restore → 输出控制信息 → 清理 etcd → 拉起 HA |
| 实例编排 | pig pitr |
单节点编排:预检 → 停 Patroni/PG → 调用 pgbackrest restore → 可选启动 PG,Patroni 保持停止 |
| 命令原语 | pig pb / pb 别名 / pg-backup |
自动填充 --stanza 与 DBSU 身份,转发给 pgbackrest 对应子命令 |
| 底层引擎 | pgbackrest |
读取 /etc/pgbackrest/pgbackrest.conf,实际执行备份/归档/恢复 |
命令原语层
pb 是登录 shell 内置的别名函数:从配置文件解析出 stanza 后转发,让您少敲一个参数:
pg-backup 脚本在此基础上增加了 角色检查(只在主库执行,从库直接退出),供 crontab 安全调用:
pig pb 提供了更完善的封装:自动检测 stanza、自动以 DBSU 身份执行(root 下 su、普通用户 sudo)、
备份前检查主库角色、恢复前显示执行计划并要求确认。完整子命令表见 管理命令。
参数如何映射
Pigsty 恢复接口的每个参数,都对应一个 pgbackrest 选项 —— 三层接口共用同一套语义:
pg_pitr 字段 |
pig pitr 选项 |
pgbackrest 选项 | 含义 |
|---|---|---|---|
cluster |
--stanza |
--stanza |
从哪个集群的备份恢复(源 stanza) |
type + time/xid/lsn/name |
--time/--xid/--lsn/--name |
--type + --target |
恢复目标 |
| (type: default) | --default |
不传 --type/--target |
重放到 WAL 归档末尾 |
| (type: immediate) | --immediate |
--type=immediate |
恢复到最近一致点 |
exclusive |
--exclusive / -X |
--target-exclusive |
停在目标之前(排除目标点) |
action |
--target-action |
--target-action |
到达目标后:pause / promote / shutdown |
timeline |
--target-timeline / -T |
--target-timeline |
目标时间线,默认 latest |
set |
--set / -b |
--set |
从哪个备份集开始还原 |
db_include / db_exclude |
— | --db-include / --db-exclude |
选择性恢复部分数据库 |
link_map |
— | --link-map |
表空间/目录软链接映射 |
process |
— | process-max |
恢复并行进程数 |
data |
--data / -D |
--pg1-path |
恢复到哪个数据目录 |
repo |
— | repo1-*(渲染临时配置) |
覆盖备份仓库定义;pig pitr --repo 仅选择现有配置中的仓库编号 |
选择性恢复仍是物理恢复:未包含或被排除的数据库会以稀疏清零文件还原,以便 PostgreSQL 完成恢复,但这些数据库本身不可访问,
恢复后需要显式删除。它不是 pg_dump 那样的逻辑子集恢复。
配置如何渲染
pgbackrest_repo 中被 pgbackrest_method
选中的仓库定义,会按简单规则渲染进 /etc/pgbackrest/pgbackrest.conf:
键名的下划线替换为连字符,加上 repo1- 前缀 —— 因此 pgBackRest 支持的仓库级配置项
可以直接写入参数:
执行 pgsql-pitr.yml 时,Pigsty 会另行渲染一份临时配置 /pg/conf/pitr.conf(避免污染常规配置),
该剧本启动恢复期间的 PostgreSQL 日志写入 /pg/tmp/recovery.log。
定时备份
Pigsty 使用 Linux crontab 调度备份任务:pg_crontab 参数中的条目
会写入 postgres 用户的 crontab。因为 pg-backup 自带角色检查,同一份 crontab 可以下发到集群所有节点 ——
故障切换后,新主库会自动接续后续定时备份。
修改后用剧本应用变更:
如何选择备份频率与保留策略,请参阅 备份策略。
部署细节
pgBackRest 组件在 pgsql.yml 剧本中完成安装与配置:
- 随
pg_packages中的pgsql-common组安装,二进制位于/usr/bin/pgbackrest pg_backup子任务负责渲染配置、创建 stanza;由pgbackrest_enabled控制(默认启用)- 集群初始化后默认尝试执行一次 初始全量备份,且只在备份成功后留下
/etc/pgbackrest/initial.done标记防止重复; 由pgbackrest_init_backup控制
文件层次
| 路径 | 用途 |
|---|---|
/usr/bin/pgbackrest |
二进制,来自 PGDG 仓库的 pgbackrest 包 |
/etc/pgbackrest/pgbackrest.conf |
主配置文件(stanza + 仓库定义) |
/pg/backup |
本地仓库数据目录(local 仓库时使用) |
/pg/spool |
异步归档的假脱机目录 |
/pg/log/pgbackrest/ |
备份/归档/恢复日志,pgbackrest_log_dir |
/pg/conf/pitr.conf |
PITR 过程中的临时 pgbackrest 配置 |
/pg/tmp/recovery.log |
PITR 过程中的 PostgreSQL 恢复日志 |
监控
每个节点运行 pgbackrest_exporter 服务(端口 pgbackrest_exporter_port:9854),
将备份状态导出为监控指标(参见 pgBackRest 监控指标)。
可通过 pgbackrest_exporter_options 定制,
或将 pgbackrest_exporter_enabled 设为 false 禁用。
2 - 备份策略
备份策略要回答三个问题:何时 备份(调度计划)、何处 存放(备份仓库)、 保留多久(保留策略)。本页给出两套久经考验的预设策略及其量化推演 —— 背后的权衡逻辑请参阅概念层文档 策略权衡。
备份频率与恢复速度直接相关:恢复时需要从最近的基础备份开始重放 WAL 日志到目标时间点, 备份越频繁,需要重放的 WAL 越少,恢复越快;而保留策略与仓库空间直接相关:窗口越长,占用空间越大。
每日全量备份
对于生产数据库,建议从最简单的每日全量备份策略开始。Pigsty 随附的标准 pigsty.yml 集群示例采用这一策略,
配合默认的 local 本地仓库(保留最近 2 个全量备份)使用:
假设您的数据库大小为 100GB,每天写入 10GB 数据,备份耗时 1 小时。
下图将「恢复窗口」与「存储空间占用」合并到同一时间轴(0~108h)上推演该策略的稳态行为:
恢复窗口在 24~48 小时 之间循环,备份占用约为 2 个全量备份加上 1~2 天的 WAL 归档。
在实践中,您需要准备至少 3~5 倍 数据库大小的备份磁盘,才能从容使用该默认策略。
tooltip: { trigger: axis, formatter: $fn:tipMerged, axisPointer: { type: line, snap: true, label: { show: false } } }
axisPointer: { link: [ { xAxisIndex: [0, 1] } ] }
legend: { show: false, bottom: 10, itemGap: 18, data: ["首要备份", "次要备份", "WAL归档", "瞬时备份"] }
grid:
- { left: 82, right: "10%", top: 42, height: 218, containLabel: false }
- { left: 82, right: "10%", top: 286, height: 218, containLabel: false }
xAxis:
- type: category
gridIndex: 0
position: bottom
boundaryGap: false
data: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108]
name: 时间 h
nameLocation: end
nameGap: 10
nameTextStyle: { align: left, verticalAlign: top, padding: [8, 0, 0, 0] }
axisLabel: { interval: 11, formatter: $fn:fmtHour }
axisLine: { show: true, symbol: [none, arrow], symbolSize: [10, 14], lineStyle: { width: 1.6, color: "#4b5563" } }
axisTick: { show: true, length: 6 }
splitLine: { show: true, lineStyle: { type: dashed, width: 1, opacity: 0.28, color: "#9ca3af" } }
minorTick: { show: true, splitNumber: 12, length: 3 }
minorSplitLine: { show: true, lineStyle: { type: dotted, width: 1, opacity: 0.14, color: "#9ca3af" } }
- type: category
gridIndex: 1
position: top
boundaryGap: true
data: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108]
axisLabel: { show: false }
axisLine: { show: true, lineStyle: { width: 1.6, color: "#4b5563" } }
axisTick: { show: true, length: 6 }
splitLine: { show: true, lineStyle: { type: dashed, width: 1, opacity: 0.22, color: "#9ca3af" } }
yAxis:
- type: value
gridIndex: 0
min: 0
max: 52
interval: 5
name: 恢复窗口 h
nameLocation: end
nameRotate: 0
nameGap: 8
nameTextStyle: { align: left, verticalAlign: bottom, padding: [0, 0, 8, 4] }
axisLabel: { formatter: $fn:fmtWin }
axisLine: { show: true, symbol: [none, arrow], symbolSize: [10, 14], lineStyle: { width: 1.6, color: "#4b5563" } }
axisTick: { show: true, length: 6 }
splitLine: { show: true, lineStyle: { type: dashed, width: 1, opacity: 0.35, color: "#9ca3af" } }
minorTick: { show: true, splitNumber: 5, length: 3 }
minorSplitLine: { show: true, lineStyle: { type: dotted, width: 1, opacity: 0.18, color: "#9ca3af" } }
- type: value
gridIndex: 1
min: 0
max: 350
interval: 50
inverse: true
name: 存储空间 GB
nameLocation: end
nameRotate: 0
nameGap: 8
nameTextStyle: { align: left, verticalAlign: top, padding: [10, 0, 0, 4] }
axisLabel: { formatter: $fn:fmtGbTick }
axisLine: { show: true, symbol: [arrow, none], symbolSize: [10, 14], lineStyle: { width: 1.6, color: "#4b5563" } }
axisTick: { show: true, length: 6 }
splitLine: { show: true, lineStyle: { type: dashed, width: 1, opacity: 0.32, color: "#9ca3af" } }
series: [ { name: 恢复窗口, type: line, smooth: false, symbol: none, showSymbol: false, xAxisIndex: 0, yAxisIndex: 0, lineStyle: { width: 3, color: "#f2a000" }, itemStyle: { color: "#f2a000" }, data: [[0,0],[1,0],[2,1],[3,2],[4,3],[5,4],[6,5],[7,6],[8,7],[9,8],[10,9],[11,10],[12,11],[13,12],[14,13],[15,14],[16,15],[17,16],[18,17],[19,18],[20,19],[21,20],[22,21],[23,22],[24,23],[25,24],[26,25],[27,26],[28,27],[29,28],[30,29],[31,30],[32,31],[33,32],[34,33],[35,34],[36,35],[37,36],[38,37],[39,38],[40,39],[41,40],[42,41],[43,42],[44,43],[45,44],[46,45],[47,46],[48,47],[49,48],[49,24],[50,25],[51,26],[52,27],[53,28],[54,29],[55,30],[56,31],[57,32],[58,33],[59,34],[60,35],[61,36],[62,37],[63,38],[64,39],[65,40],[66,41],[67,42],[68,43],[69,44],[70,45],[71,46],[72,47],[73,48],[73,24],[74,25],[75,26],[76,27],[77,28],[78,29],[79,30],[80,31],[81,32],[82,33],[83,34],[84,35],[85,36],[86,37],[87,38],[88,39],[89,40],[90,41],[91,42],[92,43],[93,44],[94,45],[95,46],[96,47],[97,48],[97,24],[98,25],[99,26],[100,27],[101,28],[102,29],[103,30],[104,31],[105,32],[106,33],[107,34],[108,35]], markLine: { symbol: none, label: { show: false }, data: [ { xAxis: 0, lineStyle: { color: "#59a14f", type: "solid", width: 1.4, opacity: 0.75 } }, { xAxis: 24, lineStyle: { color: "#59a14f", type: "solid", width: 1.4, opacity: 0.75 } }, { xAxis: 48, lineStyle: { color: "#59a14f", type: "solid", width: 1.4, opacity: 0.75 } }, { xAxis: 72, lineStyle: { color: "#59a14f", type: "solid", width: 1.4, opacity: 0.75 } }, { xAxis: 96, lineStyle: { color: "#59a14f", type: "solid", width: 1.4, opacity: 0.75 } }, { xAxis: 1, lineStyle: { color: "#336791", type: "solid", width: 1.4, opacity: 0.8 } }, { xAxis: 25, lineStyle: { color: "#336791", type: "solid", width: 1.4, opacity: 0.8 } }, { xAxis: 49, lineStyle: { color: "#336791", type: "solid", width: 1.4, opacity: 0.8 } }, { xAxis: 73, lineStyle: { color: "#336791", type: "solid", width: 1.4, opacity: 0.8 } }, { xAxis: 97, lineStyle: { color: "#336791", type: "solid", width: 1.4, opacity: 0.8 } }, { yAxis: 24, label: { show: true, formatter: "稳态下限 24h", position: "end", distance: 12, color: "#2563eb" }, lineStyle: { color: "#2563eb", type: "dashdot", width: 1.4, opacity: 0.75 } }, { yAxis: 48, label: { show: true, formatter: "窗口峰值 48h", position: "end", distance: 12, color: "#7c3aed" }, lineStyle: { color: "#7c3aed", type: "dashdot", width: 1.4, opacity: 0.75 } } ] } }, { name: 首要备份, type: bar, stack: used, xAxisIndex: 1, yAxisIndex: 1, barWidth: 5, itemStyle: { color: "#59a14f" }, data: [0,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100] }, { name: 次要备份, type: bar, stack: used, xAxisIndex: 1, yAxisIndex: 1, barWidth: 5, itemStyle: { color: "#4e79a7" }, data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100] }, { name: WAL归档, type: bar, stack: used, xAxisIndex: 1, yAxisIndex: 1, barWidth: 5, itemStyle: { color: "#edc949" }, data: [0,0.42,0.83,1.25,1.67,2.08,2.5,2.92,3.33,3.75,4.17,4.58,5,5.42,5.83,6.25,6.67,7.08,7.5,7.92,8.33,8.75,9.17,9.58,10,10.42,10.83,11.25,11.67,12.08,12.5,12.92,13.33,13.75,14.17,14.58,15,15.42,15.83,16.25,16.67,17.08,17.5,17.92,18.33,18.75,19.17,19.58,20,10.42,10.83,11.25,11.67,12.08,12.5,12.92,13.33,13.75,14.17,14.58,15,15.42,15.83,16.25,16.67,17.08,17.5,17.92,18.33,18.75,19.17,19.58,20,10.42,10.83,11.25,11.67,12.08,12.5,12.92,13.33,13.75,14.17,14.58,15,15.42,15.83,16.25,16.67,17.08,17.5,17.92,18.33,18.75,19.17,19.58,20,10.42,10.83,11.25,11.67,12.08,12.5,12.92,13.33,13.75,14.17,14.58,15] }, { name: 瞬时备份, type: bar, stack: used, xAxisIndex: 1, yAxisIndex: 1, barWidth: 5, itemStyle: { color: "#9ca3af", opacity: 0.75 }, data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,0,0,0,0,0,0,0,0,0,0,0,0] } ]全量 + 增量备份
如果使用 Silo / S3 作为集中式备份仓库,存储空间不再受本地磁盘限制, 此时可以用「周全量 + 每日增量」配合两周保留策略,换取更长的恢复窗口:
同样假设数据库 100GB、每日增量与 WAL 均按 10GB 粗估,下图推演 30 天内恢复窗口与存储占用的变化:
retention_full_type: time 会确保至少留下一份年龄达到 14 天的全量备份,周全备时恢复窗口在 14~21 天 之间循环。
按这组未压缩假设,稳态占用约为 560~690GB,新全量完成、旧链过期前的瞬时峰值约为 790GB;
实际占用取决于 WAL 量、块级增量命中率与 zstd 压缩率。
tooltip: { trigger: axis, formatter: $fn:tipMerged30, axisPointer: { type: line, snap: true, label: { show: false } } }
axisPointer: { link: [ { xAxisIndex: [0, 1] } ] }
legend: { show: false, bottom: 10, itemGap: 18, data: ["基础全量", "追加全量", "增量备份", "WAL归档"] }
grid:
- { left: 82, right: "10%", top: 42, height: 218, containLabel: false }
- { left: 82, right: "10%", top: 302, height: 218, containLabel: false }
xAxis:
- type: value
gridIndex: 0
position: bottom
boundaryGap: false
min: 0
max: 31
interval: 1
name: 时间
nameLocation: end
nameGap: 10
nameTextStyle: { align: left, verticalAlign: top, padding: [8, 0, 0, 0] }
axisLabel: { formatter: $fn:fmtDay30 }
axisLine: { show: true, symbol: [none, arrow], symbolSize: [10, 14], lineStyle: { width: 1.6, color: "#4b5563" } }
axisTick: { show: true, length: 6 }
splitLine: { show: true, lineStyle: { type: dashed, width: 1, opacity: 0.28, color: "#9ca3af" } }
minorTick: { show: true, splitNumber: 4, length: 3 }
minorSplitLine: { show: true, lineStyle: { type: dotted, width: 1, opacity: 0.14, color: "#9ca3af" } }
- type: category
gridIndex: 1
position: top
boundaryGap: true
z: 10
data: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]
axisLabel: { show: false }
axisLine: { show: true, lineStyle: { width: 1.6, color: "#4b5563" } }
axisTick: { show: true, alignWithLabel: true, length: 6 }
splitLine: { show: true, lineStyle: { type: dashed, width: 1, opacity: 0.22, color: "#9ca3af" } }
yAxis:
- type: value
gridIndex: 0
min: 0
max: 576
interval: 72
name: 恢复窗口 h
nameLocation: end
nameRotate: 0
nameGap: 8
nameTextStyle: { align: left, verticalAlign: bottom, padding: [0, 0, 8, 4] }
axisLabel: { formatter: $fn:fmtWin30 }
axisLine: { show: true, symbol: [none, arrow], symbolSize: [10, 14], lineStyle: { width: 1.6, color: "#4b5563" } }
axisTick: { show: true, length: 6 }
splitLine: { show: true, lineStyle: { type: dashed, width: 1, opacity: 0.35, color: "#9ca3af" } }
minorTick: { show: true, splitNumber: 4, length: 3 }
minorSplitLine: { show: true, lineStyle: { type: dotted, width: 1, opacity: 0.18, color: "#9ca3af" } }
- type: value
gridIndex: 1
min: 0
max: 800
interval: 100
inverse: true
z: 10
name: 存储空间 GB
nameLocation: end
nameRotate: 0
nameGap: 8
nameTextStyle: { align: left, verticalAlign: top, padding: [10, 0, 0, 4] }
axisLabel: { formatter: $fn:fmtGbTick30 }
axisLine: { show: true, symbol: [arrow, none], symbolSize: [10, 14], lineStyle: { width: 1.6, color: "#4b5563" } }
axisTick: { show: true, length: 6 }
splitLine: { show: true, lineStyle: { type: dashed, width: 1, opacity: 0.32, color: "#9ca3af" } }
series:
- { name: 恢复窗口, type: line, smooth: false, symbol: none, showSymbol: false, xAxisIndex: 0, yAxisIndex: 0, lineStyle: { width: 3, color: "#f28e2c" }, itemStyle: { color: "#f28e2c" }, data: [[1,0],[2,24],[3,48],[4,72],[5,96],[6,120],[7,144],[8,168],[9,192],[10,216],[11,240],[12,264],[13,288],[14,312],[15,336],[16,360],[17,384],[18,408],[19,432],[20,456],[21,480],[22,504],[22,336],[23,360],[24,384],[25,408],[26,432],[27,456],[28,480],[29,504],[29,336],[30,360]], markLine: { symbol: none, label: { show: false }, data: [ { xAxis: 1, lineStyle: { color: "#336791", type: "solid", width: 1.4, opacity: 0.65 } }, { xAxis: 8, lineStyle: { color: "#336791", type: "solid", width: 1.4, opacity: 0.65 } }, { xAxis: 15, lineStyle: { color: "#336791", type: "solid", width: 1.4, opacity: 0.65 } }, { xAxis: 22, lineStyle: { color: "#336791", type: "solid", width: 1.4, opacity: 0.65 } }, { xAxis: 29, lineStyle: { color: "#336791", type: "solid", width: 1.4, opacity: 0.65 } }, { yAxis: 336, label: { show: true, formatter: "窗口下限 14d", position: "end", distance: 12, color: "#2563eb" }, lineStyle: { color: "#2563eb", type: "dashdot", width: 1.4, opacity: 0.72 } }, { yAxis: 504, label: { show: true, formatter: "窗口上限 21d", position: "end", distance: 12, color: "#7c3aed" }, lineStyle: { color: "#7c3aed", type: "dashdot", width: 1.4, opacity: 0.72 } } ] } }
- { name: 基础全量, type: bar, stack: used, xAxisIndex: 1, yAxisIndex: 1, barWidth: 16, itemStyle: { color: "#59a14f" }, data: [100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100] }
- { name: 追加全量, type: bar, stack: used, xAxisIndex: 1, yAxisIndex: 1, barWidth: 16, itemStyle: { color: "#4e79a7" }, data: [0,0,0,0,0,0,0,100,100,100,100,100,100,100,200,200,200,200,200,200,200,200,200,200,200,200,200,200,200,200] }
- { name: 增量备份, type: bar, stack: used, xAxisIndex: 1, yAxisIndex: 1, barWidth: 16, itemStyle: { color: "#76b7b2" }, data: [0,10,20,30,40,50,60,60,70,80,90,100,110,120,120,130,140,150,160,170,180,120,130,140,150,160,170,180,120,130] }
- { name: WAL归档, type: bar, stack: used, xAxisIndex: 1, yAxisIndex: 1, barWidth: 16, itemStyle: { color: "#edc949" }, data: [0,10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170,180,190,200,140,150,160,170,180,190,200,140,150] }空间规划
两套策略的空间需求可以按以下经验公式粗估(实际占用受压缩与块级增量影响,通常更低):
| 策略 | 恢复窗口 | 空间粗估 | 建议预留 |
|---|---|---|---|
| 每日全量,保留 2 份(local) | 24~48 小时 | 2 × 全量 + 1~2 天 WAL | 数据库大小的 3~5 倍 |
| 周全量 + 日增量,按时间保留 14 天(minio) | 14~21 天 | 3 × 全量 + 12~18 份增量 + 14~21 天 WAL | 按实测压缩率与 WAL 量规划 |
注意 瞬时峰值:新的全量备份成功完成后,旧备份才会参与过期计算,因此仓库会短暂多出一份全量备份, 同时保留尚未清理的旧备份链与 WAL。空间规划必须覆盖这个峰值而非只看清理后的稳态。
WAL 归档的增长与写入负载成正比。批量导数、VACUUM FULL、大规模 UPDATE 都会瞬间产生大量 WAL,
如果备份仓库空间紧张,请在此类操作前后关注仓库水位(监控指标 开箱即用)。
应用策略变更
备份策略的三类变更,分别用对应的剧本任务应用:
注意:切换 pgbackrest_method 到新仓库后,旧仓库中的备份不会自动迁移;
在新仓库完成首次全量备份之前,恢复窗口存在缺口。
3 - 备份仓库
备份存储在哪里,由两个参数决定:pgbackrest_repo 定义所有候选仓库,
pgbackrest_method 选择实际使用哪一个。
仓库定义中的键值会 按固定规则 渲染为 pgbackrest 的 repo1-* 配置项,
因此 pgBackRest 支持的任何仓库选项 都可以直接写入。
v4.5.0 每次只把 pgbackrest_method 选中的一个字典项渲染为 repo1;候选项同时存在并不等于多仓备份。
默认仓库
Pigsty 预置了两个仓库定义:local 与 minio。
local:默认选项,使用本地/pg/backup目录(软链接指向pg_fs_backup:/data/backups)minio:使用 MINIO 模块部署的 Silo 或任意 S3 兼容对象存储(Pigsty 支持,默认不启用)
两套仓库的预置策略有意不同:local 不加密、不打包、按份数保留,追求简单与恢复速度;
minio 加密(AES-256-CBC)、打包(bundle)、块级增量(block)、按时间保留两周,面向生产容灾。
使用远程仓库时,请务必修改默认的 cipher_pass 加密密码与 s3_key_secret 访问密钥。
示例中的 pgBackRest 与 S3User.Backup 是公开默认值,不能直接用于生产环境。
加密密码一旦遗失,仓库中的备份将无法解密恢复 —— 请与备份分开妥善保管,具体要求参阅 部署安全。
保留策略
如果只备份不清理,仓库迟早被撑爆。保留策略在仓库定义中声明,由 pgBackRest 在每次备份后自动执行清理 (全量备份过期时,依附它的差异/增量备份与对应 WAL 归档一并清除):
retention_full_type: count+retention_full: 2:按份数保留 —— 保留最近 2 个全量备份(新备份完成前短暂存在第 3 个)retention_full_type: time+retention_full: 14:按时间保留 —— 至少留下一份年龄达到 14 天的全量备份后,才会删除更老的备份
保留策略与恢复窗口、空间占用的量化关系,请参阅 备份策略; 手动清理过期备份的方法,请参阅 管理命令。
使用 Silo 仓库
MINIO 模块 当前部署 Silo S3 兼容对象存储。
只有把对象存储部署在数据库主机或站点的故障域之外时,它才提供独立的容灾副本。部署对象存储集群后,将备份方法切换为 minio:
Pigsty 的 minio 仓库预设通过域名(默认 sss.pigsty)和 HTTPS 端点访问对象存储,
并使用自签名 CA(/etc/pki/ca.crt)验证这条链路。
默认的 pgsql 桶与 pgbackrest 访问用户在 MINIO 模块初始化时自动创建。
对于严肃的生产部署,建议使用经过验证的多节点对象存储集群(MNMD,纠删码容错),参阅 MINIO 配置。
pgbackrest_method: minio 是 Pigsty 的 S3 兼容仓库预设名,并不要求服务端必须由 MINIO 模块管理。独立部署和管理的 MinIO、RustFS 或其他 S3 兼容服务也可以使用该预设,但其安装、升级、证书和数据生命周期不在当前 MINIO 角色的支持范围内。
使用 S3 / 云对象存储
如果您只有一个节点,最有意义的备份策略就是使用云厂商的对象存储服务(AWS S3、阿里云 OSS 等), 以极低的成本获得异地容灾能力。定义一个新仓库并切换过去即可:
除 S3 兼容存储外,pgBackRest 还支持以下后端,配置方式参阅官方用户指南:
多集群共享仓库
一个集中式仓库可以同时服务多套 PostgreSQL 集群:pgBackRest 用 stanza
(即 pg_cluster)隔离各集群的备份与归档,互不干扰。
这也是 克隆集群 的基础 —— 新集群可以直接从共享仓库中读取源集群的备份进行恢复。
因此,请确保同一仓库下的集群名称 全局唯一,即使它们分属不同的部署环境。
仓库版本控制
对象存储的 版本控制(Versioning)为仓库提供同一存储系统内的版本保护:即使备份文件被覆盖或删除,历史版本仍有机会找回。
它与当前仓库共享同一故障域和管理平面,不能替代独立的异地或离线副本。
您可以在 minio_buckets 中为桶添加 versioning 标志启用:
配合 pgBackRest 的 repo-target-time 选项,
甚至可以把整个仓库"回滚"到过去某一时刻的状态读取 —— 相当于对备份系统本身做时间点恢复。
仓库锁定
部分对象存储(Silo、MinIO、S3 等)支持 对象锁定(Object Lock / WORM):对对象版本配置保留模式与期限后, 锁定版本在保留期内不可修改、不可永久删除。这是对抗勒索攻击的重要防线,但普通删除仍可能写入 Delete Marker, 暂时隐藏当前对象;恢复时需要保留版本与版本管理能力。
在 minio_buckets 中添加 lock 标志,只会在创建桶时启用对象锁定能力与版本控制:
这一步还没有给新对象设置 WORM 保留期。您还需要在 Silo / MinIO 中使用 mcli retention set 或控制台配置默认的
GOVERNANCE / COMPLIANCE 模式与期限,并用 mcli retention info 验证。GOVERNANCE 可以被拥有 bypass 权限的主体绕过;
COMPLIANCE 在期限内连 root 用户也不能解除。
锁定会改变 过期清理 与 移除集群备份 的行为:pgBackRest 可以让对象在逻辑上过期,但被锁定的历史版本仍会占用空间,直到保留期结束。上线前应在测试桶中验证 备份、expire、删除标记清理和版本恢复的完整流程。
切换仓库
变更仓库定义或切换 pgbackrest_method 后,需要重新渲染配置、初始化 stanza,并尽快建立新仓库中的恢复起点:
旧仓库中的备份不会自动迁移,但在其保留期内仍可作为恢复来源使用(通过 pg_pitr.repo 指定)。
4 - 管理命令
备份管理命令应以数据库超级用户(pg_dbsu,默认 postgres)身份在数据库节点上执行。
您可以按习惯选择三种等价的入口:
pig pb:pig 命令行工具 的封装 —— 自动检测 stanza、自动切换 DBSU 身份、带安全检查,推荐使用pb:登录 shell 的别名函数,自动填充--stanza后转发给 pgbackrestpgbackrest:原生命令,完整选项参阅 pgBackRest 命令参考
命令一览
| pig 命令 | 别名 | 对应 pgbackrest 命令 | 说明 |
|---|---|---|---|
pig pb info |
i |
info |
查看备份与归档状态 |
pig pb list |
ls |
— | 列出仓库中的 stanza 与备份 |
pig pb backup [full/diff/incr] |
b |
backup |
执行备份(自动检查主库角色) |
pig pb restore |
r |
restore |
恢复(显示计划并确认,详见 恢复操作) |
pig pb expire |
e |
expire |
按保留策略清理过期备份(--plan 预览) |
pig pb create |
c |
stanza-create |
创建 stanza |
pig pb upgrade |
u |
stanza-upgrade |
升级 stanza(版本升级或克隆善后) |
pig pb delete |
d |
stanza-delete |
删除 stanza 及其全部备份 |
pig pb check |
ck |
check |
校验备份配置与归档链路 |
pig pb start |
up |
start |
恢复 pgbackrest 操作 |
pig pb stop |
dw |
stop |
暂停 pgbackrest 操作 |
pig pb log [list/show/tail] |
l |
— | 查看 pgbackrest 日志 |
启用备份
如果集群创建时 pgbackrest_enabled 为 true(默认),备份已自动启用。
若创建时禁用了备份,或修改了仓库配置,可用 pg_backup 子任务补充配置:
集群初始化后 Pigsty 会自动尝试执行一次初始全量备份;只有备份命令成功后才写入 /etc/pgbackrest/initial.done,失败会被剧本忽略且不会留下标记。该文件只防止初始化任务重复执行,最终仍应使用 pig pb info(或 pgbackrest info)核对仓库中的实际备份状态。
定时备份计划通过 pg_crontab 声明,详见 备份策略。
移除备份
pig pb delete 是只删除备份 stanza 的首选入口。它会在实际执行前交互确认;多 stanza 配置还要求显式给出目标。核对目标后执行:
移除主实例(pg_role = primary)时,pgsql-rm.yml 默认也会尝试删除该集群的备份 stanza。以下命令会直接修改或删除状态:
执行前应确认近期备份可用、记录恢复需求,并由操作者再次输入精确的集群/stanza 名。使用 pg_rm_backup(设为 false)可以在移除集群时保留备份。
pgsql-rm.yml -t pg_backup 会在主库上强制执行 pgbackrest stanza-delete,删除本地仓库目录(仅 local 模式),并移除 pgBackRest 配置与初始备份标记;该任务会忽略部分删除错误。因此执行后必须再次检查仓库,不能把剧本“成功”当成备份已物理清空的证明。只需要删除 stanza 时,优先使用上面的 pig pb delete,它提供计划与确认保护。
如果备份仓库为对象版本配置了 对象锁定保留期, 删除可能只写入 Delete Marker,被锁定的历史版本会继续占用空间,直到保留期结束。
删除备份可能造成永久数据丢失。执行前必须确认目标集群/stanza、核对近期备份与替代恢复副本,并保留 pig pb info/删除计划的审计记录。
手动备份
crontab 之外,随时可以手动触发备份。pg-backup 脚本与 pig pb backup 都带主库角色检查,在从库上执行会直接退出,不会产生错误的备份:
备份期间会显著占用磁盘 I/O 与网络带宽(并行度已限制在 2~4 进程),建议安排在业务低峰执行。
查看备份
pb info 列出仓库中当前 stanza 的备份与 WAL 归档状态:
输出解读的关键是 备份标签:
20250715-013657F 为全量备份(F),..._20250715-013724D 为差异备份(D),..._20250715-013730I 为增量备份(I)——
下划线前的部分标识备份链所依附的全量备份。wal archive min/max 显示 WAL 归档范围,
它与最早的全量备份共同描述 恢复窗口。
监控系统同样提供备份状态的持续观测:pgbackrest_exporter(端口 9854)导出的
指标 覆盖最近备份时间、类型、大小与错误状态,可直接用于告警。
清理过期备份
保留策略默认在每次备份后自动执行(expire-auto)。手动触发或预览清理计划:
Stanza 管理
Stanza 记录集群的备份身份(system-id 与主版本), 以下场景需要手动管理:
最常见的手动场景是 克隆集群的善后:
从其他集群的备份恢复出新集群后,stanza 中记录的 system-id 与新集群不符,
必须执行 stanza-upgrade 之后,新集群的备份才能写入仓库。
检查与启停
check 命令会实际推送一个 WAL 段并确认其到达仓库,是排查"归档不工作"问题的第一步。
查看日志
使用 pgsql-pitr.yml 时,PostgreSQL 的恢复日志位于 /pg/tmp/recovery.log。
替代备份工具
pg-basebackup
Pigsty 另备有不依赖 pgbackrest 的独立备份脚本 /pg/bin/pg-basebackup,
它使用原生 pg_basebackup 生成单文件物理备份(lz4 压缩 tarball),默认写入 /pg/backup。
适合在不便使用备份仓库时快速留存一份物理副本:
pg-basebackup -e 使用 OpenSSL RC4 加密 —— 这是一个已被淘汰的弱加密算法,仅作混淆用途,
不应作为机密性保障。需要加密备份时,请使用 pgbackrest 仓库的 AES-256 加密(cipher_type: aes-256-cbc)。
逻辑备份
pg_dump 生成的逻辑备份 不能 用于 PITR,但它是跨大版本迁移、导出部分数据、长期归档快照的正确工具。
物理备份与逻辑备份互为补充,严肃的生产环境通常两者兼备。这是 PostgreSQL 自带的工具,请参阅 官方文档
5 - 恢复操作
Pigsty 提供三个层次的恢复入口,共用 同一套参数语义,按场景选用:
| 入口 | 适用场景 | 特点 |
|---|---|---|
pgsql-pitr.yml 剧本 |
生产集群恢复 | 编排整个集群:HA 暂停、多节点、etcd 清理、恢复控制信息输出 |
pig pitr 命令 |
单节点集群 / 节点本机操作 | 无需管理节点,在数据库节点上直接编排执行 |
pig pb restore 原语 |
非 Patroni 托管的实例 | pgbackrest restore 的直接封装,最精细的控制 |
手把手的沙箱演练教程请参阅 手工恢复; 用恢复克隆出新集群(不影响生产的推荐姿势)请参阅 克隆数据库集群。
pgsql-pitr.yml 会暂停 HA、停止 Patroni/PostgreSQL、以 pgbackrest --force restore 覆盖目标数据目录,
随后删除目标集群的 etcd 前缀并重建 HA;它只打印计划,不会等待人工确认。
执行任何实质恢复前,必须先用 pig pg list <目标集群> 核对当前拓扑、用 pig pb info 核对近期备份与恢复窗口,
由操作者复述并确认精确的目标集群与恢复点。生产恢复仍应安排维护窗口并保留独立、已验证的备份。
快速上手
要将 pg-meta 集群回滚到之前的时间点,声明 pg_pitr 参数并运行剧本:
参数也可以通过命令行临时传入,两种方式等价:
-e 传入的参数必须是合法 JSON:键与字符串值都要加双引号,例如 {"pg_pitr": {"time": "...", "archive": true}}。
布尔值不加引号,字符串必须加 —— 引号缺失会导致参数解析失败或静默取错值。
剧本会依次执行:暂停 Patroni 高可用 → 停止集群进程 → 执行 pgbackrest 增量还原 → 启动 PostgreSQL 并等待进入一致恢复状态 →
用 pg_controldata 打印控制信息 → 清理 etcd 元数据 → 重新拉起集群与高可用。
执行过程的第一步会打印完整的恢复计划(源集群、目标、还原命令),但不会暂停等待确认;上面的一步式示例因此显式声明了 action: promote。
如果需要在目标点检查数据,请使用下文的 分步执行,并显式选择 action: pause。
恢复目标
pg_pitr 支持 六类恢复目标,其中四类目标值互斥,只能指定一个:
恢复目标类型
未指定任何目标时,重放全部 WAL 归档恢复到最新状态(内部类型 default);
immediate 类型在到达第一个一致点后立即停止,用于最快恢复出可用实例(例如验证备份)。
按时间恢复
最常用的目标。时间应为合法的 PostgreSQL TIMESTAMP 格式,建议带时区:YYYY-MM-DD HH:MM:SS+TZ:
按名称恢复
在高危变更前用 pg_create_restore_point 打点,恢复时便有了无歧义的目标:
按事务 ID 恢复
如果误删数据的事务号已知(从监控仪表盘或 CSVLOG 的 TXID 字段获取),
配合 exclusive 精确停在该事务 之前,一条数据都不多丢:
按 LSN 恢复
LSN(日志序列号)标识 WAL 流中的精确位置,
可从 Pigsty 仪表盘的 PG LSN 面板获取;需要时可以用 timeline 指定目标时间线(默认 latest):
恢复目标默认是"包含"(inclusive)的:目标点上的事务会被重放。
exclusive: true 排除目标点本身 —— 例如 xid: 250000, exclusive: true 时,最后被重放的是 249999 号之前已提交的事务。
仅适用于 time、xid、lsn 目标,对应 PostgreSQL 的 recovery_target_inclusive。
恢复来源
默认从本集群自己的备份恢复,三个字段可以改变恢复来源:
cluster:源 stanza —— 使用共享仓库中 其他集群 的备份恢复(克隆集群 的基础)repo:临时指定备份仓库定义(格式同pgbackrest_repo的仓库条目),例如从旧仓库或异地仓库恢复set:从指定的 备份标签 开始还原(默认自动选择目标点前最近的备份集,用pb info查看可用标签)
分步执行
一步到位固然方便,但在生产事故中,您可能希望亲手控制每个阶段。剧本的任务树支持用 tags 三步走:
每步之间您可以检查状态:down 之后确认进程已停;pitr 阶段返回后,先查看 /pg/tmp/recovery.log,
并用 pg_is_in_recovery()、pg_is_wal_replay_paused()、pg_last_wal_replay_lsn() 与 pg_last_xact_replay_timestamp()
确认是否已经到达目标,再结合业务查询抽查数据。pg_controldata /pg/data 提供的是检查点与时间线摘要,不能单独证明时间、XID 或 LSN 目标已经命中。
使用 action: pause 时,确认无误后先提升实例,再执行 up;如果恢复目标选错了,可在 up 之前调整 pg_pitr 重跑 pitr 阶段。
pause / shutdown 需要配合这种分阶段流程才能形成明确的人工门;一步式执行应显式使用 action: promote。
backup: true 会把当前数据目录搬到 /pg/data-backup,而再次运行时会先删除已有的 /pg/data-backup。
因此剧本支持分阶段执行,但不能把带 backup: true 的恢复笼统视为幂等操作。
PITR 参数定义
pg_pitr 的完整字段如下。恢复目标、目标动作与原数据保留方式都建议显式声明:
每个字段与 pgbackrest 选项的对应关系,见 参数映射表。
单实例:pig pitr
在数据库节点上,pig pitr 无需 Ansible 环境即可执行单节点恢复编排:
预检(校验目标、stanza 与备份存在性)→ 停止 Patroni 与 PostgreSQL → 执行还原 → 按参数决定是否启动 PostgreSQL → 恢复后指引。
常用选项:-b/--set 指定备份集,-T/--target-timeline 指定时间线,--target-action 指定到达目标后的动作,
-D/--data 恢复到其他数据目录(此时必须配合 --no-restart)。
默认只用安全的 fast 模式停库,失败即中止 —— 除非显式给出 --force-stop,才允许升级为强制停库。
对于 Patroni 托管的数据目录,命令恢复后会让 Patroni 保持停止;验证数据后再执行 pig pt start。
pig pitr 不清理 etcd、不重建副本,也不会自动把实例重新加入 HA 集群。
完整选项参阅 pig pitr 命令手册。
原语:pig pb restore
对于 不由 Patroni 托管 的实例(或已明确停管的场景),可以使用最底层的恢复原语 ——
它是 pgbackrest restore 的直接封装,自动处理 stanza、DBSU 与时间格式,
执行前显示恢复计划并要求确认:
两道内置的安全边界值得了解:
- Patroni 托管实例会被硬拒绝:若 Patroni 服务活跃且目标是其托管的数据目录,
pig pb restore直接报错退出 —— 因为 Patroni 会立刻把恢复到一半的实例重新拉起。托管实例请使用pig pitr或pgsql-pitr.yml。 - PostgreSQL 必须已停止:实例仍在运行时拒绝执行。
-- 之后可透传原生 pgbackrest 选项(如 --tablespace-map、--link-all),
但恢复目标、stanza、仓库等关键选项已被封装接管,不允许透传覆盖。详见 pig pb 命令手册。
恢复后处理
恢复完成后,剧本会打印控制信息并重建高可用,但仍有三件事需要确认:
6 - 克隆数据库集群
克隆是恢复能力最有价值的用法:不动生产集群,把它的历史状态恢复到另一套集群上。 误删数据后从克隆库中导回、定期演练验证备份可用性、审计取证查看历史状态、把测试环境重置为生产某刻的快照 —— 这些场景的操作方式完全相同,本页给出完整流程。
目标集群需要能访问源集群的备份仓库、允许被覆盖,并使用兼容的 PostgreSQL 主版本。使用集中式仓库(Silo / S3)时, 仓库中以 stanza 隔离的各集群备份,对持有相应凭据的目标集群可见。
先用 pig pg list <目标集群> 核对目标拓扑、用 pig pb info 核对源 stanza 的近期备份与恢复窗口,
并由操作者确认精确的源集群、目标集群和恢复点后实施恢复。
目标集群上原有的数据会被覆盖;生产操作仍需维护窗口和独立、已验证的备份。
克隆现有集群
假设四节点沙箱中有 pg-meta 与 pg-test 两套集群,共享 Silo 备份仓库。
要把 pg-test 重置为 pg-meta 的 最新状态,只需在 pg_pitr
中把恢复来源指向 pg-meta 的 stanza:
配合恢复目标,可以克隆到恢复窗口内的 任意时间点 —— 例如把 pg-test 重置为 pg-meta
在 2025 年 12 月 26 日 15:30 的状态:
跨集群克隆显式设置 archive: false,在独立恢复阶段关闭归档;Patroni 接管后按下面的步骤处理目标 stanza 与归档。
目标集群也可以是全新创建的空集群:先用标准流程 创建集群(如 pg-meta2),
再对它执行跨集群 PITR,即完成了"从备份仓库引导新集群"。
pgBackRest 的还原是增量的(delta):只重写与备份不一致的文件。
因此对反复执行的演练、或已通过 备份集群(Standby Cluster)
物理复制拉齐过数据的目标集群,克隆速度会显著快于首次全量还原。
误删数据的典型找回流程到这里只剩最后一步:在克隆集群中验证数据无误后,
用 pg_dump 导出受影响的表/库,导回生产集群。全库原地回滚是最后手段,而不是第一反应。
克隆善后
克隆出的新集群带着 源集群的数据,但备份仓库中它自己的 stanza 仍记录着 原来的身份(system-id)。 pgBackRest 写入备份前会核对身份,不一致即拒绝 —— 这个保护机制防止新集群的备份污染源集群的备份历史。
因此,确认克隆结果符合预期后,必须执行三步善后,新集群的备份链路才能恢复正常。 其中集群重启是服务变更:先核对主库、复制状态和维护窗口,并获得明确批准:
跳过善后的后果是可预期的:下一次例行备份会因身份核对失败而报错, 在此期间新集群 没有备份保护(如果关闭了归档,也不会产生新的 WAL 归档):
重建备份身份
stanza-upgrade 让新集群沿用原 stanza 继续写备份。如果您希望新集群拥有 全新的备份历史
(例如克隆出的集群将长期独立演化),也可以选择彻底重建其备份配置 —— 声明式方式:
或者用 pgbackrest 原语 手动完成同样的事情:
只有在已经核对近期备份、保留了需要的独立恢复副本,并由操作者确认精确的 pg-test stanza 后,才可执行删除。对象锁定仓库中的历史版本可能继续保留并占用空间;删除命令成功不等于底层版本已经物理清空。
在线副本:备份集群
克隆得到的是 静态的时间点快照。如果需要的是持续跟随源集群的 在线副本, 应使用 备份集群(Standby Cluster,基于流复制); 需要"一直落后一小时"的快速反悔窗口,则使用 延迟集群。
三者互为补充:备份集群提供实时副本,延迟集群提供固定延迟的反悔窗口, PITR 克隆提供恢复窗口内的历史快照 —— 且不需要提前准备在线副本。
恢复演练
克隆是 不触碰生产集群的恢复演练:目标集群会被覆盖,但它能端到端验证备份系统的每个环节。 建议将以下演练纳入例行运维(每季度,或每次重大变更后):
- 选定演练目标:生产集群恢复窗口内的某个时间点;
- 向演练集群执行跨集群 PITR,记录耗时 —— 这就是实测的 PITR RTO;
- 验证数据完整性:行数抽查、关键业务表校验、应用连通测试;
- 执行 克隆善后,确认演练集群自身备份恢复正常(验证善后流程本身也是演练的一部分);
- 记录结果:恢复耗时、发现的问题、文档与实际操作的出入。
沙箱环境中使用 pgbackrest 原语手工执行恢复的完整教程,参阅 手工恢复; 在同一台机器上用 XFS 快照快速 Fork 实例的进阶技巧,参阅 Fork 实例。