2026-09-09 21:01:27 +00:00
|
|
|
|
# Phase 5 集成与上线准备报告
|
|
|
|
|
|
|
|
|
|
|
|
日期:2026-09-10
|
|
|
|
|
|
基线代码:`9568622`(Phase 4 验收通过)
|
|
|
|
|
|
本轮修复提交:`8657f44`(已部署为发布 `8657f44`)
|
|
|
|
|
|
测试环境:`192.168.199.22`,隔离库 `kaotings_p5`,隔离音频目录 `/tmp/kaotings-p5-audio*`,本地伪造上游
|
|
|
|
|
|
原则:只把有证据的能力标为完成;故障注入全部在隔离环境进行,不触碰生产 `kaotings` 库与既有音频。
|
|
|
|
|
|
|
|
|
|
|
|
## 0. 结论摘要
|
|
|
|
|
|
|
|
|
|
|
|
| 项 | 结果 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| 任务与额度异常矩阵(Phase 3 遗留) | **通过**(修复后 13 个场景全部通过;见 §1) |
|
|
|
|
|
|
| 管理员并发调减额度不变式 | **发现并修复缺陷**,已验证(见 §1.1) |
|
|
|
|
|
|
| 自动化验收固化 | **通过**(测试工具、依赖版本、运行说明入仓库,秘密走环境变量) |
|
|
|
|
|
|
| 音频运维方案 + 隔离恢复演练 | **通过(带 1 项发现)**:库/音频可恢复、额度与流水一致;发现 4 条 `available` 音频行无对应文件(见 §3) |
|
|
|
|
|
|
| 上游安全改造 | **待外部条件**:公网 HTTP 仍为上线阻断,需上游提供 HTTPS/加密通道 + 来源限制(见 §4) |
|
|
|
|
|
|
| 剩余小项(slug 500 / SMTP) | **通过(记录在案)**:非法 ID 500 不泄露堆栈/内部信息;SMTP 维持 V1 已声明限制 |
|
|
|
|
|
|
|
|
|
|
|
|
上线阻断项:**公网 HTTP 上游**(§4),未解除前不得判定“可正式上线”。
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 任务与额度异常矩阵(隔离故障注入)
|
|
|
|
|
|
|
|
|
|
|
|
### 1.1 关键缺陷:管理员调减额度与任务并发(已修复)
|
|
|
|
|
|
|
|
|
|
|
|
- 现象(基线 `9568622`):`POST /admin/users/{id}/quota-adjustments` 计算投影余额时**未**对额度账户加行锁、也未持用户级咨询锁,只依据一次无锁读取判断。并发调减时多个请求读到陈旧的 `used/reserved`,全部通过校验后 `adjustment` 被相对累加,导致 `limit + adjustment - used - reserved` 出现负值(可用额度为负)。
|
|
|
|
|
|
- 隔离复现(S3):对 `limit=100` 的账户并发发起 4 次 `-30` 调减,重复 8 轮。**基线代码 7/8 轮不变式被破坏**(available 出现负值)。
|
|
|
|
|
|
- 修复(`8657f44`):
|
|
|
|
|
|
- `admin_quota`:先 `pg_advisory_xact_lock(hashtext(user_id))`,再 `SELECT ... FROM quota_accounts ... FOR UPDATE`,随后基于加锁后的行重算 `projected`。校验同时覆盖 `used` 与 `reserved`(冻结),不只是已用。
|
|
|
|
|
|
- `finish_task_success` / `finish_task_failure` / `recover_expired_tasks`:结算/回收前对额度账户 `FOR UPDATE`,使“冻结、结算、调减”都串行化在同一把行锁上。
|
|
|
|
|
|
- 与 `create_tts_task`(已持咨询锁 + 额度行锁)保持同一加锁顺序,避免死锁。
|
|
|
|
|
|
- 修复后(`8657f44`):S3 八轮**不变式全部保持**(最坏 available=10,恒 ≥0);S4(冻结+调减并发压测)终态不变式成立、`reserved` 归零。
|
|
|
|
|
|
|
|
|
|
|
|
不变式定义(每个额度账户恒成立):`limit_snapshot + adjustment - used - reserved >= 0`。
|
|
|
|
|
|
|
|
|
|
|
|
### 1.2 场景矩阵(隔离环境,伪造上游)
|
|
|
|
|
|
|
|
|
|
|
|
被测代码:`8657f44`。每项记录:上游调用次数、任务终态、已用/冻结/可用(当前周期账户)、音频文件结果。
|
|
|
|
|
|
|
|
|
|
|
|
| 场景 | 上游调用 | 任务终态/错误码 | 已用 | 冻结 | 可用 | 音频文件 | 结论 |
|
|
|
|
|
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
|
|
|
|
| S1 成功基线 | 1 | `succeeded` | 2 | 0 | 9998 | 存在,回放/下载 200 | 通过 |
|
|
|
|
|
|
| S2 最后额度并发争用 | 1 | `codes=[202,409]`(仅 1 个冻结成功) | 3 | 0 | 2 | n/a | 通过 |
|
|
|
|
|
|
| S3 管理员并发调减额度 | 0 | 8 轮不变式保持(最坏可用 10) | 0 | 0 | 10 | n/a | 通过(修复后) |
|
|
|
|
|
|
| S4 冻结与调减并发压测 | 3 | 终态不变式成立、冻结归零 | 24 | 0 | 16 | n/a | 通过 |
|
|
|
|
|
|
| S5 非音频 | 1 | `failed / UPSTREAM_NOT_AUDIO` | 0 | 0 | 1000 | 无 | 通过 |
|
|
|
|
|
|
| S5 损坏 WAV | 1 | `failed / UPSTREAM_AUDIO_CORRUPT` | 0 | 0 | 1000 | 无 | 通过 |
|
|
|
|
|
|
| S5 超大响应 | 1 | `failed / UPSTREAM_RESPONSE_TOO_LARGE` | 0 | 0 | 1000 | 无 | 通过 |
|
|
|
|
|
|
| S5 上游 5xx | 1 | `failed / UPSTREAM_HTTP_500` | 0 | 0 | 1000 | 无 | 通过 |
|
|
|
|
|
|
| S6 跨周期结算 | 1 | 结算落到任务创建时所属(上一)周期账户 | 5(上周期) | 0 | 100(本周期未变) | n/a | 通过 |
|
|
|
|
|
|
| S7 存储失败 | 1 | `failed`(音频目录不可写) | 0 | 0 | 10000 | 无 | 通过 |
|
|
|
|
|
|
| S8 上游超时 | 1 | `failed / UPSTREAM_TIMEOUT` | 0 | 0 | 10000 | 无 | 通过 |
|
|
|
|
|
|
| S9 重启恢复-queued | 2 | `queued→succeeded`(`running` 保持,租约未到期) | 3 | 3 | 9994 | queued 任务音频存在 | 通过 |
|
|
|
|
|
|
| S10 重启恢复-running 租约过期 | 0 | `failed / WORKER_LEASE_EXPIRED`,冻结释放 | 0 | 0 | 10000 | 无 | 通过 |
|
|
|
|
|
|
|
|
|
|
|
|
要点:
|
|
|
|
|
|
- 所有失败/异常路径均**释放冻结**、**不产生残留音频文件**、**不破坏额度不变式**。
|
|
|
|
|
|
- 重启恢复:`queued` 任务在进程重启后被重新领取并结算;`running` 任务在租约过期后被回收为 `WORKER_LEASE_EXPIRED` 并释放冻结。租约 5 分钟、单 Worker、数据库锁为跨进程边界(与 Phase 3 一致)。
|
|
|
|
|
|
- S9 中 `running` 任务在重启后仍为 `running` 属预期(租约未到期,不会重复处理,待租约过期由回收逻辑兜底)。
|
|
|
|
|
|
|
|
|
|
|
|
对比基线:`9568622` 下 S3 失败(7/8 轮破坏不变式),其余场景与上表一致。**修复后 `8657f44` 全部通过。**
|
|
|
|
|
|
|
|
|
|
|
|
复现方式(隔离,秘密走环境变量):`services/api/tests/phase5_exception_matrix.py`,见 `services/api/tests/README.md`。
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 自动化验收固化
|
|
|
|
|
|
|
|
|
|
|
|
- 测试与运行时依赖版本固化:`services/api/requirements.txt`、`services/api/requirements-test.txt`(已验证版本见 `services/api/tests/README.md` 表格)。
|
|
|
|
|
|
- 隔离库准备脚本 `services/api/tests/provision_isolated_env.sh`:`SUDO_PASSWORD`、`P5_DB_PASSWORD` 等**仅经环境变量注入**,不落仓库。
|
|
|
|
|
|
- 异常矩阵脚本 `services/api/tests/phase5_exception_matrix.py`:自启动隔离 API + 伪造上游,逐项断言并输出结果表;退出码 0 表示全部不变式通过,可重复执行、可作为验收门禁。
|
|
|
|
|
|
- 已在独立环境(隔离库 + 独立音频目录 + 伪造上游)验证可重复执行,不依赖测试机手工安装的工具;浏览器测试的 Playwright 浏览器二进制仍不入库(沿用 Phase 4 约定,`PLAYWRIGHT_BROWSERS_PATH` 指向本地缓存)。
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 音频运维方案与隔离恢复演练
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 容量 / 保留 / 清理(需用户决定的参数集中列于 OPERATIONS.md §3)
|
|
|
|
|
|
|
|
|
|
|
|
- 存储:本地私有目录 `/home/flym/kaotings-audio`(`{user_id}/{task_id}.wav`),非对象存储。
|
|
|
|
|
|
- 保留期:`AUDIO_RETENTION_SECONDS`(当前默认 7 天)——**待用户确认**。
|
|
|
|
|
|
- 清理:进程启动时 `cleanup_orphan_audio` 删除“无 `available` 记录且 mtime>1h”的孤儿文件;保留期清理策略待确认后再启用,**本轮不擅自删除任何既有音频**。
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 隔离恢复演练(业务库 + CMS 库 + 音频)
|
|
|
|
|
|
|
|
|
|
|
|
在隔离库 `p5_restore_biz` / `p5_restore_cms` 与隔离音频目录执行 `pg_dump`→`pg_restore` + 音频拷贝,结果:
|
|
|
|
|
|
|
|
|
|
|
|
- 业务库行数(源 vs 恢复)全部一致:users 35、quota_accounts 34、tts_tasks 28、audio_files 26、usage_records 61、membership_grants 10。
|
|
|
|
|
|
- 额度↔流水一致性(恢复库):34 个额度账户,`used` 与 `consume` 流水 0 不符、`reserved` 与 `reserve-consume-release` 流水 0 不符。
|
|
|
|
|
|
- CMS 库恢复 14 张表:products=1、users=1、payload_migrations=1(结构完整)。
|
|
|
|
|
|
- 音频:磁盘 22 个 `.wav` 全部可被 `available` 记录引用(无孤儿);**发现 4 条 `status=available` 且任务 `succeeded` 的 `audio_files` 行在磁盘无对应文件**(源目录与恢复目录均缺失)。
|
|
|
|
|
|
- 影响:这些历史任务在“音频可用”标记下回放/下载会返回 404(优雅降级,不崩溃);属既有 DB/文件系统不一致,**非本轮引入**。
|
|
|
|
|
|
- 处置:作为待调查项记录,不阻断恢复;恢复流程本身(库 + 音频 + 额度/流水)验证通过。
|
|
|
|
|
|
|
|
|
|
|
|
结论:业务库、CMS 库、音频均可在隔离环境完整恢复,历史↔音频关联与额度数据一致(除上述 4 条既有缺失)。
|
|
|
|
|
|
|
2026-09-10 07:24:04 +00:00
|
|
|
|
### 3.3 异地备份(本阶段后续配置,已落地)
|
|
|
|
|
|
|
|
|
|
|
|
- 用户提供异地 NAS(Synology)`103.40.14.100`(SSH `52200` / rsync 守护 `50873`)。
|
|
|
|
|
|
- 采用 **rsync over SSH(52200,加密 + 密钥认证)**,不使用公网明文 rsync 守护端口;密钥 `/root/.ssh/id_ed25519_ktsbak`,公钥加入 NAS `kts_bak`。
|
|
|
|
|
|
- 脚本 `/usr/local/sbin/kaotings-backup.sh`(root):`pg_dump` 业务库 + CMS 库(每库保留 7 份)+ `rsync -a --delete` 音频目录 → `…/backup/kaotings/{db,audio}/`;systemd `kaotings-backup.timer` 每日 03:00 触发。
|
|
|
|
|
|
- 首份备份已落盘并 `pg_restore --list` 验证可恢复;详见 `OPERATIONS.md` §2。
|
|
|
|
|
|
- 仍待确认:保留份数/周期、磁盘告警阈值、责任人(`OPERATIONS.md` §5)。
|
|
|
|
|
|
|
2026-09-09 21:01:27 +00:00
|
|
|
|
## 4. 上游安全改造(上线阻断,待外部条件)
|
|
|
|
|
|
|
|
|
|
|
|
- 现状:TTS 上游为公网 **HTTP**(`TTS_UPSTREAM_URL`),Bearer Key 走明文传输。这是正式上线阻断项。
|
|
|
|
|
|
- 上线前必须满足(具体实施条件):
|
|
|
|
|
|
1. **传输加密**:上游提供 HTTPS(受信任/可内部校验的证书),或受控加密通道(如内网 VPN/专线、mTLS)。
|
|
|
|
|
|
2. **来源限制**:上游侧限制仅本站服务器出口 IP/网段可访问(防火墙/安全组/WAF 白名单),并在业务侧保留 Bearer Key。
|
|
|
|
|
|
3. **外部网络复测**:加密与来源限制生效后,重新执行成功生成、音频校验、回放/下载、超时/失败矩阵与外部不可达性核查。
|
|
|
|
|
|
- 需要上游负责人提供:
|
|
|
|
|
|
- HTTPS 端点与证书(或 mTLS/专线接入方式、允许的来源网段)。
|
|
|
|
|
|
- 凭据注入名称、有效期、轮换方式(密钥仍只进 `/etc/kaotings/api.env`,不进 Git/前端/日志)。
|
|
|
|
|
|
- 允许的来源机器/IP 段与网络方式,便于双方配置来源限制。
|
|
|
|
|
|
- 边界:**不擅自修改上游服务**;本轮仅记录条件与所需输入,未改动任何上游配置。
|
|
|
|
|
|
|
|
|
|
|
|
## 5. 剩余小项
|
|
|
|
|
|
|
|
|
|
|
|
- **按 slug 的 REST 路由**:不新增。公网产品展示统一走 `where[status][equals]=published` 列表 + `where[slug][equals]=` 筛选(已验证 200)。
|
|
|
|
|
|
- **非法 ID 导致 500**:`GET /cms/api/products/:slug`(Payload 将 `:slug` 按 `id` 处理)传入非 UUID 返回 500,响应体为**通用** `{"errors":[{"message":"Something went wrong."}]}`,**未泄露堆栈、源码路径或内部信息**(已扫描确认)。作为 Payload 行为记录在案,公网展示闭环不受影响。
|
|
|
|
|
|
- **SMTP 未接入**:维持为 **V1 已声明限制**(验证渠道返回未启用)。管理员密码恢复依赖本地受控流程,见 `docs/cms-admin-recovery.md`;本轮不接 SMTP。
|
|
|
|
|
|
|
|
|
|
|
|
## 6. 部署与版本核对
|
|
|
|
|
|
|
|
|
|
|
|
- 修复提交 `8657f44` → 发布目录 `/home/flym/releases/8657f44`(自 `p4` 完整拷贝后更新 API)。
|
|
|
|
|
|
- 软链 `kaotings-api`→`…/8657f44/services/api`(web/cms 同步指向 `8657f44`,仅重启了发生代码变更的 `kaotings-api`)。
|
|
|
|
|
|
- 版本核对:线上 `services/api/app/main.py` SHA256 = `571037f4…`,与本地 `8657f44` 完全一致;`systemctl is-active kaotings-api` = `active`;`GET /healthz` = `{"status":"ok","database":"ok","service":"api"}`。
|
|
|
|
|
|
- 回滚:软链指回 `…/p4/services/api` 并重启 `kaotings-api` 即可(`p4` 保持完好)。
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 未通过 / 待外部条件项(不得标为完成)
|
|
|
|
|
|
|
|
|
|
|
|
- **上线阻断**:公网 HTTP 上游未加密、无来源限制(§4),待上游提供条件并外部复测。
|
|
|
|
|
|
- **既有数据发现**:4 条 `available` 音频行无对应文件(§3.2),待调查根因;恢复流程本身通过。
|
|
|
|
|
|
- 旧 Big-TTS Web 退役前,仍需旧系统负责人提供源端备份证明与替代验收确认(沿用 Phase 3/4 边界)。
|