# 备份与恢复

Raise 通过 `--backup` 开关使用 WAL-G 对整个 PostgreSQL 数据库集群执行持续的 WAL 日志归档与每日全量备份，并在加密后上传至对象存储；通过 `backup-state.sh` 脚本备份 OpenBao 密钥卷、环境配置 `.env` 与系统状态目录 `state/`。在恢复数据时，你可以先使用 `restore-drill.sh` 脚本在一次性临时容器中进行演练，验证通过后再对实际运行中的数据库执行恢复操作。

## 谁能用

执行系统备份与恢复涉及以下权限与安全要求：

- **部署方与主机运维（主机 `root` 用户）**：具备服务器系统操作权限，通过主机命令行执行备份开启、状态备份、演练与恢复脚本。脚本运行时需要读取 `deploy/compose/.env` 配置文件（文件中必须配置 `BACKUP_ENCRYPTION_KEY` 环境变量）。
- **租户成员与管理员**：不能查看、触发或管理平台级备份。

> **危险**
>
> 丢失 `BACKUP_ENCRYPTION_KEY` 密钥将导致所有备份无法恢复。该密钥必须与 OpenBao 解封份额同等严格保管，切勿只存放在单台服务器的 `.env` 文件中。

## 怎么打开

备份与恢复属于底层基础设施运维能力，系统没有提供独立的图形化产品界面。

你可以通过以下入口与命令管理不同对象的备份：

| 保护对象 | 备份方式 | 执行频率 | 入口与管理方式 |
|---|---|---|---|
| PostgreSQL（`app`、`audit`、`zitadel` 及工具库） | `--backup`：WAL-G 流式复制 + 每日全量备份 | WAL 持续归档（约 ≤1 分钟）；全量备份每日在 `BACKUP_FULL_AT`（默认 02:30 UTC）执行 | 在安装时添加 `--backup` 参数；日常通过 `docker compose exec pg-backup kwork-backup ...` 执行管理 |
| 对象存储 | 版本化 + 跨区 / 异机复制（部署方自备） | 持续同步 | 依赖底层 [对象存储](/deploy/storage) 系统自身配置 |
| OpenBao | Raft 快照或文件卷；打包在状态包中 | 每日 / 每次变更 | 通过 `backup-state.sh` 脚本备份 |
| 环境配置与系统状态（`.env`、`compose/state/`） | `backup-state.sh` 状态备份脚本 | 每次变更 | 执行 `deploy/scripts/backup-state.sh` |

常用运维命令速查：

```bash
# 开启备份（可在执行安装脚本时与其它参数组合）
./install.sh --pack onprem --infra --backup

# 查看已有的全量备份列表
docker compose exec pg-backup kwork-backup list

# 手动触发一次全量备份
docker compose exec pg-backup kwork-backup full

# 查看 WAL 归档日志状态
docker compose exec pg-backup kwork-backup wal-g wal-show

# 校验 WAL 完整性与时间线
docker compose exec pg-backup kwork-backup wal-g wal-verify integrity timeline

# 备份系统状态包
deploy/scripts/backup-state.sh [--out DIR] [--keep 14] [--upload s3://...]

# 还原系统状态包至指定目录
deploy/scripts/backup-state.sh restore FILE --into DIR

# 执行无损恢复演练
deploy/scripts/restore-drill.sh

# 演练恢复到指定时间点
deploy/scripts/restore-drill.sh --target-time '2026-10-06 11:40:00+00'
```

## 界面一览

系统没有提供图形化备份页面。你可以通过日志输出与管理命令查看备份状态与运行健康度：

- **查看备份运行日志**：
  ```bash
  docker compose logs pg-backup
  ```
- **查看全量备份列表**：
  ```bash
  docker compose exec pg-backup kwork-backup list
  ```
- **健康检查判定标准**：
  - 容器内的 WAL 归档流进程处于正常运行状态；
  - 最近一次全量备份的完成时间距当前不超过 26 小时。

> **提示**
>
> Grafana 与 Prometheus 的监控指标不能替代这份健康检查。部署运维人员仍需定期检查备份日志与全量备份时间。

## 常用操作

### 开启 PostgreSQL 备份

