www_site/docs/OPERATIONS.md

151 lines
11 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
## 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`,其它主机静默退出并继续交给 GCMGitHub / Azure DevOps 等不受影响)。取用凭据失败时(如令牌被撤销、服务器不可达)同样静默退出,交由 GCM 走浏览器重新授权,不会阻断推送流程。
- 长期 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>
```
- 换新机器/新环境:仍需先走一次浏览器 OAuthGCM拿到 refresh token 后本助手才有凭据可用。
### 8.4 验证记录
- 手动把缓存 access token 置为已过期后,单次 `git push` 直接成功(日志显示助手先执行并刷 token无 401
- 非目标主机(如 `example.com`)调用助手无输出、正常放行给 GCM。
- 轮换后 refresh token `iat` 更新且已写入凭据管理器。