跳转到内容

备份、时间点恢复与恢复演练

备份 Quire、将其恢复到某个时间点,并通过恢复演练验证备份。

架构设计见 docs/architecture/23-ops.md 第 8 节。此处是 Docker Compose 产品的操作手册,供未编写本文档的人按照步骤操作;如果某个步骤不清楚,说明本文档需要修正。

保护哪些内容以及如何保护

资产 方式 位置
数据库 从首次启动开始持续归档 WAL,间隔最多 60 秒 pgwal 卷
数据库 默认每日使用 pg_basebackup 创建基础备份(backup-scheduler) pgbackup 卷
数据库 每五分钟创建一次基础备份和 WAL 的加密副本(backup-offsite) 您指定的独立存储
文件 files 卷。可用主机备份工具复制,或使用启用版本控制的对象存储 files 卷
密钥 docker/.env,尤其是 QUIRE_MASTER_KEY(以及仍在使用的 QUIRE_MASTER_KEY_RETIRED)、QUIRE_BACKUP_ENCRYPTION_KEY 和 docker/secrets/audit-signing-key.pem 在此主机之外保存副本
搜索索引、缓存、媒体版本 不备份,之后重新生成

目标:发生故障后最多丢失 60 秒数据,并在 60 分钟内恢复 500 GB 数据库。

常见错误有两种。数据库恢复后未恢复文件,页面就会显示损坏。数据库恢复后没有 QUIRE_MASTER_KEY,则无法解密其中保存的 SSO、webhook 和集成凭据;在主密钥轮换完成且没有未解决项之前(参见 key-rotation.md),还包括退休密钥。这些都属于备份内容。

创建备份

创建整个集群的基础备份:

docker compose -f docker/compose.yaml --profile backup run --rm backup

系统会保留最新的 QUIRE_BACKUP_KEEP 个基础备份(默认值为 5),并清理最旧备份不再需要的 WAL,因此归档不会无限增长。请在主机上通过 cron 或 systemd 定时器每日安排备份:

15 2 * * * cd /srv/quire && docker compose -f docker/compose.yaml --profile backup run --rm backup >> /var/log/quire-backup.log 2>&1

也可以让服务栈自动安排备份:backup 配置档案会启动 backup-scheduler,每隔 QUIRE_BACKUP_INTERVAL_HOURS 小时(默认 24 小时)创建一次基础备份,并启动下文所述的 backup-offsite。

docker compose -f docker/compose.yaml --profile backup up -d

异地加密副本

这两个卷和数据库位于同一主机上;故障主机上的备份不算真正的备份。backup-offsite 会通过存储端口,将每份基础备份和归档 WAL 段加密后复制到独立存储,并按保留规则保存:

  • 加密:使用 QUIRE_BACKUP_ENCRYPTION_KEY(或 QUIRE_BACKUP_ENCRYPTION_KEY_FILE 指定的文件)进行 AES-256-GCM 加密,密钥为 32 字节,可用 openssl rand -hex 32 生成。每个文件都有自己的 nonce 和认证标签,因此没有密钥就无法读取副本,任何更改也会被检测到。请将此密钥与 QUIRE_MASTER_KEY 一起保存在此主机和备份存储之外。没有该密钥就无法恢复。
  • 位置:QUIRE_BACKUP_STORAGE_DRIVER 可设为 s3、azure 或 local(挂载到 QUIRE_BACKUP_STORAGE_ROOT 的远程磁盘)。设置与文件存储相同,但前缀为 QUIRE_BACKUP_:例如 QUIRE_BACKUP_S3_ENDPOINT、QUIRE_BACKUP_S3_BUCKET、QUIRE_BACKUP_S3_ACCESS_KEY_ID 等。请使用不同的存储桶,最好也使用与文件存储不同的账户;如服务提供商允许,凭据应能写入但不能删除。
  • 保留期限:保留最新的 QUIRE_BACKUP_OFFSITE_KEEP 个基础备份(默认值取 QUIRE_BACKUP_KEEP,否则为 7),以及最旧基础备份所需的 WAL;存储中的旧备份集和片段会被删除。
  • 频率:每隔 QUIRE_BACKUP_SHIP_INTERVAL_SECONDS 秒发送一次(默认 300 秒)。传输具有幂等性:已存储的内容会跳过;只有在最后写入清单后,基础备份才算存储完成。

也可以手动运行相同命令:

docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts ship
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts verify

要在新主机恢复,请先取回一组备份,然后按照下文步骤操作;将下载的目录作为 pgbackup 卷的替代,并将下载的 wal-archive 作为 pgwal 的替代:

bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore

恢复到某个时间点

