# 备份与灾备说明

> 初稿（2026-11-05）。适用于私有化部署（单机 Docker Compose 与 Kubernetes）。部署形态见[《产品架构与部署白皮书》](/trust/architecture)；加密与访问控制见[《安全白皮书》](/trust/security)。

## 1. 数据在哪里

| 数据 | 内容 | 存放位置 |
|---|---|---|
| PostgreSQL 集群 | 平台业务库（租户、业务数据、草稿等）、审计库、身份（Zitadel）的库、工作流（Temporal）与各工具的库 | Postgres 数据卷 |
| OpenBao | 敏感字段的数据密钥、租户凭据（自带存储、自带模型的密钥）、派生密钥的根 | OpenBao 数据卷（文件存储） |
| 平台文件 | 上传的文件、头像、录音、审计归档分段 | `single`：kwork 数据卷；`onprem`：自带 SeaweedFS；云 Pack：云厂商的桶 |
| 配置与密钥 | 安装时生成的全部密钥（含备份加密密钥）、导出的网关根证书 | 主机上的部署配置目录 |
| 许可证 | 许可证文件与厂商公钥 | 主机上的部署配置目录 |
| 租户自带存储 | 租户自己配置的 S3 / OSS / COS / MinIO 桶里的文件 | 租户自己的桶 |

## 2. 备份方式与频率

| 数据 | 方式 | 频率 | 怎么打开 |
|---|---|---|---|
| PostgreSQL 整个集群 | WAL-G：经复制协议持续接收 WAL，加密后上传到 S3 兼容存储；每天一次全量 | WAL 持续（每 60 秒强制切换一次 WAL 段）；全量每天 02:30 UTC，默认保留最近 7 个 | 安装时加 `backup`；默认不打开 |
| OpenBao 数据卷、kwork 数据卷、配置与密钥 | 状态包：一次打成一个加密归档，可直接上传到 S3 兼容存储 | 每次执行一个；按每日执行配置定时任务（由你配置） | 随平台提供，需你安排定时执行 |
| 许可证 | 不在任何备份里 | 换文件时 | 厂商给的原件另存 |

- 数据库备份不改动、不重启 Postgres 服务，只通过网络读取。
- 备份服务有健康检查：WAL 流在运行、上一次全量不超过 26 小时。不满足时显示为不健康。
- 升级脚本在打开备份时，升级前先做一次全量备份和一次状态包。
- 不打开数据库备份的安装，至少每天自行做一次数据库导出。
- 以上备份服务与状态包只用于单机 Compose 形态。Kubernetes 形态的 PostgreSQL、对象存储与 OpenBao 由你提供，按所用服务的方式备份（例如托管数据库自带的时间点恢复）。

### 2.1 备份送到哪里

- `onprem` 不另行配置时，数据库备份送到自带的 SeaweedFS。**它还在同一台主机上**：主机或磁盘整体损坏时备份也会丢失。生产环境请指向另一个机房或云上的存储。巡检（`kwork doctor` 的 `backup_offsite`）在备份送到同一台主机时给出提醒。
- `single` 与云 Pack 必须先配置备份存储，否则安装拒绝打开备份。
- 建议：备份存储的凭据只授权这一个桶；桶开启版本控制，可能时开启对象锁定，防止主机被入侵后备份被删除。

## 3. 加密与密钥保管

| 对象 | 加密 | 密钥 | 谁保管 |
|---|---|---|---|
| 数据库全量与 WAL | 在主机上用 libsodium（secretbox）加密后才上传，存储方看不到内容 | 备份加密密钥（32 字节，安装打开备份时生成） | 你：生成后立即离线另存，至少两份 |
| 状态包 | AES-256-CBC，PBKDF2 派生（200,000 轮） | 同一把备份加密密钥 | 同上 |
| OpenBao 数据 | OpenBao 自身封存 | 5 份解封密钥，任意 3 份可解封 | 你指定的不同保管人，各自离线保存 |

- **丢了备份加密密钥，所有数据库备份和状态包都无法打开**，没有任何恢复办法。不要只放在这台主机上。
- 更换备份加密密钥只对之后的备份生效；旧备份仍需旧密钥，旧全量过了保留期之后才能丢弃旧密钥。
- 解封密钥在初始化时写入主机上的一个文件。**分发给保管人后要从主机删除该文件**：否则它会进入状态包，解封份额、OpenBao 数据和加密密钥会出现在同一个包里，3/5 分持失去意义。OpenBao 的 root token 同样离线保存。巡检（`kwork doctor` 的 `unseal_file`）只看这个文件在不在、放了多久，不读内容：在就提醒，超过 7 天报异常。
- OpenBao 每次重启后都处于封存状态，需要 5 位保管人中任意 3 位输入各自的份额解封；封存期间平台的就绪检查失败。目前没有自动解封。
- 数据库备份角色可以读到整个集群的数据文件，其密码按超级用户密码的级别保管。

## 4. RPO 与 RTO

RPO 是可能丢失的数据时间窗口；RTO 是恢复所需时间。