1. 在执行系统 [安装](/deploy/install) 时，在安装命令中添加 `--backup` 开关：
   ```bash
   ./install.sh --pack onprem --infra --backup
   ```
   该开关可以与其它环境参数任意组合。
2. 安装脚本会自动完成以下 4 项配置（已有密钥不会被覆盖）：
   - 生成复制角色密码 `BACKUP_DB_PASSWORD` 与备份加密密钥 `BACKUP_ENCRYPTION_KEY`（64 位十六进制）。
   - 初始化复制规则：在数据库中创建专用的复制角色 `kwork_backup`（具备 `REPLICATION` 权限，对 `app` 与 `audit` 业务库无 `CONNECT` 权限）；在 `pg_hba` 中增加一行复制规则；将 `max_slot_wal_keep_size` 设为 `8GB`；执行配置重载 `pg_reload_conf()`。此过程不修改 PostgreSQL 镜像，也不需要重启数据库。
   - 基于当前的 `POSTGRES_IMAGE` 镜像构建备份容器镜像 `deploy/compose/pg-backup`（内置 WAL-G v3.0.9）。
   - 启动 `pg-backup` 容器：启动 `wal-g wal-receive` 进程持续接收 WAL 流（复制槽名为 `kwork_backup`，日志段大小为 16 MB）；每隔 `BACKUP_WAL_SWITCH_SECONDS`（默认 60 秒）调用一次 `pg_switch_wal()` 触发切段；并在每日指定时刻生成全量备份，仅保留最近 `BACKUP_RETAIN_FULL`（默认 7 个）全量备份。
3. 检查备份存储与机房灾备：
   - 默认配置下，备份文件写入套件自带的对象存储（在私有化 onprem 环境下为 SeaweedFS 的 `kwork-backup` 存储桶，前缀为 `pg/`）。
   - 默认备份文件仍存储在同一台物理主机上。如果主机磁盘损坏，备份文件会一并丢失。
   - 生产环境中，建议将 `BACKUP_S3_*` 系列变量指向另一机房或第三方云对象存储。远端存储桶需要预先创建完成并设置 `BACKUP_CREATE_BUCKET=false`，建议同时开启对象锁或多版本控制。
4. 本地加密：
   - 备份数据在上传前由 libsodium 在本机完成加密。更换加密密钥只对后续生成的新备份生效。

### 备份与还原系统状态包

OpenBao 卷、环境配置文件 `.env` 以及运行状态目录 `state/` 需要通过状态包脚本单独备份。

1. **执行状态包备份**：
   - 运行默认备份命令：
     ```bash
     deploy/scripts/backup-state.sh
     ```
     命令将在 `./state-backups/` 目录下生成加密压缩包 `kwork-state-<utc>.tar.gz.enc`。
   - 指定输出目录并保留最近 14 份备份：
     ```bash
     deploy/scripts/backup-state.sh --out /srv/bk --keep 14
     ```
   - 备份完成后直接上传至 S3 对象存储：
     ```bash
     deploy/scripts/backup-state.sh --upload s3://bucket/kwork-state/ --endpoint https://s3.example.com
     ```
   - 状态包使用 OpenSSL AES-256-CBC 配合 PBKDF2（迭代 200000 次）加密，密钥直接使用 `.env` 中的 `BACKUP_ENCRYPTION_KEY`。
2. **还原系统状态包**：
   - 执行还原命令：
     ```bash
     deploy/scripts/backup-state.sh restore FILE --into DIR
     ```
> **注意**
>
>      还原命令**不会直接覆盖当前主机的运行文件**。它只会将解密后的文件提取到指定的 `DIR` 目录中。
   - 根据终端打印的 `cp` 或 `docker run … tar` 命令提示，手动把文件复制放回原位。
   - 启动 OpenBao 容器，并使用 3/5 份额的解封密钥完成解封。

### 执行恢复演练（不影响线上业务）

恢复演练用于在不触碰生产运行环境的前提下，验证备份的可用性与恢复时间。

1. 在终端执行演练脚本：
   ```bash
   deploy/scripts/restore-drill.sh
   ```
   若要演练恢复至特定时间点，可以传入 `--target-time` 参数：
   ```bash
   deploy/scripts/restore-drill.sh --target-time '2026-10-06 11:40:00+00'
   ```
