www_site/docs/OPERATIONS.md
flym df6a0e46a6 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).
2026-09-10 17:48:51 +08:00

99 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 运维方案OPERATIONS
日期2026-09-10 · 版本 `8657f44`
范围:音频容量/保留/清理/备份/恢复、数据库备份/恢复、Worker 与额度对账、告警。**本轮不擅自删除任何既有音频。**
## 1. 音频存储现状
- 位置:`/home/flym/kaotings-audio`,布局 `{user_id}/{task_id}.wav`(私有本地目录,非对象存储)。
- 元数据:`audio_files``storage_key`、`mime_type`、`size_bytes`、`checksum`、`status`、`expires_at`)。
- 写入:成功结算时先写文件、再登记 `audio_files`(含 SHA256
- 读取:回放/下载按 `storage_key` 取文件;文件缺失返回 404优雅降级
## 2. 备份(已配置异地备份,真实恢复已验证)
- 本机暂存:`/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_<ts>.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 条为既有“有行无文件”,快照如实捕获)。
| 对象 | 本机暂存 | 异地(`/volume2/NetBackup/kaotings` |
| --- | --- | --- |
| 业务库 `kaotings` | `kaotings_<ts>.dump` | `db/kaotings_<ts>.dump`(保留 7 |
| CMS 库 `kaotings_cms` | `kaotings_cms_<ts>.dump` | `db/kaotings_cms_<ts>.dump`(保留 7 |
| 音频 | `audio_<ts>.tar.gz`(整树) | `audio/audio_<ts>.tar.gz`(保留 7 |
> 待确认:备份保留份数(当前 7可调脚本 `KEEP`)、失败通知渠道(见 §6。音频保留期 / 磁盘阈值见 §5。
## 3. 容量 / 保留 / 清理(需用户决定的参数集中列出)
| 参数 | 当前值 | 需用户决定 |
| --- | --- | --- |
| 音频保留期 `AUDIO_RETENTION_SECONDS` | 7 天(默认) | 是(产品/隐私/存储预算) |
| 磁盘容量上限 / 告警阈值 | 根盘约 48 GiB 可用,无上限告警 | 是(阈值 + 通知方式) |
| 保留期清理策略 | 未启用定时清理(仅启动时清孤儿) | 是(是否按 `expires_at` 定时删除) |
| 原始文本保留期 | 存于 `tts_tasks.text` | 是(产品/隐私) |
| 备份保留份数 / 周期 | 已配置:每库 7 份 · 每日 03:00 · 异地 NAS | 否(可调脚本 `KEEP` 与 timer |
- 清理安全边界:
- 启动时 `cleanup_orphan_audio` 仅删除“**无** `available` 记录且 mtime>1h”的孤儿文件。
- 保留期清理在参数确认并实现后启用;**在此之前不删除任何既有音频**。
- 告警:磁盘使用率超阈值、`audio_files` 与磁盘文件数量长期不一致、Worker 长期无 `queued` 消化。
## 4. 恢复(已从 NAS 实际恢复验证)
### 4.1 流程
1. 停止写入(可选,视 RPO`systemctl stop kaotings-api`。
2. 建空库(装 `pgcrypto`)→ `pg_restore --clean --if-exists --no-owner` 恢复**目标日期**的业务库 / CMS 库 dump。
3. 音频:从 NAS 拉取**对应日期**的 `audio_<ts>.tar.gz`,解压到 `/home/flym/kaotings-audio`(源:`/volume2/NetBackup/kaotings/audio/`)。
4. 校验:行数、音频文件↔`audio_files` 关联、额度↔流水一致(见 §4.2)。
5. `systemctl start kaotings-api``/healthz` 复核。
### 4.2 从 NAS 实际恢复校验(已执行,非仅 `pg_restore --list`
- 拉取 NAS 最新 `kaotings_<ts>.dump` / `kaotings_cms_<ts>.dump` / `audio_<ts>.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 重启 / 额度对账
- 单 Workersystemd 单进程),原子领取(`FOR UPDATE SKIP LOCKED`+ 5 分钟租约 + 过期 `running` 回收(`WORKER_LEASE_EXPIRED`,释放冻结)。
- 对账:`GET /admin/usage/summary` 汇总 `quota_accounts``usage_records` 口径;`GET /admin/tts/tasks` 查结算状态consumed/released/pending
- 不变式(每账户恒成立):`limit_snapshot + adjustment - used - reserved >= 0`(由 Phase 5 异常矩阵 S3/S4 保证)。
## 5. 待用户确认清单(集中)
1. 音频保留期(`AUDIO_RETENTION_SECONDS`)。
2. 磁盘容量上限与告警阈值、通知方式。
3. 是否启用按 `expires_at` 的定时保留期清理。
4. 原始文本保留期。
5. 备份:异地 NAS 已配置(`/volume2/NetBackup`,每日 03:00追加 + 校验 + 每序列 7 份);**剩余待确认**:保留份数(脚本 `KEEP`)、失败通知渠道、是否改为非 root 运行(见 §6
6. 上游 HTTPS/加密端点、允许来源网段、凭据轮换(见 `DEPLOYMENT.md` §4
## 6. 备份运行权限与失败通知
- **当前以 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