---
date: 2026-05-14
type: learning
source: hands-on sandbox
related: issue #342 (备份与异地容灾 PRD)
tags: [pgbackrest, postgres, backup, pitr, disaster-recovery]
---

# pgbackrest 沙箱实操学习

> 跑沙箱的目的：本人没有 pgbackrest 实操经验，但要写 #342 备份与容灾 PRD。**先建一个能跑 PITR 的最小可用样例，再写 PRD**，比直接综述文档可信度高。
>
> 沙箱代码：`/tmp/pgbackrest-sandbox/`（不入 git）—— postgres:16 + pgbackrest 2.58.0，Docker compose 1 个 PG + 1 个本地 repo。
> 配套样本：3500 行 marker 表 ≈ 29.5MB DB。

---

## 关键结论速览（给 PRD 用）

| 现象 | 沙箱实测 | 对 PRD 的影响 |
|---|---|---|
| PITR 精度 | **秒级**（按 `--target=YYYY-MM-DD HH:MM:SS` 解析） | RPO ≈ 几分钟可达成，issue 验收条款 C（误删 30min 内恢复到误删前 10s）**可行** |
| 全量备份耗时 | 29.5MB DB → 2.9s | RTO 主要瓶颈在数据量级 + WAL 重放，不是 pgbackrest 本身 |
| Restore 耗时 | 29.5MB → 1.7s（空白卷）/ 0.5s（delta） | RTO ≤ 4h 对应可处理的 DB 大小 ≫ 当前生产规模 |
| 端到端恢复（含 PG 启动 + 重放） | 空白卷 → 数据可用 ≈ 10s | 验收条款 A（空白 VM 4h 内可用）有数量级 buffer |
| 备份包含 conf 文件 | postgresql.conf / pg_hba.conf / postgresql.auto.conf **都在备份里** | 不需要单独备份 PG 配置，但**自定义 init scripts**（如 `docker-entrypoint-initdb.d/`）**不在备份里**，需另存 |
| archive_command 失败时主库行为 | **不阻塞写入**，WAL 在 pg_wal/ 堆积；`pg_stat_archiver.failed_count` 上涨；PG 按退避（实测 ~30-60s）重试 | **必须监控** `pg_stat_archiver.failed_count` 和 `pg_wal` 目录大小 → 告警 webhook，否则静默失败拖死磁盘 + 静默丢 PITR 能力 |
| archive 修复后 | PG 自动追平，不需要人工干预 | 短暂中断（< 1 小时）可容忍，长期失败必须有告警 |

---

## Q1：archive_command 失败时主库怎样

### 实验

1. 起 PG + archive_command 指向 pgbackrest
2. 跑 stanza-create + 首次 full backup（baseline 正常）
3. `chmod 000 /var/lib/pgbackrest/archive` 故意搞坏 repo 权限
4. 写入 1000 行 + `pg_switch_wal()` × 5 次强制切 WAL

### 观察

```
baseline 后:
  pg_wal 大小: 65MB / 6 个 WAL 文件
  archived_count = 4, failed_count = 4

破坏后写入并强制切 WAL × 5 次:
  pg_wal 大小: 145MB / 11 个 WAL 文件     ← 涨了 80MB
  archived_count = 4 (没变), failed_count = 16
  写入 INSERT 仍然成功，主库不阻塞

修复 chmod 750 后 ~60s + 一次 pg_switch_wal():
  archived_count = 10 (追平了)
  failed_count = 16 (历史失败计数不清零)
  pg_wal 大小: 145MB (没立即收缩,等 checkpoint)
```

### 教训 → PRD 决策

1. **archive_command 失败不会马上拖死 PG**，但 pg_wal 会无界增长直到磁盘满 → 监控不可少
2. **`failed_count` 是单调增长的累计值**，告警逻辑要用增长率（delta）或 `last_failed_wal > last_archived_wal` 而不是绝对值
3. **PG 退避重试间隔**：实测约 30-60s（非 PG 配置项；硬编码的 ARCHIVE_RETRY_INTERVAL）
4. **修复后自动追平**：不需要人工干预，除非 pg_wal 已满或 WAL 已被 PG 自己回收

### PRD 落地

在 `docs/ops/backup-and-dr/` 必须包含：

- 监控指标：`pg_stat_archiver.failed_count` 增量 + `pg_wal/` 大小阈值
- 告警动作：>15 分钟未追平 → 告警；pg_wal > N GB → 紧急告警
- Runbook：archive 故障排查 SOP（先查 repo 可达性 + 权限 + 磁盘空间）

---

## Q3：PITR 任意时间点 — 最小命令序列

### 实验

```
T0 = 沙箱启动 + baseline full backup
T1 (07:09:27.305) = INSERT marker 'T1-pre-disaster'
T2 (07:09:31.497) = INSERT marker 'T2-pre-disaster'
T3 (07:09:35.679) = DROP TABLE marker             ← 灾难
PITR 目标 = '2026-05-14 07:09:32'                  ← T2 之后 1s, T3 之前
```