2. 脚本会在一次性临时容器与数据卷 `kbk-drill-<时间>` 中拉取全量备份、回放 WAL 日志并执行提升。演练数据库仅开放 Unix domain socket，不监听外部网络端口。
3. 脚本会自动执行以下检查：
   - 校验 `app`、`audit` 与 `zitadel` 数据库是否正常存在；
   - 校验 `platform.tenant` 表是否可正常读取；
   - 校验数据库迁移版本记录；
   - 校验 `audit.event` 表；
   - 校验 Zitadel 数据表数量。
4. 终端会打印数据库各恢复阶段的耗时（数据库部分 RTO）与最后一个事务的时间戳。若当前可以连接在线源库，脚本还会比对 WAL 落后程度以及租户和审计日志记录数。
5. 演练结束后，脚本会自动销毁临时容器与卷。如果需要保留环境排查问题，可以添加 `--keep` 参数。

演练脚本支持的常用参数：

| 参数选项 | 说明 | 默认值 |
|---|---|---|
| `--env-file` | 指定使用的环境配置文件 | `deploy/compose/.env` |
| `--pack` | 指定环境包名称 | 当前环境配置 |
| `--network` | 指定容器网络 | `kwork_default` |
| `--backup NAME` | 指定恢复的特定全量备份名称 | 最新全量备份 |
| `--target-time` | 指定时间点恢复的目标时间戳（UTC） | 最新 WAL 终点 |
| `--source-url` / `--no-source` | 比对所连接的源数据库地址，或跳过源库比对 | 连接源库 |
| `--probe SQL` | 附加自定义 SQL 探测验证语句 | 无 |
| `--superuser` | 演练数据库的超级用户名 | `kwork` |

### 执行真实灾难恢复

当数据库发生故障或需要回滚至指定时刻时，必须先停止写入，再执行真实恢复。

1. **停止业务写服务**：
   在终端停止核心应用容器，防止新数据写入：
   ```bash
   docker compose stop kwork kwork-agent zitadel pg-backup
   ```
   若开启了可选组件，还需要停止 `cube`、`superset`、`langfuse`、`temporal` 与 `collab` 容器。
> **提示**
>
>    在停止服务与执行恢复前，建议先运行 `restore-drill.sh` 演练确认全量备份与目标时间点。
2. **转移现有数据卷**：
   PostgreSQL 恢复命令要求目标数据目录必须为空。停止数据库容器并将现有数据移动到临时数据卷中，不要直接删除：
   ```bash
   docker compose stop postgres
   docker volume create kwork_pg_broken
   docker run --rm -v kwork_pg:/from -v kwork_pg_broken:/to alpine sh -c 'cp -a /from/. /to/ && find /from -mindepth 1 -delete'
   ```
3. **执行数据库恢复**：
   使用备份容器执行恢复操作，可按需指定特定全量备份 `--backup` 或恢复时间点 `--target-time`：
   ```bash
   docker compose --profile backup run --rm --no-deps -v kwork_pg:/var/lib/postgresql/data pg-backup restore --stop [--backup NAME] [--target-time '2026-10-06 11:40:00+00']
   ```
   恢复完成后，终端会输出格式为 `KBK-RESTORED <最后事务时间> | <WAL 位置> | <新时间线>` 的确认信息。
4. **验证数据库并重启业务**：
   先启动数据库容器：
   ```bash
   docker compose up -d postgres
   ```
   进入数据库检查租户数据表：
   ```bash
   psql -U kwork -d app -c 'select count(*) from platform.tenant'
   ```
   确认数据无误后，启动全部服务容器：
   ```bash
   docker compose up -d
   ```
5. **验收服务与审计日志**：
   - 通过 `curl` 检查 [工作台](/guide/workspace) 访问入口与 `/api/capabilities` 接口状态。
   - 对每一个租户调用 `/api/audit/verify` 接口校验审计完整性。
   - 告知用户所选恢复时间点之后的改动已经丢失。
6. **立即生成新全量备份**：
   恢复完成后数据库会进入新的时间线，复制槽也会重建。必须立刻手动触发一次全量备份：
   ```bash
   docker compose exec pg-backup kwork-backup full
   ```
