2026-09-09 21:01:27 +00:00
|
|
|
|
# 运维方案(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(优雅降级)。
|
|
|
|
|
|
|
2026-09-10 09:48:51 +00:00
|
|
|
|
## 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。
|
2026-09-09 21:01:27 +00:00
|
|
|
|
|
|
|
|
|
|
## 3. 容量 / 保留 / 清理(需用户决定的参数集中列出)
|
|
|
|
|
|
|
|
|
|
|
|
| 参数 | 当前值 | 需用户决定 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| 音频保留期 `AUDIO_RETENTION_SECONDS` | 7 天(默认) | 是(产品/隐私/存储预算) |
|
|
|
|
|
|
| 磁盘容量上限 / 告警阈值 | 根盘约 48 GiB 可用,无上限告警 | 是(阈值 + 通知方式) |
|
|
|
|
|
|
| 保留期清理策略 | 未启用定时清理(仅启动时清孤儿) | 是(是否按 `expires_at` 定时删除) |
|
|
|
|
|
|
| 原始文本保留期 | 存于 `tts_tasks.text` | 是(产品/隐私) |
|
2026-09-10 07:24:04 +00:00
|
|
|
|
| 备份保留份数 / 周期 | 已配置:每库 7 份 · 每日 03:00 · 异地 NAS | 否(可调脚本 `KEEP` 与 timer) |
|
2026-09-09 21:01:27 +00:00
|
|
|
|
|
|
|
|
|
|
- 清理安全边界:
|
|
|
|
|
|
- 启动时 `cleanup_orphan_audio` 仅删除“**无** `available` 记录且 mtime>1h”的孤儿文件。
|
|
|
|
|
|
- 保留期清理在参数确认并实现后启用;**在此之前不删除任何既有音频**。
|
|
|
|
|
|
- 告警:磁盘使用率超阈值、`audio_files` 与磁盘文件数量长期不一致、Worker 长期无 `queued` 消化。
|
|
|
|
|
|
|
2026-09-10 09:48:51 +00:00
|
|
|
|
## 4. 恢复(已从 NAS 实际恢复验证)
|
2026-09-09 21:01:27 +00:00
|
|
|
|
|
|
|
|
|
|
### 4.1 流程
|
|
|
|
|
|
|
|
|
|
|
|
1. 停止写入(可选,视 RPO):`systemctl stop kaotings-api`。
|
2026-09-10 09:48:51 +00:00
|
|
|
|
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)。
|
2026-09-09 21:01:27 +00:00
|
|
|
|
5. `systemctl start kaotings-api`,`/healthz` 复核。
|
|
|
|
|
|
|
2026-09-10 09:48:51 +00:00
|
|
|
|
### 4.2 从 NAS 实际恢复校验(已执行,非仅 `pg_restore --list`)
|
2026-09-09 21:01:27 +00:00
|
|
|
|
|
2026-09-10 09:48:51 +00:00
|
|
|
|
- 拉取 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(与源一致的既有“有行无文件”,非备份缺失)。
|
2026-09-09 21:01:27 +00:00
|
|
|
|
|
|
|
|
|
|
### 4.3 Worker 重启 / 额度对账
|
|
|
|
|
|
|
|
|
|
|
|
- 单 Worker(systemd 单进程),原子领取(`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. 原始文本保留期。
|
2026-09-10 09:48:51 +00:00
|
|
|
|
5. 备份:异地 NAS 已配置(`/volume2/NetBackup`,每日 03:00,追加 + 校验 + 每序列 7 份);**剩余待确认**:保留份数(脚本 `KEEP`)、失败通知渠道、是否改为非 root 运行(见 §6)。
|
2026-09-09 21:01:27 +00:00
|
|
|
|
6. 上游 HTTPS/加密端点、允许来源网段、凭据轮换(见 `DEPLOYMENT.md` §4)。
|
|
|
|
|
|
|
2026-09-10 09:48:51 +00:00
|
|
|
|
## 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. 待调查 / 未闭环项(不标为完成)
|
2026-09-09 21:01:27 +00:00
|
|
|
|
|
2026-09-10 09:48:51 +00:00
|
|
|
|
- **4 条 `available` 音频行无对应文件**:既有 DB/文件系统不一致,待定位根因(文件被外部删除 or 写入路径异常);备份/恢复流程本身通过(快照如实捕获 22 个实际文件)。
|
2026-09-09 21:01:27 +00:00
|
|
|
|
- **上游公网 HTTP**:上线阻断,待加密 + 来源限制 + 外部复测(`DEPLOYMENT.md` §4)。
|
|
|
|
|
|
- **音频容量/保留/清理参数**:待用户确认(§5)。
|
2026-09-10 09:48:51 +00:00
|
|
|
|
- **备份失败通知渠道 / 是否降权**:待用户确认(§6)。
|
2026-09-18 06:47:30 +00:00
|
|
|
|
|
|
|
|
|
|
## 8. Git 推送凭据:Gitea OAuth token 1 小时过期导致「首次推送必失败」(已修复)
|
|
|
|
|
|
|
|
|
|
|
|
### 8.1 现象
|
|
|
|
|
|
|
|
|
|
|
|
推送时第一次总是失败,重试第二次才成功:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
remote: Verify
|
|
|
|
|
|
fatal: Authentication failed for 'http://rand.team:44000/Kaotings/www_site.git/'
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 8.2 根因(上游 Gitea token 有效期 + GCM 缓存)
|
|
|
|
|
|
|
|
|
|
|
|
- 上游 Gitea(`rand.team:44000`)签发的 OAuth token 是一对 JWT,解码 payload 可见:
|
|
|
|
|
|
- access token:`{"gnt":2,"tt":0,"exp":iat+3600}` → **仅 1 小时有效**
|
|
|
|
|
|
- refresh token:`{"gnt":2,"tt":1,"exp":iat+2628000}` → 30 天有效(每次刷新会轮换)
|
|
|
|
|
|
- Git Credential Manager 把 access token 缓存进 Windows 凭据管理器,而它对 generic OAuth 主机**无法感知 token 过期时间**,于是闲置超过 1 小时后:
|
|
|
|
|
|
1. 第一次推送 → GCM 返回已过期的 access token → 服务器拒绝(`remote: Verify`);
|
|
|
|
|
|
2. GCM 删除该缓存凭据;
|
|
|
|
|
|
3. 第二次推送 → 缓存已空 → GCM 用 refresh token 静默换新 token → 成功。
|
|
|
|
|
|
|
|
|
|
|
|
即:不是服务器故障,也不是密码错误,而是「1 小时 token + 无过期感知的缓存」。
|
|
|
|
|
|
|
|
|
|
|
|
### 8.3 解决方式(已在本机实施)
|
|
|
|
|
|
|
|
|
|
|
|
新增凭据助手 `C:/Users/kts/.git-credential-kaotings.mjs`,改为**每次都用 refresh token 换取新 access token**,并在本地记录过期时间提前 5 分钟刷新,从而第一次推送即成功。
|
|
|
|
|
|
|
|
|
|
|
|
关键配置(**顺序很重要**):git 的 helper 按 `系统 → 全局 → 仓库` 顺序追加,空值 `helper =` 会清空此前累积的列表。系统级 `C:/Program Files/Git/etc/gitconfig` 中存在 `helper = manager`(修改它需管理员权限),因此必须在全局配置 `C:/Cadence/SPB_Data/.gitconfig` 里先重置再排序:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
[credential]
|
|
|
|
|
|
helper =
|
|
|
|
|
|
helper = !node C:/Users/kts/.git-credential-kaotings.mjs
|
|
|
|
|
|
helper = manager
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- 助手只响应 `host=rand.team:44000`,其它主机静默退出并继续交给 GCM(GitHub / Azure DevOps 等不受影响)。
|
|
|
|
|
|
- 长期 refresh token **只存于 Windows 凭据管理器**(DPAPI 保护);本地缓存 `~/.kaotings-git-token.json` 只存 1 小时的 access token;刷新时轮换得到的新 refresh token 自动回写凭据管理器。
|
|
|
|
|
|
- 手动兜底(不依赖上述配置,例如换机器时):
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
git -c credential.helper= -c credential.helper="!node C:/Users/kts/.git-credential-kaotings.mjs" push origin <branch>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- 换新机器/新环境:仍需先走一次浏览器 OAuth(GCM),拿到 refresh token 后本助手才有凭据可用。
|
|
|
|
|
|
|
|
|
|
|
|
### 8.4 验证记录
|
|
|
|
|
|
|
|
|
|
|
|
- 手动把缓存 access token 置为已过期后,单次 `git push` 直接成功(日志显示助手先执行并刷 token,无 401)。
|
|
|
|
|
|
- 非目标主机(如 `example.com`)调用助手无输出、正常放行给 GCM。
|
|
|
|
|
|
- 轮换后 refresh token `iat` 更新且已写入凭据管理器。
|