### 恢复命令（最小集）

```bash
# 1. 停掉主库（生产上是 pg_ctl stop -m fast 或 systemctl stop postgresql）
docker compose stop pg

# 2. 跑 pgbackrest restore（sidecar 容器，挂同卷）
docker run --rm \
  -v <pgdata-vol>:/var/lib/postgresql/data \
  -v <repo-vol>:/var/lib/pgbackrest \
  --user postgres pgbackrest-sandbox-pg \
  bash -c "pgbackrest --stanza=demo --delta \
                      --type=time \
                      --target='2026-05-14 07:09:32' \
                      --target-action=promote \
                      restore"

# 3. 起 PG，自动进入 recovery，应用 WAL 到 target，promote 退出 recovery
docker compose start pg
```

### 观察

- restore 阶段 484ms（增量 delta，因为只回滚 disaster 期间的少量改动）
- PG 启动 + recovery replay ≈ 3-5s
- 验证 SELECT：marker 表存在，含 T1 + T2 + 之前所有数据，**不含 DROP**
- `pg_is_in_recovery() = false`（target-action=promote 直接退出 recovery 进入正常服务）
- 推进了 **新时间线**：原 timeline 1 → 恢复后 timeline 2（`.history` 文件自动生成并 push 到 repo）

### 教训 → PRD 决策

1. **PITR 精度真的到秒级**：`--target='YYYY-MM-DD HH:MM:SS'` 字符串解析，时区跟随服务器 TZ
2. **`--delta` 选项**：不全量擦写 data dir，只覆盖差异文件 → 在原机器上恢复**极快**；空白机器恢复必须**不**用 --delta（卷为空，没东西可 delta）
3. **`--target-action=promote`**：直接退出 recovery 进入正常服务。其他选项：`pause`（停在 recovery 等手动 promote）、`shutdown`（达到 target 后停机）。**生产误删恢复推荐 `pause`**：先验证数据再人工 promote，防止"恢复完了发现 target 选错"
4. **timeline 切换的副作用**：每次 promote 都创建新 timeline，旧 timeline 的 WAL 仍保留可恢复；这意味着可以**多次 PITR 尝试不同 target** 而不破坏 repo
5. **target 时间选择**：`now() - interval '10 seconds'` 这类需要业务方提供"灾难发生时间"才能反推 target；issue 验收条款 C "误删前 10 秒" → SOP 里必须教运维**先看 audit_log 找 DROP/DELETE 的时间戳**再设 target

### PRD 落地（误删恢复 runbook 原型）

```
1. 立即冻结所有对受影响库的写入（pg_hba.conf 临时改 host all all 0.0.0.0/0 reject + reload）
2. 从 audit_log 或 PG log 找到误操作的 timestamp T_disaster
3. target = T_disaster - INTERVAL '10 seconds'
4. 在 standby/spare 机器上跑 pgbackrest restore --target-action=pause
5. PG 起来后，pg_is_in_recovery() = true（等待 promote）
6. 连进去 SELECT 验证: 误删的表回来了吗? 没多余的诡异数据吗?
7. 验证 OK → pg_wal_replay_resume() 或重启时改 --target-action=promote
8. 切流: 把应用连接串切到 spare → 旧主库下线归档以备 forensics
```

---

## Q8：空白机器恢复边界

### 实验

```
docker compose down (彻底销毁容器)
docker volume rm <pgdata-volume>
docker volume create <pgdata-volume>  ← 真·空白
docker run --rm <restore command>     ← 无 --delta
docker run -d <PG with same image>
```

### 观察

- restore 1.7s（写入全部 1269 个文件到空白卷）
- PG 启动后**自动进入 recovery 重放 WAL**，archive-get 从 repo 拉缺失段 `00000002.history`
- 8s 内 ready to accept connections
- 全部 3504 行数据完整（最新 PITR promote 后的 timeline 2 末端）

### 备份内容清单（亲自验证）

| 文件 | 在备份里？ | 备注 |
|---|---|---|
| `postgresql.conf` | ✅ | 含所有 GUC 设置 |
| `postgresql.auto.conf` | ✅ | ALTER SYSTEM 设置 |
| `pg_hba.conf` | ✅ | 客户端认证规则 |
| `pg_ident.conf` | ✅ | |
| `base/`（用户数据）| ✅ | |
| `global/`（roles, tablespaces 等）| ✅ | 用户/角色都恢复 |
| `pg_logical/`（replication slots）| ✅（结构）| slot 内容是否有用要看 PG 版本 |
| **`docker-entrypoint-initdb.d/` 的脚本** | ❌ | 这是 docker 镜像层的东西,**不属于 PGDATA** |
| **OS 级配置**（systemd unit, pgbouncer 配置等）| ❌ | 备份范围外 |
| **PG 版本/OS/locale** | 隐式约束 | 恢复目标库必须 PG 大版本完全一致（16→16），locale 必须能装得上 |

### 教训 → PRD 决策