7. **清理临时故障数据卷**：
   在系统全面验收稳定后，再删除此前保存的临时卷 `kwork_pg_broken`。

### 整机灾备演练流程

在全新主机上进行完整灾难恢复时（runbook 建议每季度演练一次；公测文档建议上线前做一次，之后每月一次），遵循以下顺序执行：

1. 在新主机上准备基础运行环境。
2. 还原 `.env` 配置文件与 `state/` 状态目录。
3. 执行 PostgreSQL 时间点恢复。
4. 还原 OpenBao 密钥卷并使用 3/5 份额解封。
5. 执行 `docker compose up -d` 启动全部容器。
6. 对所有租户调用 `/api/audit/verify` 完成审计完整性验证。

### 停止备份与故障排错

- **停用备份服务与清理复制槽**：
  如果停止了 `pg-backup` 容器，**必须手动在数据库中删除复制槽**。否则数据库会持续为该槽保留 WAL 日志（最大占用 8 GB 空间）：
  ```sql
  select pg_drop_replication_slot('kwork_backup');
  ```
- **复制槽失效（`wal_status=lost`）**：
  若复制槽状态变为 `wal_status=lost`，说明日志段已丢失。你需要删除当前槽、重启 `pg-backup` 容器，并立即执行一次全量备份。在缺口发生前的时间范围仍可依赖旧全量备份恢复，缺口之后的恢复则从新全量备份开始。
- **容器持续重启排查**：
  检查对象存储网络连通性与认证配置，或确认 `.env` 中的 `BACKUP_ENCRYPTION_KEY` 是否为标准的 64 位十六进制格式。若日志中偶发出现 `wal-receive exited` 报错，进程会在 5 秒后自动重新建立连接。
- **数据库镜像升级兼容性**：
  如果更改了 `POSTGRES_IMAGE`（例如升级至包含 pgvector 的新版本），必须重新构建 `pg-backup` 镜像。用于数据恢复的 PostgreSQL 容器必须与备份时保持相同的主版本号以及相同的 C 语言运行库。从 PostgreSQL 17 升级至 18 后，旧版本的备份文件无法直接用于新版本数据库：升级完成后必须立刻创建一次新的全量备份，并更换新的 `BACKUP_S3_PREFIX` 存储路径。

## 实测性能指标

以下数据基于开发机一次性环境（329 MB 数据库集群）实测：

- 冷启动恢复演练全程合计耗时约 35 秒；
- 时间点恢复能够精确还原数据；
- 强行停止源数据库并删除数据卷后，实际丢失数据约 33 秒（由于闲时切 WAL 间隔为 60 秒，远低于 5 分钟的 RPO 目标）；
- 冷拉取全量备份耗时约 19 秒（传输速率约 17 MB/s）；
- 几十 GB 以上的大规模数据场景尚未进行实测。

## 让助手帮你做

系统没有向助手（Agent）开放任何备份与恢复相关的操作指令。备份属于主机底层的基础设施管理命令，不暴露给租户或组织的助手，助手不能查询、触发或配置备份任务。

## 与其他应用的关系

- **身份中心（Zitadel）**：用户账号与身份鉴权数据存储在 `zitadel` 数据库中，随 PostgreSQL 集群的 WAL 日志与全量备份一同备份。
- **密钥管理（OpenBao）**：OpenBao 中保存了租户自带存储（BYO 存储）凭证与自备模型 API 密钥。在灾难恢复时仅恢复 PostgreSQL 数据库是不够的，必须同时恢复 OpenBao 的状态卷。
- **对象存储**：用户上传的文件保存在对象存储中，不包含在 WAL-G 的备份范围内，需要依赖对象存储自身的多版本控制或跨区复制策略。
- **审计日志归档**：审计日志归档存储桶可以通过配置 `KWORK_AUDIT_ARCHIVE_BUCKET` 环境变量单独开启对象锁，防止归档日志被意外删除或篡改。

## 设置

PostgreSQL 备份相关的 [配置项](/deploy/configuration) 位于 `deploy/compose/.env` 文件中：

