From df6a0e46a6c24477800941b3b2fabfefdf54396c Mon Sep 17 00:00:00 2001 From: flym Date: Thu, 10 Sep 2026 17:48:51 +0800 Subject: [PATCH] docs: rework offsite backup into a real, restorable archive Move backup to /volume2/NetBackup (DSM shared folder, kts_bak read/write) and change the design from a --delete mirror to an additive, date-stamped archive: per-run pg_dump snapshots for the DBs and per-run full tar.gz snapshots for audio, each kept for 7 generations, transferred over encrypted SSH with post-transfer SHA256 verification (success only when byte-identical). This removes the --delete risk (source-side deletion no longer wipes the backup), enables restore-to-a-date, and is verified by an actual restore from the NAS into isolated DBs (0 errors, quota/ledger and audio associations consistent). --- docs/OPERATIONS.md | 72 ++++++++++++++++++++++++------------------ docs/phase-5-report.md | 17 ++++++---- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index b5f050f..4743a91 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -10,27 +10,30 @@ - 写入:成功结算时先写文件、再登记 `audio_files`(含 SHA256)。 - 读取:回放/下载按 `storage_key` 取文件;文件缺失返回 404(优雅降级)。 -## 2. 备份(已配置异地备份) +## 2. 备份(已配置异地备份,真实恢复已验证) -- 本机暂存:`/home/flym/backups/`(仅存放 `.dump`)。 -- 异地备份目标:**Synology NAS** `103.40.14.100`,账号 `kts_bak`,落盘 **`/volume1/NetBackup/kaotings/`**(`db/` + `audio/`,**不在 home 下**),可用容量约 92 GiB。 -- 传输方式:**tar over SSH**(端口 `52200`,**加密**),密钥认证(`/root/.ssh/id_ed25519_ktsbak`,公钥已加入 NAS `kts_bak` 的 `authorized_keys`)。**不使用** 公网明文 rsync 守护端口 `50873`。 - - 为何不用 rsync:Synology 限制 rsync-over-SSH 仅能写入**已注册的共享文件夹**;`/volume1/NetBackup` 是普通目录(非 DSM 共享文件夹),rsync 会报 `0 bytes received`。而 `scp`/sftp 虽能写入,但会生成 **root 属主**文件,`kts_bak` 无法轮转删除。**tar over SSH(ssh exec)** 生成的文件属主为 `kts_bak`,可正常轮转。 - - 升级路径:若在 DSM 中将 `NetBackup` 建为**共享文件夹**(控制面板→共享文件夹,授予 `kts_bak` 读写),即可改回 `rsync -a --delete` 获得增量传输。 -- 脚本:`/usr/local/sbin/kaotings-backup.sh`(root 运行): - 1. `pg_dump --format=custom` 业务库(`/etc/kaotings/api.env` → `DATABASE_URL`)与 CMS 库(`/etc/kaotings/cms.env` → `DATABASE_URI`)。 - 2. 每库本机保留最近 **7** 份(按序列独立轮转)。 - 3. `tar` over SSH 传 `db/` 与 `audio/`(`AUDIO_STORAGE_DIR`)到 NAS;再在远端按序列保留 7 份(ssh exec,可删除 `kts_bak` 属主文件)。 -- 定时:systemd `kaotings-backup.timer`,每日 **03:00**(`Persistent=true`,`RandomizedDelaySec=30min`)触发 `kaotings-backup.service`。 -- 验证:备份已落盘 `/volume1/NetBackup/kaotings`(db 5+6 份、音频 22 文件,共 5.4M,属主 `kts_bak`);`pg_restore --list` 确认归档可恢复。 +- 本机暂存:`/home/flym/backups/`(`.dump` + `audio_*.tar.gz`;仅暂存,真正归档在 NAS)。 +- 异地备份目标:**Synology NAS** `103.40.14.100`(DSM),账号 `kts_bak`,落盘 **`/volume2/NetBackup/kaotings/`**(`db/` + `audio/`,**不在 home 下**)。`/volume2/NetBackup` 为 DSM 共享文件夹,`kts_bak` 已授读写。 +- 传输:SSH `52200`(**加密**,密钥 `/root/.ssh/id_ed25519_ktsbak`);**不使用** 公网明文 rsync 守护端口 `50873`。 +- 设计原则(这是“备份”,不是“镜像”): + - **追加、绝不用 `--delete`**:每次只向 NAS 追加本次的时间戳快照;源端删除/清理/损坏**不会**删除或清空 NAS 副本(避免“单一镜像被源端误删连带清空”)。 + - **按日期可恢复**:DB 每运行一份时间戳 `pg_dump` 快照;音频每运行一份**整树 `tar.gz` 快照**。任一历史日期可单独恢复。 + - **有界保留**:每序列保留最近 **7** 份(本地与 NAS 各自按时间戳裁剪,互不依赖源端状态)。 + - **传输后校验**:对本次 3 个产物(2 个 dump + 1 个音频快照)逐一比对本地与 NAS 的 **SHA256**;任一缺失/不一致即判失败并 `exit 1`,**只有完整且校验通过才记为成功**(半成品/损坏不会标记成功)。 +- 脚本:`/usr/local/sbin/kaotings-backup.sh`(**root 运行**,原因见 §6): + 1. `pg_dump --format=custom` 业务库(`api.env`→`DATABASE_URL`)与 CMS 库(`cms.env`→`DATABASE_URI`)。 + 2. `tar -czf audio_.tar.gz` 整树音频快照。 + 3. rsync 追加 3 个产物到 NAS → SHA256 校验 → 按序列保留 7 份。 +- 定时:systemd `kaotings-backup.timer`,每日 **03:00**(`Persistent=true`,`RandomizedDelaySec=30min`)。 +- 已验证(**从 NAS 实际恢复**,非仅 `pg_restore --list`):拉取最新 dump + 音频快照 → `pg_restore` 到独立库 **0 错误**;行数与源一致(users 35 / quota 34 / tasks 28 / audio 26 / usage 61);**额度↔流水 0 不符**(34 账户:`used=Σconsume`、`reserved=Σreserve−Σconsume−Σrelease`);音频 22/26 与源一致(4 条为既有“有行无文件”,快照如实捕获)。 -| 对象 | 工具 | 本机 | 异地(NAS `/volume1/NetBackup/kaotings`) | -| --- | --- | --- | --- | -| 业务库 `kaotings` | `pg_dump --format=custom` + tar | `/home/flym/backups/kaotings_.dump` | `…/kaotings/db/` | -| CMS 库 `kaotings_cms` | `pg_dump --format=custom` + tar | `/home/flym/backups/kaotings_cms_.dump` | `…/kaotings/db/` | -| 音频目录 | tar over SSH | `/home/flym/kaotings-audio/` | `…/kaotings/audio/` | +| 对象 | 本机暂存 | 异地(`/volume2/NetBackup/kaotings`) | +| --- | --- | --- | +| 业务库 `kaotings` | `kaotings_.dump` | `db/kaotings_.dump`(保留 7) | +| CMS 库 `kaotings_cms` | `kaotings_cms_.dump` | `db/kaotings_cms_.dump`(保留 7) | +| 音频 | `audio_.tar.gz`(整树) | `audio/audio_.tar.gz`(保留 7) | -> 仍待用户确认:音频保留期、磁盘告警阈值、是否启用按 `expires_at` 的定时清理(见 §5);备份保留份数当前=7(可调脚本 `KEEP`)。如希望增量传输,请在 DSM 将 `NetBackup` 建为共享文件夹(见上“升级路径”)。 +> 待确认:备份保留份数(当前 7,可调脚本 `KEEP`)、失败通知渠道(见 §6)。音频保留期 / 磁盘阈值见 §5。 ## 3. 容量 / 保留 / 清理(需用户决定的参数集中列出) @@ -47,22 +50,22 @@ - 保留期清理在参数确认并实现后启用;**在此之前不删除任何既有音频**。 - 告警:磁盘使用率超阈值、`audio_files` 与磁盘文件数量长期不一致、Worker 长期无 `queued` 消化。 -## 4. 恢复(含隔离演练结果) +## 4. 恢复(已从 NAS 实际恢复验证) ### 4.1 流程 1. 停止写入(可选,视 RPO):`systemctl stop kaotings-api`。 -2. 建空库 → `pg_restore --clean --if-exists` 业务库 / CMS 库。 -3. 音频:从 NAS 用 `tar` over SSH 拉回 `/home/flym/kaotings-audio`(源:`/volume1/NetBackup/kaotings/audio/`)。 -4. 校验:行数、音频文件↔`audio_files` 关联、`checksum`、额度↔流水一致(见 §4.2 校验 SQL)。 +2. 建空库(装 `pgcrypto`)→ `pg_restore --clean --if-exists --no-owner` 恢复**目标日期**的业务库 / CMS 库 dump。 +3. 音频:从 NAS 拉取**对应日期**的 `audio_.tar.gz`,解压到 `/home/flym/kaotings-audio`(源:`/volume2/NetBackup/kaotings/audio/`)。 +4. 校验:行数、音频文件↔`audio_files` 关联、额度↔流水一致(见 §4.2)。 5. `systemctl start kaotings-api`,`/healthz` 复核。 -### 4.2 校验(隔离演练已执行) +### 4.2 从 NAS 实际恢复校验(已执行,非仅 `pg_restore --list`) -- 业务库行数源=恢复:users 35 / quota 34 / tasks 28 / audio 26 / usage 61 / grants 10。 -- 额度↔流水:`used` 与 `consume`、`reserved` 与 `reserve-consume-release` 均 0 不符。 -- 音频:磁盘 22 文件全部被 `available` 引用(无孤儿)。 -- **发现**:4 条 `status=available` 且任务 `succeeded` 的 `audio_files` 行在磁盘无文件(既有不一致,回放 404 降级)。待调查根因(见 §6)。 +- 拉取 NAS 最新 `kaotings_.dump` / `kaotings_cms_.dump` / `audio_.tar.gz` → `pg_restore` 到独立库,**业务库 0 错误、CMS 0 错误**。 +- 行数源=恢复:users 35 / quota 34 / tasks 28 / audio 26 / usage 61。 +- 额度↔流水:**0 不符**(34 账户;`used=Σconsume`、`reserved=Σreserve−Σconsume−Σrelease`)。 +- 音频↔文件:`available` 行 26,快照内命中 22、缺 4(与源一致的既有“有行无文件”,非备份缺失)。 ### 4.3 Worker 重启 / 额度对账 @@ -76,11 +79,20 @@ 2. 磁盘容量上限与告警阈值、通知方式。 3. 是否启用按 `expires_at` 的定时保留期清理。 4. 原始文本保留期。 -5. 备份:异地 NAS 已配置(`103.40.14.100`,每日 03:00,每库 7 份);**剩余待确认**:保留份数/周期是否调整、备份责任人。 +5. 备份:异地 NAS 已配置(`/volume2/NetBackup`,每日 03:00,追加 + 校验 + 每序列 7 份);**剩余待确认**:保留份数(脚本 `KEEP`)、失败通知渠道、是否改为非 root 运行(见 §6)。 6. 上游 HTTPS/加密端点、允许来源网段、凭据轮换(见 `DEPLOYMENT.md` §4)。 -## 6. 待调查 / 未闭环项(不标为完成) +## 6. 备份运行权限与失败通知 -- **4 条 `available` 音频行无对应文件**:既有 DB/文件系统不一致,待定位根因(文件被外部删除 or 写入路径异常);恢复流程本身通过。 +- **当前以 root 运行**(`kaotings-backup.service` `User=root`)。原因:需读取 root-only 的 `/etc/kaotings/api.env`、`cms.env`(数据库连接串)并使用 `/root/.ssh` 密钥。**并非“无需管理员权限”**。 + - 若需降权:可建专用 `kts_backup` 用户 + 仅含所需 `GRANT` 的 DB 角色 + 0600 的独立 env 文件 + 专用 SSH 密钥,使备份以非 root 运行。**待用户确认是否需要**(当前测试环境 root 运行可接受)。 +- **失败通知**: + - 已具备:备份脚本校验失败即 `exit 1` → systemd 将服务标记为 `failed`,日志进 **journald**(`journalctl -u kaotings-backup` 可查)。 + - 待接入:主动告警(邮件/webhook/IM)需要一个渠道。SMTP 为 V1 已声明限制(未接入)。**待用户指定通知渠道**后,在 service 增加 `ExecStopPost`/`OnFailure` 钩子推送。 + +## 7. 待调查 / 未闭环项(不标为完成) + +- **4 条 `available` 音频行无对应文件**:既有 DB/文件系统不一致,待定位根因(文件被外部删除 or 写入路径异常);备份/恢复流程本身通过(快照如实捕获 22 个实际文件)。 - **上游公网 HTTP**:上线阻断,待加密 + 来源限制 + 外部复测(`DEPLOYMENT.md` §4)。 - **音频容量/保留/清理参数**:待用户确认(§5)。 +- **备份失败通知渠道 / 是否降权**:待用户确认(§6)。 diff --git a/docs/phase-5-report.md b/docs/phase-5-report.md index 20aa146..80ce3df 100644 --- a/docs/phase-5-report.md +++ b/docs/phase-5-report.md @@ -90,14 +90,17 @@ 结论:业务库、CMS 库、音频均可在隔离环境完整恢复,历史↔音频关联与额度数据一致(除上述 4 条既有缺失)。 -### 3.3 异地备份(本阶段后续配置,已落地) +### 3.3 异地备份(本阶段后续配置,已落地并实际恢复验证) -- 用户提供异地 NAS(Synology)`103.40.14.100`(SSH `52200` / rsync 守护 `50873`)。按要求**不放 home**,落盘 `/volume1/NetBackup/kaotings/`(`db/`+`audio/`)。 -- 采用 **tar over SSH(52200,加密 + 密钥认证)**,不使用公网明文 rsync 守护端口;密钥 `/root/.ssh/id_ed25519_ktsbak`,公钥加入 NAS `kts_bak`。 - - 说明:`/volume1/NetBackup` 为普通目录(非 DSM 共享文件夹),Synology 限制 rsync-over-SSH 仅写共享文件夹,且 `scp` 生成 root 属主文件不可轮转,故选 tar over SSH(文件属主 `kts_bak`,可轮转)。若后续在 DSM 建为共享文件夹,可改回 rsync 增量。 -- 脚本 `/usr/local/sbin/kaotings-backup.sh`(root):`pg_dump` 业务库 + CMS 库(每库保留 7 份)+ `tar` 传音频目录 → `/volume1/NetBackup/kaotings/{db,audio}/`;systemd `kaotings-backup.timer` 每日 03:00 触发。 -- 已验证:备份落盘(db 5+6 份、音频 22 文件,5.4M,属主 `kts_bak`),`pg_restore --list` 确认可恢复;详见 `OPERATIONS.md` §2。 -- 仍待确认:保留份数/周期、磁盘告警阈值、责任人(`OPERATIONS.md` §5)。 +- 用户提供异地 NAS(Synology)`103.40.14.100`。按要求**不放 home**,落盘 **`/volume2/NetBackup/kaotings/`**(DSM 共享文件夹,`db/`+`audio/`,`kts_bak` 已授读写)。 +- 传输 **SSH `52200`(加密 + 密钥认证)**,不使用公网明文 rsync 守护端口 `50873`;密钥 `/root/.ssh/id_ed25519_ktsbak`。 +- 设计为“备份”而非“镜像”(针对评审意见修正): + - **追加、绝不用 `--delete`**:每次追加时间戳快照;源端删除/清理/损坏不会删除或清空 NAS 副本。 + - **按日期可恢复**:DB 每运行一份 `pg_dump` 快照;音频每运行一份整树 `tar.gz` 快照;保留最近 7 份/序列。 + - **传输后 SHA256 校验**:3 个产物逐一比对本地与 NAS,缺失/不一致即 `exit 1`,**只有完整且校验通过才记成功**。 +- 脚本 `/usr/local/sbin/kaotings-backup.sh`(root);systemd `kaotings-backup.timer` 每日 03:00 触发。 +- **已从 NAS 实际恢复验证**(非仅 `pg_restore --list`):`pg_restore` 到独立库 0 错误;行数与源一致;额度↔流水 0 不符(34 账户);音频 22/26 与源一致。详见 `OPERATIONS.md` §2/§4。 +- 仍待确认:保留份数(`KEEP`)、失败通知渠道、是否降权运行(`OPERATIONS.md` §5/§6)。 ## 4. 上游安全改造(上线阻断,待外部条件)