1. **PG 配置全在备份里** → 不需要额外备份 `postgresql.conf` 等
2. **OS 级东西要单独备份**：pgbouncer 配置 / systemd unit / Nginx 反代 / pm2 ecosystem 等 → 走 secrets/configs 备份维度（Phase 1 范围内）
3. **PG 版本必须严格一致**：恢复机的 PG major version（16）和 OS 必须能装相同 patch level。**演练第一步是校验 `SELECT version()`** before restore
4. **locale**：备份里有 `pg_control` 记录的 collation，恢复机必须能 init 出相同 locale（实测 `en_US.UTF-8` 这种通用 locale 不会出问题；如果用了 `zh_CN.UTF-8` 必须先 `locale-gen`）
5. **空白机器恢复的"准备清单"**：装 PG 16 binary + 装 pgbackrest + 写 pgbackrest.conf（指向 repo）+ 创建 postgres user + 创建空白 PGDATA 目录 + 跑 restore + 启 PG → 大约**手工 10 分钟，脚本化后 1-2 分钟**

### PRD 落地

`docs/ops/backup-and-dr/02-drill-matrix.md` 必须包含：

- "新机器恢复演练" 必须从 OS image 起，不能用已装好 PG 的机器作弊
- 演练前必须执行: `dpkg -l postgresql-16` + `pgbackrest version` 验证 binary 已装到指定版本
- 演练成功标准：除了数据完整，还要验证 `SELECT current_setting('TimeZone')`、`pg_hba.conf` 中的认证规则正常工作

---

## Q2：archive_mode 改变是否需要重启 PG（理论 + 部分实测）

### 实测（顺手观察）

- 沙箱用 `command: postgres -c archive_mode=on -c archive_command=...` 启动，所以 archive_mode 在 init 时就为 on
- 没正面跑"已有运行库事后启用 archive_mode"，但 PG 文档明确说 `archive_mode` is `postmaster` 级 GUC，**改值需要 restart**（不是 reload）

### PRD 决策

- **生产启用 PITR 必须安排重启窗口**（典型 30s-2min）→ 走 deploy-ops + 应用层 graceful drain
- **`archive_command` 本身是 sighup 级 GUC**，可以 reload 不重启（修脚本路径等改动免重启）
- stanza-create 顺序：**先在 PG 配 archive_mode=on + archive_command + 重启 PG → 再跑 `pgbackrest stanza-create` → 跑 `pgbackrest backup --type=full`**。沙箱实测顺序 OK；如果 stanza-create 在 archive_command 配好之前跑就会报 `archive.info missing`（实测见 baseline 启动日志）

---

## Q4 / Q5 / Q6 / Q7（未实测，推迟到实施阶段）

| Q | 推迟理由 | 实施阶段补充方式 |
|---|---|---|
| **Q4 repo 拓扑（本地 / 枢纽 / COS）** | 本沙箱已验证"本地 repo"形式可用；其他两种属于实施阶段的部署变体 | UAT Phase 2 阶段顺路验证 |
| **Q5 14 天保留存储占用** | 沙箱数据集 29.5MB，乘 14 天没意义；要拿实际生产 DB dump 大小做线性估算 | 估算公式：`(全量大小) × 2 + (WAL 量/天 × 14)`，生产 DB dump 大小走 `db-backup` skill 拉一份看 |
| **Q6 LUKS 性能** | LUKS 透明加密 in-kernel，pgbackrest 看不到差别；普遍 benchmark 显示开销 < 10% | UAT 实施时跑一次 baseline vs LUKS 对比 |
| **Q7 备份损坏检测** | pgbackrest 内置 `verify` 命令 + 每文件 sha256 manifest；属于已知设计，不需要实测验证 | 演练 SOP 强制 `pgbackrest verify` 作为前置检查 |

---

## 沙箱清理

```bash
cd /tmp/pgbackrest-sandbox
docker rm -f pgbr-restored 2>/dev/null
docker compose down -v   # 删卷
rm -rf /tmp/pgbackrest-sandbox
```

---

## 直接进 PRD 的 5 条决策

1. **架构**：pgbackrest，本地 repo + 枢纽 pull（沿用 `10-backup-strategy.md` §3 模型，新增 `pgbackrest` 子命令分发）
2. **恢复模式**：误删用 `--target-action=pause` 验证后人工 promote；空白机灾难恢复用 `--target-action=promote` 直接服务
3. **监控指标**：`pg_stat_archiver.failed_count` 增量 + `pg_wal/` 大小阈值（必须，否则静默失败拖死）
4. **配置备份范围**：PG 自身配置 ✅ 已含；额外需备份 = pgbouncer / systemd / Nginx / pm2 / certs / `.env`（secrets 维度，Phase 1 范围）
5. **演练验收**：起空白 VM → 装 PG 16 + pgbackrest → restore → 验证 `SELECT version()` + 数据 + pg_hba 认证 + 应用连得上。沙箱实测 10s 完成（29.5MB），生产规模留 1-2h buffer 在 4h 内充裕。