发生数据丢失后使用此流程,例如导入错误、课程误删,或需要撤销的契约迁移。此操作会替换线上数据库,因此请先按下方演练进行排练。

  1. 选择目标时间,使用 UTC,并选在损坏发生之前,例如 2026-09-24 09:30:00+00。通常可从审计日志(/admin/audit)查看具体时间。
  2. 停止所有写入服务:docker compose -f docker/compose.yaml stop web content worker scheduler collab。
  3. 保留受损集群,直到确认恢复成功:
    docker compose -f docker/compose.yaml stop postgres
    docker run --rm -v quire_postgres18-data:/from -v quire_postgres-damaged:/to alpine cp -a /from/. /to/
  4. 将早于目标时间的最新基础备份解压到数据卷,并请求定向恢复:
    docker run --rm -v quire_pgbackup:/backups:ro -v quire_postgres18-data:/var/lib/postgresql postgres:18-alpine sh -euc '
      base="$(ls -1d /backups/base-* | sort | tail -n 1)"   # or the one before the target
      rm -rf /var/lib/postgresql/18/docker && mkdir -p /var/lib/postgresql/18/docker
      tar -xzf "$base/base.tar.gz" -C /var/lib/postgresql/18/docker
      touch /var/lib/postgresql/18/docker/recovery.signal
      chown -R postgres:postgres /var/lib/postgresql/18/docker && chmod 700 /var/lib/postgresql/18/docker'
  5. 执行恢复:通过 Compose 覆盖文件使用恢复设置启动一次 Postgres,避免更改常规配置文件:
    # docker/compose.recover.yaml
    services:
      postgres:
        command: [postgres, -c, "restore_command=cp /var/lib/postgresql/wal-archive/%f %p",
                  -c, "recovery_target_time=2026-09-24 09:30:00+00",
                  -c, recovery_target_action=promote, -c, archive_mode=off,
                  -c, max_connections=200, -c, hba_file=/etc/postgresql/pg_hba.conf]
    覆盖文件会替换整个命令,因此必须重复恢复所需的两项设置:max_connections 不得低于主库的设置(否则恢复会因 “insufficient parameter settings” 中止),以及挂载的 pg_hba.conf。
    docker compose -f docker/compose.yaml -f docker/compose.recover.yaml up -d postgres
    docker compose -f docker/compose.yaml logs -f postgres   # wait for "database system is ready"
  6. 检查结果后再允许用户访问:验证审计链(docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts),并确认丢失的数据已恢复。
  7. 恢复正常运行:docker compose -f docker/compose.yaml up -d。这会重新启用归档并重启 Postgres,同时开始新的 WAL 时间线。请立即创建新的基础备份。

每个卷名称前都有项目名 quire;运行 docker volume ls 可查看准确名称。

定期验证演练

backup-offsite 还会每隔 QUIRE_BACKUP_DRILL_INTERVAL_HOURS 小时运行一次演练(默认 168,即每周一次);演练失败后,也会在下一轮再次执行。它会获取最新的异地主基础备份及之后的所有 WAL 片段,逐个解密(证明密钥仍可解开文件且内容未被更改),将每个文件与清单比较,确认归档是 Postgres 数据目录,并检查从基础备份开始的 WAL 是否连续。报告会写入存储中的 reports/drill-<time>.json 和服务日志;演练失败时会指出文件名或第一个缺失的片段。

恢复演练

从未恢复过的备份不算备份。演练会将早于目标时间的最新完整基础备份及 WAL 归档恢复到与线上环境完全隔离的临时 Postgres 中,并验证结果:

docker/scripts/restore-drill.sh                                  # to ninety minutes ago
docker/scripts/restore-drill.sh --target "2026-09-24 09:30:00+00"

目标必须使用 UTC,并严格采用此格式。演练需要早于目标时间的基础备份以及晚于目标时间的归档 WAL:新安装时,请先创建基础备份并等待下一个 WAL 片段归档(有写入时最长约一分钟),再选择晚于备份的目标时间。主机只需要安装 Docker 和 bash。

任一环节失败都会导致演练失败:

  1. 可恢复性:临时集群可以重放到目标时间并启动。
  2. 完整性:对比每张表在当前数据库中的行数(tooling/restore-drill)。当前数据库的数据晚于目标时间,因此任一方向的差异允许达到 500 行或表行数的十分之一(取较大值;写入会使恢复结果落后,删除会使其保留较多记录);目标时间之后创建的分区不算丢失表。较繁忙的安装可用 QUIRE_DRILL_MAX_BEHIND 和 QUIRE_DRILL_MAX_DRIFT_RATIO 放宽范围。表缺失或被清空则失败。
  3. 完整性校验:还原副本中的审计哈希链通过验证。
  4. 可用性:应用角色可通过行级安全机制读取数据。
  5. 耗时:从启动到成功的时间满足 QUIRE_DRILL_RTO_SECONDS(默认 3600)。

演练绝不会向线上数据库或其卷写入数据:备份和 WAL 卷以只读方式挂载,临时集群无论成功还是失败都会在结束时删除。

设置 QUIRE_DRILL_REPORT 为某个路径,可在成功或失败后都写入 JSON 报告;您可以从 Docker 主机安排定期运行:

30 3 1 * * cd /srv/quire && QUIRE_DRILL_REPORT=/var/log/quire-drill.json docker/scripts/restore-drill.sh >> /var/log/quire-drill.log 2>&1

每月运行一次,并在每次升级前运行。演练失败会阻止升级。每季度请让一位没有撰写本操作手册的人仅根据本文档,在备用主机上执行真正的时间点恢复。

文件

本地文件保存在 files 卷中。请与数据库同时备份,并在恢复时一起还原:

docker run --rm -v quire_files:/files:ro -v "$PWD":/out alpine tar -czf /out/files-$(date -u +%Y%m%d).tar.gz -C /files .

使用对象存储时,请启用存储桶版本控制并保留 35 天的非当前版本;这样,文件的时间点恢复由存储桶自身负责。

导航

输入以搜索…

↑↓ 导航↵ 选择Esc 关闭