| 项 | 值 | 类型 |
|---|---|---|
| RPO | ≤ 5 分钟 | 目标 |
| RTO | ≤ 1 小时（SaaS 的目标；私有化部署的 RTO 取决于你的备份存储、数据量与演练结果） | 目标 |
| 丢数据窗口（设计上限） | 约 60 秒加上传时间（由 WAL 段切换间隔决定） | 配置值 |
| 丢数据窗口：源库被强制杀掉并删除数据卷后恢复 | 33 秒 | 实测 |
| 丢数据窗口：恢复后的第二条时间线 | 55 秒 | 实测 |
| 丢数据窗口：对运行中的完整平台数据库做备份并恢复 | 42 秒 | 实测 |
| 数据库恢复耗时：329 MB 集群，最新全量 + 全部 WAL，冷启动 | 35.4 秒 | 实测 |
| 数据库恢复耗时：时间点恢复 | 17.4 秒 | 实测 |
| 数据库恢复耗时：514 MB 的完整平台数据库 | 13.0 秒 | 实测 |

实测条件（2026-10-06）：开发机上的 Docker 虚拟机（aarch64，4 核，与其他任务共用，磁盘 I/O 慢），存储为同网络的 SeaweedFS。恢复耗时只是数据库部分，不含新主机准备、状态包解开、OpenBao 解封和服务启动。按这次的速度粗算，取全量约 17 MB/s。

尚未实测的：

- 几十 GB 到 TB 级数据的全量与回放耗时；
- 跨机房、跨地域的备份存储，以及云厂商的对象存储（AWS S3、阿里云 OSS、腾讯云 COS）；
- 把恢复出的数据库换回正在运行的平台，以及整机重建的端到端耗时；
- 7 天保留期的自然轮转；复制槽超过 8 GB 被放弃后的追赶。

## 5. 恢复演练

数据库恢复演练不碰正在运行的平台：

1. 演练脚本在一个一次性的容器和数据卷里取出全量、从备份存储取 WAL 回放到末尾（或指定时间点），然后提升为主库。
2. 恢复出的数据库只开本地 socket，不监听网络、不归档，网络上的任何服务都连不到它。
3. 脚本检查业务库、审计库、身份库都在，报告租户数、迁移版本、审计事件数与最新事件时间，并与运行中的源库对比落后多少数据。
4. 打印每一段的耗时：这就是你环境里数据库部分的 RTO。
5. 结束后容器和数据卷全部删除。

状态包演练：用离线保存的那份备份加密密钥（不读主机上的配置）把最新状态包解到一个临时目录，确认 OpenBao 数据在其中。这同时证明离线保存的密钥是对的。

建议频率：上线前一次，之后每季度一次（单机 Beta 环境建议每月一次）。整机重建演练的顺序：新主机装好 Docker → 放上同一版本的部署文件与许可证 → 用离线密钥解开状态包并放回配置、密钥与 OpenBao 数据 → 恢复数据库到指定时间点 → 启动平台 → 保管人解封 OpenBao → 登录、打开一个已有文件、对每个租户校验审计链。网关的内置 CA 不在备份中，重建后会生成新的根证书，要重新分发给用户和运维人员。

## 6. 时间点恢复

- 可以把数据库恢复到保留期内的任意时间点：默认保留最近 7 个每日全量及其之后的 WAL。
- 实测：目标时间设在两批写入之间，恢复结果准确落在两批之间（第一批在、第二批不在）。
- 真实恢复时先停止所有写入数据库的服务，把旧数据移到另一个数据卷（不删除），再恢复到空目录。恢复只往空目录写。
- 恢复之后数据库进入新的时间线，要立刻做一次全量备份。
- 时间点恢复会丢掉该时间之后的全部改动，要告知用户。
- 数据库与 OpenBao 要恢复到同一时期，否则依赖派生密钥的退订链接与站点会话会失效。

## 7. 不在平台备份范围内的

| 数据 | 说明 |
|---|---|
| 租户自带存储里的文件 | 在租户自己的桶里，由租户开启版本控制或跨地域复制 |
| 云 Pack 的平台文件桶 | 在你的云账号里，由你开启版本控制或跨区复制 |
| `onprem` 自带 SeaweedFS 里的平台文件与审计归档 | 不在状态包里，需另做复制 |
| 网关证书与内置 CA | 不备份；重建后重新签发 |
| 观测数据（指标、日志、链路、Langfuse）、模型网关的本地数据 | 不备份；模型网关的供应商与密钥由平台重新同步 |
| 许可证文件与厂商公钥 | 厂商给的原件另存 |
| 主机磁盘快照 | 不由平台提供；云主机可使用云厂商的快照 |

## 8. 当前限制

- **单机部署不是高可用**：主机故障期间平台不可用，按本文 §5 的顺序在新主机上重建。
- **多可用区与异地灾备没有提供**。备份可以送到另一个地域的存储，但没有自动切换。
- **数据库主备切换**：只适用于 Kubernetes 形态，由你提供的 Postgres 负责切换，平台连跟随主库的地址。已在单节点测试集群上用 CloudNativePG（一主一备、异步复制）实测计划切换、删除主库、进程被杀、网络隔离、进程冻结五种场景，写入在约 2.5 秒到 126 秒内恢复，取决于场景。异步复制下非计划切换会丢失尚未复制到备库的已提交写入，实测中出现过“变更已生效、审计没写上”。同步复制、托管数据库（RDS、PolarDB 等）的切换、PgBouncer、多节点集群的真实节点故障都没有测。
- **OpenBao 需要人工解封**：每次主机或 OpenBao 重启后，由保管人解封。
- **审计归档的对象锁定**：自带的 SeaweedFS 不支持对象锁定，此时归档是未锁定的独立副本；要锁定，换用支持对象锁定的 S3 兼容存储。
