Compare commits

..

1 Commits

Author SHA1 Message Date
guke b06d8716fa docs: 线上 PG 数据库备份/恢复方案设计
覆盖每日定时备份、平台手动备份、平台指定备份恢复到旁库、平台不可用时 SSH 交互式恢复四条路径;逻辑备份(pg_dump)方案,本地存储 + S3 预留升级路径。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 16:11:20 +08:00
@@ -0,0 +1,304 @@
# 线上 PostgreSQL 数据库备份 / 恢复方案 设计
- 日期:2026-08-03
- 状态:设计已评审,待写实现计划
- 相关代码(复用的现成范式):
- [deploy/daily-exchange.service](../../../deploy/daily-exchange.service) / [deploy/daily-exchange.timer](../../../deploy/daily-exchange.timer)systemd oneshot + timer 定时任务范式:`TZ=Asia/Shanghai``Persistent=true` 补跑、`ProtectSystem=strict` 加固、文件锁)
- [app/core/heartbeat_monitor_worker.py](../../../app/core/heartbeat_monitor_worker.py)(单实例文件锁 + 优雅退出范式)
- [app/integrations/notifier.py](../../../app/integrations/notifier.py)(可插拔通知器:`LogNotifier` 占位、协议不变后续替换)
- [app/admin/deps.py](../../../app/admin/deps.py)`require_role("super_admin")` 权限守卫、审计 IP
- [app/admin/routers/withdraw.py](../../../app/admin/routers/withdraw.py)admin router 风格范式)
- [scripts/init_postgres.py](../../../scripts/init_postgres.py)PG 运维脚本范式)
## 1. 背景与目标
线上业务库 `shaguabijia`PostgreSQL 16,当前数据量 1–10GB)目前没有系统化的备份/恢复方案。本方案提供覆盖「定时 + 手动 + 平台恢复 + 平台不可用兜底」四条路径的逻辑备份体系。
四条明确需求:
1. **每日定时备份** —— 无人值守,不依赖平台进程。
2. **平台手动备份** —— admin 后台一键触发。
3. **平台指定备份文件快速恢复** —— 在 admin 后台选一份备份,恢复到**旁库**供核对。
4. **平台不可用时手动恢复** —— 登录服务器,交互式选择备份恢复,与平台走**同一套脚本**。
数据规模 110GB → **逻辑备份(`pg_dump`)足够**,不引入物理备份 / PITR 的复杂度(见 §2 预留路径)。
## 2. 非目标(本期不做 / 预留升级路径)
- **物理备份 + PITR**`pg_basebackup` + WAL 归档):数据量涨到「凌晨全量 dump 也影响业务」时再上。§3 的架构不阻碍后续叠加。
- **异地对象存储(S3 / 阿里云 OSS)**:本期只落**本地磁盘**。备份脚本预留 `upload_to_remote()` 可插拔 hook`BACKUP_S3_ENABLED=false` 时直接返回),等 S3 服务确认后填充,主流程不改。
- **从只读副本 dump**:搭流复制 standby、改从副本备份以对主库零影响,属后续升级;那个副本还能顺带承载 PITR。
- **平台「一键覆盖生产」按钮**:刻意不做。扶正生产(旁库→主库)永远是人工 SSH 执行的高危脚本(§6)。
- **平台下载备份文件**:整库备份含手机号 / 微信 / 提现等敏感数据,经浏览器下载放大泄露面且文件大。取文件走 SSH。
- **真实推送告警渠道**:项目当前无真实推送能力(`notifier.py``LogNotifier` 占位、心跳 worker 也只打印)。本期告警走「日志 + openobserve + 平台新鲜度徽标」,推送做占位 hook(§8)。
## 3. 核心设计原则与整体架构
### 3.1 两条铁律
1. **执行逻辑沉到自包含脚本,平台与定时器都只是调用者。** —— 需求 4(平台不可用时手动恢复)因此不是另写一套,而是复用同一套脚本,逻辑不重复、兜底路径永远可用。
2. **备份清单的权威来源是文件系统(备份目录 + 每目录内的 `manifest.json`),不是数据库任务表。** —— 任务表只记录「平台发起的操作过程」用于展示进度与审计;删掉任务表不影响任何一份备份的可用性与可恢复性。最需要恢复的时刻(DB 崩了)恰恰是任务表也读不到的时刻,而恢复根本不读任务表。
### 3.2 分层架构
```
┌─────────────────────────────────────────┐
│ 核心执行层(自包含,不依赖平台/DB 任务表) │
│ scripts/db_backup.sh ← 生成备份 │
│ scripts/db_restore.sh ← 从备份恢复到旁库 │
│ scripts/db_promote.sh ← 旁库扶正生产(高危)│
└─────────────────────────────────────────┘
▲ ▲ ▲
┌─────────────┘ │ └──────────────┐
① 每日定时 ② 平台手动备份/恢复 ④ 平台挂了,人 SSH
systemd timer admin API → setsid 起脚本 跑 db_restore.sh
(不经过平台) (任务表记状态,web 不阻塞) (交互式选备份)
```
### 3.3 平台执行方式:方案 B(轻量版)
平台触发的「几分钟重操作」不放进 web 请求,采用**任务表解耦 + 独立进程执行**:
| | 采用 | 说明 |
|---|---|---|
| 谁拉起脚本 | admin API 插任务后 `setsid` 起一个**脱离 web 进程组**的 subprocess 跑脚本 | 不新增常驻 worker 服务,最贴合项目「脚本 + systemd」风格;web 重启不影响已起的脚本 |
| 并发控制 | `db_backup.sh``flock` 文件锁天然串行 | 防定时与手动同时跑、上一轮未完下一轮又起 |
| 升级路径 | 任务量大、需排队/限流/重试时,升级为常驻 worker 轮询任务表 | 本期低频操作用不上 |
**被否决的方案 A**admin API 里同步 `subprocess` 跑 dump/restore,请求一直挂着。否决原因:几分钟操作阻塞 web 进程、易请求超时;admin 进程需持有 DB 高权限;平台一挂手动备份路径也没了(解耦度反而更差)。
## 4. 备份设计
### 4.1 备份产物:一个自描述目录
```
$BACKUP_DIR/20260803_030000_daily/
├── shaguabijia.dump # pg_dump -Fc(自定义格式,内建压缩)
├── globals.sql # pg_dumpall --globals-only --no-role-passwords
├── manifest.json # 元信息(见下)
└── SHA256SUMS # 上述两个文件的校验和
```
目录名(= `backup_id`)格式:`YYYYMMDD_HHMMSS_<reason>`(北京时 `Asia/Shanghai`,纯数字 + 下划线),如 `20260803_030000_daily``reason ∈ {daily, manual}`。刻意不用连字符/冒号/字母,使其**同时是合法文件名与合法 PG 库名**——旁库名由它直接拼接(见 §5.1)。
`manifest.json` 字段:
| 字段 | 说明 |
|---|---|
| `backup_id` | = 目录名,全局唯一标识 |
| `created_at` | ISO8601 北京时 |
| `reason` | `daily` / `manual` |
| `triggered_by` | `systemd-timer` / admin 用户名 / `ssh-manual` |
| `pg_version` | 备份时 PG 版本 |
| `database` | `shaguabijia` |
| `format` | `custom` |
| `alembic_version` | 备份时的 schema 版本(恢复时对齐迁移用;旁库 `alembic_version` 表亦可查) |
| `files` | 每个文件的 `name` / `size_bytes` / `sha256` |
| `dump_duration_sec` | 导出耗时 |
| `status` | `success` / `failed`**仅自检通过才写 success** |
### 4.2 `db_backup.sh` 关键行为
| 环节 | 做法 | 理由 |
|---|---|---|
| 导出业务库 | `pg_dump -Fc``shaguabijia.dump` | 单文件、压缩,`pg_restore` 支持并行(`-j`)与按表选择性恢复 |
| 导出全局角色 | `pg_dumpall --globals-only --no-role-passwords``globals.sql` | 恢复到新机器时角色/权限齐全;`--no-role-passwords` 免超级用户读 `pg_authid`、旁库核对也不需要密码 |
| 资源降级 | `nice -n 19 ionice -c3` 包裹 pg_dump | 让业务优先,压制备份对 CPU/IO 的抢占 |
| **备份后自检** | dump 完立刻 `pg_restore --list *.dump >/dev/null` 解析 TOC + 校验 sha256 | 不验证的备份是薛定谔的备份;能抓到文件截断/损坏,只有通过才写 `status=success` |
| 并发锁 | `flock` 独占锁(仿 daily-exchange 30min 锁) | 串行化,防重入 |
| 保留清理 | **仅自检通过后**执行(§4.4) | 保证先有新备份、再删旧的 |
| 远程上传 | 末尾 `upload_to_remote()``BACKUP_S3_ENABLED=false` 时 return 0 | S3 可插拔扩展点 |
| 通知 | 末尾 `notify()`(§8),成功/失败都打结构化日志 | 贴合现状、可插拔 |
| 退出码 | 失败非 0 退出 + `SyslogIdentifier=pg-backup` | systemd 与 openobserve 可感知 |
| 触发标签 | `--reason daily|manual`,写进 manifest | 定时与手动共用一个脚本 |
### 4.3 对线上读写的影响与缓解
`pg_dump` 基于 MVCC 一致性快照,**不阻塞正常增删改查**,导出的是某一时刻的一致性视图。要点:
- **唯一会互斥的例外是 DDL**:dump 持 `ACCESS SHARE` 锁,与 `ALTER TABLE` / `DROP` / `TRUNCATE` / `VACUUM FULL` / 非并发建索引冲突。规避:alembic 迁移仅在手动部署时跑,与凌晨备份天然错开。
- **真正的影响是资源争抢**(1–10GB 下的关注点):磁盘 I/O(整库顺序读)、CPU(zlib 压缩)、缓存冲刷(热数据被挤出 shared_buffers,备份后短时命中率下降)、长事务期间 VACUUM 暂时回收不了死元组。
- **缓解**:定时放凌晨低谷(03:00,与 0 点 daily-exchange 错开)+ `nice`/`ionice` 降级 + 控制 dump 时长在几分钟内。
### 4.4 保留策略
清理只在**备份成功且自检通过后**执行,且**只删本地、不碰 S3**(S3 用自身生命周期策略):
- `BACKUP_KEEP_DAILY_DAYS=14` —— 保留最近 14 天每日备份。
- `BACKUP_KEEP_MONTHLY_COUNT=6` —— 更早的备份中,每月 1 号那份额外保留 6 个月(防「问题两周前就埋下」)。
- `BACKUP_MIN_KEEP=3` —— **硬底线:无论配置如何,永远至少保留最近 3 份**,防时间跳变/配置错误把备份删光。
- 清理前检查磁盘剩余空间,不足则打告警日志而非静默继续。
## 5. 恢复设计(恢复到旁库)
**铁律:平台与默认脚本永远只恢复到旁库,绝不自动碰生产。**
### 5.1 `db_restore.sh` —— 恢复到旁库(常规路径)
输入一个 `backup_id`,产出可供核对的旁库 `shaguabijia_restore_<backup_id>`(如 `shaguabijia_restore_20260803_030000_daily`):
| 步 | 动作 | 护栏 |
|---|---|---|
| 1 | **恢复前先验完整性**:校验 `SHA256SUMS` + `pg_restore --list` 确认可解析 | 不把损坏备份恢复到一半才发现 |
| 2 | 创建旁库 `shaguabijia_restore_<backup_id>`;已存在则提示换名或显式 `--force` 重建 | 不撞库、不误删 |
| 3 | 灌 `globals.sql`(角色已存在则跳过,幂等) | |
| 4 | `pg_restore -j <并行度,默认 2>` 到旁库(custom 格式自动识别,无需 `-Fc`),`nice`/`ionice` 降级 | 往旁库写,**不锁生产表**,对生产仅轻微资源争抢 |
| 5 | **恢复后自检报告**:表数量、关键业务表(user / wallet / withdraw 等)行数、`alembic_version`、最新记录时间戳 | 人凭报告判断「数据对不对、新不新」 |
**磁盘峰值提醒**:旁库与生产库共存于同一实例,恢复期间磁盘占用约为 2×(生产 + 旁库),恢复前脚本检查磁盘余量。
### 5.2 兜底:交互式选择(需求 4)
`db_restore.sh` **不带参数**运行时 → 扫描 `$BACKUP_DIR`,列表打印所有备份(`backup_id` / 时间 / 大小 / `status`),提示输入序号选一个恢复到旁库。这就是「平台挂了 SSH 登录手动选备份恢复」——与平台调用的是同一个脚本,零额外学习成本。
### 5.3 深度核对的边界
平台展示 §5.1 第 5 步的自检报告即可支撑「数据对不对」的判断。**逐行深度核对不在本方案范围**——需要时 DBA 直接连旁库跑只读 SQL,旁库为此存在。
## 6. 扶正生产:`db_promote.sh`(高危、人工、不进平台)
核对无误后把旁库扶正为生产。**故意不做成平台按钮**,必须人工 SSH 执行,因为不可逆且涉及短暂停机:
1. **二次确认**:要求手输生产库名 `shaguabijia` 才继续(仿 GitHub 删仓库确认)。
2. **先自动备份当前生产库**(调 `db_backup.sh --reason manual`)—— 最关键的兜底,扶错了能退回来。
3. **断开生产连接**:停 app 服务或 `pg_terminate_backend` 清连接(执行者自身不能连在待改名的库上)。**此步有短暂停机。**
4. **rename 切换**(不用 drop):`shaguabijia``shaguabijia_old_<ts>``shaguabijia_restore_xxx``shaguabijia`。出错可立即换回,比删库重建安全得多。
5. 重启 app 服务,人工验证。
**为何用脚本而非裸敲 SQL**:切换不是「只有 rename 一句」,而是一串不能漏的动作,其中「先备份当前生产库」裸敲最易漏、漏了就没退路。脚本把这串封装成带护栏的原子操作。极端情况下连脚本都不可用时,rename 本质是两条 `ALTER DATABASE` SQL,DBA 亦可手工执行,但会失去自动兜底备份保护,属下策。
## 7. 平台 API + 数据模型 + 权限
### 7.1 数据模型(两张任务表,均新建 + alembic 迁移)
`db_backup_job`
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | PK | |
| `triggered_by_admin_id` | FK admin_user, nullable | 手动才有 |
| `status` | str | `pending` / `running` / `success` / `failed` |
| `backup_id` | str, nullable | 成功后填产出目录名 |
| `error_msg` | text, nullable | |
| `created_at` / `started_at` / `finished_at` | datetime | 后两者 nullable |
| `duration_sec` | int, nullable | |
`db_restore_job`
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | PK | |
| `backup_id` | str | 恢复哪个备份 |
| `target_db` | str | 旁库名 |
| `triggered_by_admin_id` | FK admin_user | |
| `status` | str | `pending` / `running` / `success` / `failed` |
| `sanity_report` | json, nullable | §5.1 第 5 步的自检报告 |
| `error_msg` | text, nullable | |
| `created_at` / `started_at` / `finished_at` | datetime | 后两者 nullable |
模型需在 [app/models/__init__.py](../../../app/models/__init__.py) 导入以便 Alembic 发现。
**备份清单不查任务表**:定时备份不写任务表(不经过平台),但其产出的备份**出现在备份列表**(列表来自扫盘)。平台因此有两个视图:备份文件列表(扫盘,含所有来源)、操作记录(任务表 + 审计)。
### 7.2 API 端点(新建 `app/admin/routers/db_backup.py`,注册进 `app/admin/main.py`
| 方法 | 路径 | 作用 | 返回 |
|---|---|---|---|
| GET | `/admin/db-backup/backups` | 列所有备份(扫盘读 manifest) | `list[BackupItem]` |
| GET | `/admin/db-backup/health` | 新鲜度:最近成功备份时间与年龄 | `{last_success_at, age_hours, healthy}` |
| POST | `/admin/db-backup/backups` | 手动触发备份 | `202 {job_id}` |
| GET | `/admin/db-backup/jobs/{id}` | 轮询备份任务状态 | `BackupJobStatus` |
| POST | `/admin/db-backup/restores` | 恢复 `backup_id` 到旁库(body: `{backup_id}` | `202 {restore_job_id}` |
| GET | `/admin/db-backup/restores/{id}` | 轮询恢复状态 + 自检报告 | `RestoreJobStatus` |
`BackupItem``backup_id / created_at / reason / triggered_by / size_bytes / database / pg_version / alembic_version / status`
契约放 `app/schemas/db_backup.py`Pydantic)。POST 端点为**异步**语义:插任务表 → `setsid` 起脚本 → 立即 `202` 返回 `job_id`,前端轮询对应 GET 端点。
### 7.3 权限与审计
- **权限**:备份与恢复端点**均限 `super_admin`**(复用 `require_role("super_admin")`)。恢复能触及全库数据,不宜下放;将来给运维岗再引入 `require_page("db_backup")` 细分。
- **审计**:每次备份 / 恢复写现有审计日志(触发人、`backup_id`、结果),与项目其它高危操作一致。
- **不提供下载**(见 §2)。
## 8. 监控告警(贴合「日志 + openobserve + 平台徽标」现状)
三层,从被动到主动:
1. **结构化日志**:脚本成功/失败均打 `SyslogIdentifier=pg-backup` 日志,`journalctl` 可查、openobserve 可抓,失败为 ERROR 级 → 若已配 openobserve 告警规则即命中。
2. **平台新鲜度徽标**(最直观、不依赖推送):admin 备份页顶部显示「最近成功备份:X 小时前」,超 `BACKUP_FRESH_MAX_HOURS`(默认 26h)红色高亮。数据源为 `GET /admin/db-backup/health`
3. **可插拔 `notify()` hook**:脚本内通知点,现在只打日志(照搬 `notifier.py``LogNotifier` 模式),将来推送能力(飞书/短信)就绪再填,主流程不改。
**新鲜度自检兜底**`deploy/pg-backup-check.timer`+service)每天 09:00 检查最新成功备份是否在 26h 内,过期打 ERROR 日志 + 调 `notify()`。这覆盖 `OnFailure` 抓不到的盲区(timer 被禁 / 宕机没补跑)——因为那种情况脚本根本没运行,靠「有没有新备份」反向判断。
## 9. 脚本连库身份与权限
备份/恢复脚本**以本机 `postgres` 超级用户走 Unix socket(peer 认证,无需密码)**执行,而非业务的 `DATABASE_URL`TCP + 密码)。理由:
- `pg_dumpall --globals-only``CREATE DATABASE`(建旁库)、`ALTER DATABASE ... RENAME``pg_terminate_backend` 都需高权限;
- 本机 socket peer 认证是运维脚本标准做法,权限最省心,且不用把超级用户密码写进任何配置文件。
脚本以 `root``postgres` 系统用户运行(systemd service 内 `User=postgres` 或经 `sudo -u postgres`)。SSH 手动执行时同理。
## 10. 配置项
新增到 [app/core/config.py](../../../app/core/config.py) 的 `Settings`(供平台侧 §8 新鲜度等读取),shell 脚本经 systemd `EnvironmentFile=.env` 注入、SSH 手动跑时脚本内有默认值兜底:
| 配置 | 默认 | 说明 |
|---|---|---|
| `BACKUP_DIR` | `/opt/pg_backups` | 备份根目录(建议独立数据盘,与 PG 数据文件不同物理盘) |
| `BACKUP_KEEP_DAILY_DAYS` | `14` | 每日备份保留天数 |
| `BACKUP_KEEP_MONTHLY_COUNT` | `6` | 月度长留份数 |
| `BACKUP_MIN_KEEP` | `3` | 硬底线,永远至少保留份数 |
| `BACKUP_FRESH_MAX_HOURS` | `26` | 新鲜度阈值 |
| `BACKUP_PG_SUPERUSER` | `postgres` | 脚本连库超级用户 |
| `BACKUP_PGHOST` | `/var/run/postgresql` | socket 目录(peer 认证) |
| `BACKUP_S3_ENABLED` | `false` | 远程上传总开关(预留) |
| `BACKUP_S3_*` | 空 | bucket / endpoint / 凭证(预留,S3 确认后填) |
同步更新 `.env.example`
## 11. 完整文件清单
### 本仓(后端 + 脚本)
**新增:**
| 文件 | 作用 |
|---|---|
| `scripts/db_backup.sh` | 备份核心(dump + 自检 + 保留清理 + upload hook + notify |
| `scripts/db_restore.sh` | 恢复到旁库(含无参交互式选择 = 需求 4) |
| `scripts/db_promote.sh` | 旁库扶正生产(高危、人工、二次确认) |
| `app/models/db_backup_job.py` | `db_backup_job` + `db_restore_job` 模型 |
| `app/admin/repositories/db_backup_job.py` | 任务表数据访问 + 扫盘读 manifest |
| `app/admin/routers/db_backup.py` | 平台 API6 个端点) |
| `app/schemas/db_backup.py` | API 契约 |
| `alembic/versions/xxxx_add_db_backup_jobs.py` | 建两张任务表 |
| `deploy/pg-backup.service` / `pg-backup.timer` | 每日定时备份 |
| `deploy/pg-backup-check.service` / `pg-backup-check.timer` | 新鲜度自检 |
| `deploy/pg-backup.md` | 部署文档(仿 [deploy/daily-exchange.md](../../../deploy/daily-exchange.md) |
**改动:** `app/models/__init__.py`(导模型)、`app/core/config.py`(§10 配置项)、`app/admin/main.py`(注册 router)、`.env.example`
### 另一仓(`shaguabijia-admin-web`)配套
备份管理前端页:备份列表 + 一键备份 + 选备份恢复到旁库 + 恢复进度/自检报告展示 + 新鲜度徽标。本 spec 定义 API 契约(§7.2),前端据此实现,单独走该仓的开发流程。
## 12. 需求覆盖对照
| 需求 | 落地 |
|---|---|
| 每日定时备份 | `pg-backup.timer``db_backup.sh --reason daily`,凌晨 3 点,不依赖平台 |
| 平台手动备份 | `POST /admin/db-backup/backups` → 插任务 + `setsid``db_backup.sh --reason manual` |
| 平台指定备份快速恢复 | `POST /admin/db-backup/restores``db_restore.sh` 恢复到旁库 + 自检报告 |
| 平台不可用手动恢复 | SSH 跑 `db_restore.sh`(交互式选备份),同一套脚本 |
| (延伸)扶正生产 | 人工 `db_promote.sh`,二次确认 + 先备份当前 + rename 切换 |
## 13. 部署时需确认的运维参数
以下取决于服务器实际情况,部署或本 spec 复审时确认,不阻碍设计:
1. **备份目录位置**:默认 `/opt/pg_backups`;若有独立数据盘,建议放数据盘且与 PG 数据文件不同物理盘(避免一盘挂掉数据与备份同亡)。
2. **保留量匹配磁盘余量**:1–10GB 压缩后每份约几百 MB~2GB,14 天约 3–30GB;核对与磁盘余量是否匹配。
3. **备份时间**:默认凌晨 03:00(与 0 点 daily-exchange 错开);确认无其它凌晨任务撞车。
4. **脚本运行用户**:确认以 `postgres`(或可 `sudo -u postgres`)运行、socket peer 认证可用。