| 环境变量 | 作用说明 | 默认值 |
|---|---|---|
| `BACKUP_ENCRYPTION_KEY` | 加密全量备份、WAL 日志与 state 状态包（64 位十六进制） | 运行 `--backup` 时自动生成 |
| `BACKUP_DB_PASSWORD` | 数据库复制角色 `kwork_backup` 的连接密码 | 自动生成 |
| `BACKUP_S3_ENDPOINT` | 备份对象存储的 API 端点地址（未配置时可回退读取 `KWORK_S3_*` 或 `S3_*`） | onprem 环境使用内置套件存储 |
| `BACKUP_S3_BUCKET` | 备份对象存储桶名称 | `kwork-backup`（onprem） |
| `BACKUP_S3_PREFIX` | 备份在存储桶中的对象前缀路径 | `pg/` |
| `BACKUP_S3_ACCESS_KEY` | 备份对象存储的访问密钥 Access Key | 同上 |
| `BACKUP_S3_SECRET_KEY` | 备份对象存储的私有密钥 Secret Key | 同上 |
| `BACKUP_WAL_SWITCH_SECONDS` | 闲时强制切换 WAL 日志的间隔秒数 | `60` |
| `BACKUP_FULL_AT` | 每日触发全量备份的时刻 | `02:30` |
| `BACKUP_TZ` | 全量备份时刻对应的时区（对应 compose 中的 `TZ`） | `UTC` |
| `BACKUP_RETAIN_FULL` | 保留的全量备份份数 | `7` |
| `BACKUP_CREATE_BUCKET` | 是否在启动时自动创建备份存储桶 | 套件环境下通常为 `true` |
| `BACKUP_MAX_SLOT_WAL_KEEP` | 复制槽保留 WAL 日志的最大磁盘空间 | `8GB` |

## 注意事项

- **高权限复制角色**：复制角色 `kwork_backup` 能够通过复制协议读取整个集群的数据，其密码必须按照超级用户密码级别进行保密。
- **单机同盘风险**：默认配置下备份存储在同一台物理主机上，磁盘损坏会导致数据与备份一并丢失，不属于真正的异地灾备。
- **恢复目标目录限制**：执行 `restore` 命令时，目标数据目录必须为空，否则恢复命令将拒绝执行。
- **演练与线上环境隔离**：恢复演练在独立容器内进行，不触碰线上运行中的服务栈；真实恢复时必须先停止所有业务写入。
- **时间线切换与全量备份**：恢复到新库后系统会切换时间线，必须立刻执行一次全量备份以重建复制槽。

## 目前不支持

- 运营台没有一键备份或恢复按钮，不支持在网页界面上直接触发或还原备份。
- 对象存储的跨区或异机自动复制功能未内置，需部署方自备。
- 未提供把演练数据库容器直接切换为生产运行栈的自动化流程。
- 真实环境未提供从头再跑一遍 `install.sh --backup` 的自动化操作。

## 常见问题

### 备份加密密钥丢失后能否恢复数据？

不能。丢失 `BACKUP_ENCRYPTION_KEY` 会导致所有已加密的历史全量备份、WAL 日志与 state 状态包彻底无法解密恢复。更换加密密钥只对后续新产生的备份生效。因此必须将该密钥与 OpenBao 解封份额一起进行脱机异地保存。

### 为什么停止备份容器后数据库磁盘空间持续下降？

停止 `pg-backup` 容器后，若没有在 PostgreSQL 中删除复制槽 `kwork_backup`，数据库会持续保留 WAL 日志，直到达到 `BACKUP_MAX_SLOT_WAL_KEEP` 设定的 8 GB 上限。停用备份后必须执行 `select pg_drop_replication_slot('kwork_backup')` 显式清理复制槽。

### 恢复演练会不会影响正在运行的业务系统？

不会。`restore-drill.sh` 脚本在一次性临时容器与临时卷（`kbk-drill-<时间>`）中拉取备份与回放 WAL 日志，恢复后的测试库仅绑定 Unix domain socket，不监听对外网络端口。演练完成并输出检查结果后，脚本会自动销毁临时容器与卷。

### 真实恢复时执行 restore 命令报错提示目录非空该如何处理？

WAL-G 恢复工具出于安全保护机制，只允许写入空的数据目录。你需要先停止 PostgreSQL 容器，将原有数据目录中的文件移动至临时卷（例如 `kwork_pg_broken`），清空原目录后再重新运行恢复命令。
