Files
MoviePilot/docs/v3t-runtime-governance.md
T

245 lines
18 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.
# MoviePilot V3t 运行时治理
> 状态:持续维护。本文定义 `moviepilot-v3` 与 `moviepilot-v3t` 的产品边界、运行依赖分层、
> 故障恢复合同和兼容项退场门禁。具体版本以 `pyproject.toml` 与 `uv.lock` 为准。
## 1. 治理目标
MoviePilot 同一版本提供两套镜像:
- `moviepilot-v3` 使用标准 CPython 3.14,是默认稳定镜像和故障回滚基线。
- `moviepilot-v3t` 使用 CPython 3.14 free-threaded 构建,用于获得多线程 CPU 并行能力;
Rust 能力固定启用,不能在运行时关闭。
两套镜像必须来自相同源码 revision、使用相同产品版本和同一份 `uv.lock`。V3t 不替换默认镜像,
也不在业务代码中维护一套平行实现;解释器、原生 ABI 或插件不兼容时,应切回同版本标准 V3。
正式发布按同版本制品对验收和晋升 `latest`;任一变体未通过构建、扫描或发布时,本次版本不移动
两边的 `latest`。这不会回退已经发布的标准 V3,且避免两个 `latest` 指向不同源码版本。
V3t 的目标不是让所有请求都更快。它主要改善可并行的 Python CPU 热点,同时验证主程序、原生扩展
和插件生态在 free-threaded 解释器下的正确性。启动、内存、普通 API 和数据库路径不得为获得局部
并发收益而出现不可接受的退化。
## 2. 单一依赖事实源
`pyproject.toml` 中的 `project.dependencies` 是两套镜像共享的运行依赖,ABI 敏感依赖放在两个互斥组:
- `runtime-standard`
- `runtime-free-threaded`
`tool.uv.conflicts` 保证两个组不能同时解析。Docker 的两个构建 stage 分别固定选择对应组;源码升级、
启动恢复和插件安装后的宿主恢复由 `app.runtime.dependencies.runtime_dependency_group()` 根据当前
解释器的 `Py_GIL_DISABLED` 构建标志选择 profile。
依赖选择不得改为镜像标签判断,也不得在业务模块中按包名散落 V3/V3t 分支。Dockerfile 只选择运行
profile,依赖名称、版本和 source 语义全部由 `pyproject.toml``uv.lock` 管理。
## 3. 当前原生依赖矩阵
下表中的版本用于解释当前分叉原因,不代替锁文件。版本变化后应同步更新本表的治理状态。
| 能力 | 标准 V3 | V3t | 当前处理与上游解除条件 |
| --- | --- | --- | --- |
| Python | CPython 3.14 | CPython 3.14t | 跟随同一 Python 3.14 patch 版本;升级后必须重新验证解释器 ABI、GIL 状态、启动与完整测试。 |
| `moviepilot-rust` | `cp314-abi3` | `cp314t` | 同一包版本按 wheel tag 选择;两套 ABI 和 V2 使用的 `cp311-abi3` 必须在发布链中保持独立可用。 |
| `bcrypt` | 4.x | 5.x | V3t 使用提供 free-threaded 制品的版本;当同一稳定版本同时满足两套 ABI 与密码合同后可合并约束。 |
| Brotli | 1.2.0 wheel | 同版本固定源码构建 | V3t 当前只对该 profile 使用固定上游源码。上游提供可复现的稳定 `cp314t` wheel 后,验证导入、压缩结果、并发与 GIL,再删除 source 覆盖。 |
| CRC 加速 | `crcmod-plus` 2.3.1 | `crcmod-plus` 2.3.1 | 两套镜像统一使用继续维护且兼容 `crcmod` 导入接口的实现。`oss2` 的陈旧元数据仍声明不再维护的 `crcmod`,由 uv 在解析时排除该传递依赖;宿主不在运行时映射、卸载或替插件兼容旧分发包。 |
| `lxml` | 6.1.2 | 7.0.0b1 | V3t 暂用提供目标 ABI 的预发布版本,是当前最高风险项。稳定版提供 `cp314t` wheel 后,需通过 XML、HTML、RSS、站点解析、并发和内存验证再替换。 |
| PostgreSQL 同步驱动 | `psycopg2-binary` 2.x | `psycopg[c]` 3.3.4 | 当前分叉同时受 ABI 与实测性能影响,不要求仅为版本统一而收敛。若上游能力或性能变化,必须重跑三方案 PostgreSQL A/B 后再决策。异步路径继续使用 `asyncpg`。 |
| `orjson` | 3.12.0 wheel | 同版本源码构建 | V3t 使用同一锁定版本并启用 free-threaded 构建变量。上游发布覆盖 Linux amd64/arm64 的稳定 `cp314t` wheel 后可删除本地构建要求。 |
| 中文转换 | `zhconv-rs` | `moviepilot-rust.zhconv_fast()` | 主程序统一经 `app.foundation.text.convert()`,插件统一经 SDK;不得让调用方感知后端差异。只有语义、性能、体积和 ABI 均更优时才考虑统一实现。 |
| 站点资源 | `cpython-314` | `cpython-314t` | 资源文件必须按解释器 ABI 独立构建和选取,不能让 V3t 复用普通 CPython 扩展,也不能影响 V2 的历史 ABI 制品。 |
表中差异不是全部都要消除。只有临时 source 构建、预发布依赖或第三方元数据兼容适合在上游成熟后
优先退场;已由性能与产品边界证明合理的驱动或实现选择,可以继续由 profile 集中管理。
## 4. 依赖检查与自愈合同
“自愈”包含两个不同的恢复边界,不能与普通依赖校验混为一谈。
### 4.1 启动前恢复
容器启动前先导入一组后端核心依赖。导入成功时不运行 uv 同步;导入失败时才选择可用依赖源,并对
`/app` 执行锁定的项目同步。同步命令必须:
- 使用当前虚拟环境中的解释器推导 runtime profile
- 使用 `--locked`,禁止启动时重新求解未锁定版本;
- 使用 `--inexact`,保留共享环境中的插件额外依赖;
- 只恢复主项目运行依赖,不安装项目本身或开发依赖;
- 恢复后再次执行核心导入探针,仍失败则停止后端启动并保留诊断入口。
源码更新事务中的依赖同步和失败回滚使用同一 profile 选择入口。否则 V3t 在恢复时可能被普通默认组
覆盖,得到“3.14t 解释器 + 标准原生依赖”的无效组合。
### 4.2 插件安装后的宿主恢复
插件与主程序共享虚拟环境。插件依赖安装前后都会采集宿主健康快照,只对安装后新增的异常执行补偿:
1. 优先使用安装前生成的主程序保护约束恢复被修改的包;
2. 约束不可用时,按主项目 `pyproject.toml``uv.lock` 和当前 runtime profile 恢复;
3. 恢复完成后重新执行依赖诊断与核心能力探针;
4. 宿主即使恢复成功,本次插件安装仍返回失败,不能把被回滚的安装报告为成功。
依赖诊断使用 `uv pip check`,项目在 `tool.uv.exclude-dependencies` 中明确排除的传递依赖不进入健康
异常集合,其余诊断按安装前后的稳定错误集合识别新增问题;这样既不会反复报告 `oss2` 对旧 `crcmod`
的陈旧元数据,也不会让既有告警遮蔽其他新增错误。核心能力
探针统一由 `app.doctor.dependencies` 执行。普通启动使用轻量模式验证 Web 栈、中文分词与转换;
镜像构建及插件安装前后使用完整模式,继续验证 ABI 敏感原生扩展、CRC C 实现、PostgreSQL C 实现及
导入后的 GIL 状态。插件允许的非核心依赖升级不要求与宿主锁文件逐版本相同,因此插件健康检查不得
用整份 `uv.lock` 强制回滚共享环境。
自愈是异常补偿,不是每次启动的常态安装流程。任何新增恢复入口都必须复用统一 profile 选择函数,
不得自己拼装默认 uv group。
## 5. GIL 与原生扩展可观测性
构建阶段必须导入并执行已知核心原生依赖,验证 V3t 在导入前、导入后和热点执行后均保持 GIL 关闭。
运行阶段还必须保留动态观测,因为构建探针无法穷举插件延迟导入的第三方扩展:
- 启动收尾日志记录解释器类型和实际 GIL 状态;
- 登录后全局设置与系统环境 API 返回 `PYTHON_FREE_THREADED``PYTHON_GIL_ENABLED`,系统环境 API
同时返回 `RUST_ACCEL_REQUIRED`
- 插件安装或重载导致 GIL 从关闭变为开启时,日志记录对应插件归因;
- 前端在 V3t 实际启用 GIL 时显示兼容告警,正常状态不误报。
不得通过 `PYTHON_GIL=0` 或等效方式强制绕过扩展的 GIL 声明。扩展一旦使进程退化为 GIL 模式,
应保留可观测性并修复或替换依赖;无法及时处理时回退标准 V3。
## 6. 当前 A/B 基线与决策
以下数据来自同一源码 revision、相同资源限制的 arm64 候选镜像,用于确定当前架构和依赖策略。后续
上游重放、依赖更新、构建链变化或采集 schema 扩充都会使其失去“最终发布验收”资格,但不抹去已经
建立的设计结论;新的发布候选必须重跑并在本节追加或替换相应数据。
### 6.1 镜像与解释器基线
| 指标 | 标准 V3 | V3t | 结论 |
| --- | ---: | ---: | --- |
| arm64 镜像体积 | 647.5 MiB | 674.0 MiB | V3t 增加 26.5 MiB,约 4.1%;标准镜像仍处于既有 660+ MB 发布口径。 |
| 启动就绪中位数 | 6.177 s | 5.608 s | V3t 约快 9.2%,未造成启动退化;不据此承诺所有部署都会更快。 |
| 空载 working set | 274.9 MiB | 336.8 MiB | V3t 增加约 22.6%,低于 25% 验收阈值。 |
| 主工作负载 PSS | 296.4 MiB | 356.3 MiB | V3t 增加约 20.2%,属于实验镜像的明确资源成本。 |
| 32 线程纯 Python CPU 探针 | 13,143.7 ops/s | 42,583.5 ops/s | V3t 约为 3.24 倍,证明解释器并行收益;该探针不经过 Rust 开关。 |
| 32 线程直接 Rust ABI 探针 | 40,832.4 ops/s | 61,550.9 ops/s | V3t 约为 1.51 倍;两边都直接调用 Rust,仅验证原生 ABI 与并发。 |
| 普通 API 最大 p95 比例 | 基线 | 1.10x | 四个本地 API 中最大退化来自订阅列表,仍低于 1.25x 门禁。 |
同 revision 的 amd64 最终候选也已完成交叉架构验收。标准 V3 与 V3t 的本地镜像体积分别为
680.6 MiB 和 714.0 MiBV3t 增加 33.4 MiB、约 4.9%。两者均使用 Python 3.14.7,标准 V3
保持 GIL 启用;V3t 使用 free-threaded 解释器,完整原生依赖、`psycopg` C 实现、Rust 文本能力及
`sites.cpython-314t-x86_64-linux-gnu.so` 依次加载后 GIL 始终关闭。两个镜像均以空白配置启动到
`/health/ready` 且 Docker health 为 healthy。该启动运行在 arm64 宿主的 amd64 模拟环境中,只作为
发布候选功能与 ABI 验收,不纳入性能比例。
该候选还通过了四分片全量回归(5,981 passed、3 skipped、14 subtests passed)、PostgreSQL 18.6
迁移/提交/回滚、现代插件清单、历史 `requirements.txt`、插件源码与依赖恢复以及恢复后单实例加载。
这些结果证明双 profile 方案可行;最终发布仍必须使用最新 revision 和正式不可变 digest 重跑。
### 6.2 应用热点三组
真正受产品 Rust 开关影响的应用识别热点必须分成三个对象:
| 运行方式 | 解释器 | 应用实现 | 产品语义 |
| --- | --- | --- | --- |
| V3 + Python | 标准 CPython | Python fallback | 标准镜像关闭 `RUST_ACCEL` 的兼容基线。 |
| V3 + Rust | 标准 CPython | `moviepilot-rust` | 标准镜像启用 `RUST_ACCEL` 的默认性能路径。 |
| V3t + Rust | free-threaded CPython | `moviepilot-rust` | V3t 固定路径,Rust 不允许关闭。 |
当前六个交替样本的中位耗时分别为 `0.068769s``0.035339s``0.035511s`,三组业务结果校验和
一致。标准 V3 启用 Rust 后耗时约为 Python fallback 的 `0.514x`;V3t 固定 Rust 路径约为标准 V3
Rust 路径的 `1.005x`,即慢约 0.5%,仍低于 1.25x 门禁。解释器探针的并发收益不能抵销产品路径
退化,因此两组比例必须继续独立验收。
### 6.3 PostgreSQL 三方案
三组使用同一 PostgreSQL 18.6、相同 SQL 和六轮全排列顺序,以标准 V3 + `psycopg2` 为 1.00 倍基线:
| 方案 | 单连接查询吞吐 | 16 线程查询吞吐 | 批量事务写耗时 |
| --- | ---: | ---: | ---: |
| V3 + `psycopg2` | 1.00x | 1.00x | 1.00x |
| V3 + `psycopg3 binary` | 0.946x | 0.367x | 0.094x |
| V3t + `psycopg3 C` | 0.931x | 1.258x | 0.096x |
因此标准 V3 保留 `psycopg2`V3t 为满足 free-threaded ABI 使用 `psycopg3 C``psycopg3` 的批量
事务写明显更快,但查询吞吐没有形成全面优势;当前不为统一驱动扩大数据库重构。18 个当前镜像样本
均通过驱动实现、GIL、SQL 结果、长事务并行和样本完整性门禁。三种制品内嵌的 libpq 分别为 17.9、
18.0 和 18.6,性能差异不能全部归因于 Python 驱动。未来驱动或应用访问模式变化
时,必须重跑真实短事务、批量写、连接池、长事务和迁移场景,不能只用单项微基准改写结论。
### 6.4 环境检查成本
`uv sync --check --locked --offline --inexact` 适合验证镜像构建、开发环境和锁定项目能否复现,
不用于覆盖插件允许的非核心依赖升级。真正恢复可能访问包索引,耗时仍受缺失制品、缓存命中、网络和
原生构建影响,不能用检查耗时推断用户故障恢复时长。
当前公共 A/B schema 2 已记录完整安装包集合及哈希、全部原生 distribution 和 wheel tag、导入前后
GIL、working set、RSS/PSS/USS、API p50/p95、SQLite 同步/异步结果和 1/8/16/32 线程热点。两套镜像
各安装 174 个包;V3t 的 16 个核心原生模块在同一进程依次导入后,GIL 始终保持关闭。
### 6.5 冷启动、插件恢复与宿主自愈
空白配置卷首次启动需要下载 CloakBrowser 内核。该场景可能接近用户报告的“两分钟启动”,主要瓶颈
是持久浏览器缓存未命中和下载网络,不是插件初始化;缓存命中后的普通重启不重复下载。
现代 `pyproject.toml` 插件(`boltons==25.0.0`)与历史 `requirements.txt` 插件
`humanize==4.15.0`)均已在 V3t 真实容器中验收。首次运行从快照恢复两个源码目录耗时 1.60ms,
联网安装依赖及激活耗时 4.64s;优雅关停后使用同一不可变镜像和持久 `/config` 断网重建,源码恢复
耗时 1.95ms,仅从持久 uv 缓存恢复依赖及激活耗时 1.56s。两轮各插件都只产生一条实例初始化记录,
最终 API 状态均为 `active`、无 pending/failed,且没有启用 GIL。该结果证明插件源码恢复不应被依赖
下载阻塞,依赖未就绪的插件可以在 Web 可用后渐进加载。
删除一个主程序核心依赖后重启,启动前探针能触发锁定自愈,`--inexact` 保留插件额外依赖;恢复后
插件仍为单实例 `active`,V3t GIL 仍关闭。该路径的价值是让受污染共享环境能够恢复启动,不是性能
优化;它只在核心导入失败时执行,正常重启不会承担这段解析成本。
## 7. 上游能力的渐进式接入
第三方上游发布新版本或新 wheel 后,按以下顺序处理,不能看到文件名包含 `cp314t` 就直接删除本地兼容:
1. **确认制品**:稳定版本覆盖 Linux amd64/arm64wheel tag 与解释器 ABI 匹配;源码构建路线还要固定
可审计的源版本和构建工具链。
2. **建立候选锁**:只修改对应 runtime group 或 source,更新单一 `uv.lock`,确认标准与 free-threaded
两套 profile 均可 `uv sync --locked` 重建。
3. **验证原生合同**:导入、核心功能、错误边界、首次初始化和 1/8/16/32 线程并发结果一致;V3t
在每个阶段保持 GIL 关闭。
4. **验证恢复链**:覆盖启动前恢复、源码更新与回滚、现代插件清单、历史 `requirements.txt` 和插件
安装后宿主恢复,确认 profile 不串组且插件额外依赖不被裁剪。
5. **执行同 revision A/B**:先证明标准 V3 修改前后无可重复退化,再比较同一 revision、同一资源限制
和不可变镜像 digest 的 V3/V3t。至少保留镜像体积、启动、RSS/PSS/USS、API p50/p95、SQLite、
PostgreSQL 和 CPU 热点原始样本。
6. **完成双架构验收**amd64、arm64 的构建、漏洞扫描、启动和核心功能均通过后,才发布候选制品。
7. **删除临时处理**:同一变更中移除不再需要的 source、版本分叉或排除规则,并更新本表、锁文件、
构建探针和回归测试;不要永久保留失效的兼容分支。
公共 A/B 基线为 `scripts/perf/free_threaded_ab.py`,使用方法和输出合同见
[`scripts/perf/README.md`](../scripts/perf/README.md)。正式验收原始数据应按发布候选独立留存,PR 只提交
可复用脚本和维护者可判断的汇总结论。
## 8. 插件生态边界
- 插件不得直接依赖 V3/V3t 的内部实现选择,应使用 `app.sdk` 暴露的文本、网络和运行时能力。
- 插件直接导入 `zhconv_rs` 等标准 V3 专属包时,V3t 可以明确判定为不兼容;宿主不伪造第三方模块。
- 插件若自行声明不再维护的 `crcmod`,必须由插件迁移到 `crcmod-plus`;宿主不映射分发包、不增加插件
特判,也不在运行期卸载插件声明的依赖。
- 插件携带原生 wheel 时必须匹配当前解释器和平台 ABI。缺少 `cp314``cp314t` 制品属于插件依赖
兼容问题,不通过宿主插件 ID 特判绕过。
- 插件安装后若使 V3t 重新启用 GIL,功能可能仍可运行,但该进程已经失去 free-threaded 产品语义,
必须告警并引导用户升级插件、移除依赖或切回标准 V3。
V3 `3.0.0` 的依赖集合随镜像交付。相同版本 Tag 的 `release` 自动更新只执行版本比较,不下载源码或
同步依赖;用户必须拉取并重建容器后才会使用新镜像内的锁定环境。`/app``/opt/venv` 不属于标准
持久化卷,非标准挂载和 `dev` 自动更新不属于正式镜像迁移合同。
## 9. 变更检查表
修改 Python patch 版本、runtime group、原生依赖、Rust 制品、资源 ABI 或双镜像构建链时,至少确认:
- [ ] `pyproject.toml``uv.lock` 同步,两个互斥 profile 均能锁定重建。
- [ ] 标准 V3 依赖集合、性能、体积和功能没有因 V3t 改动退化。
- [ ] V3t 核心原生扩展导入及并发执行后 GIL 仍关闭。
- [ ] amd64、arm64 的 Python、Rust 和站点资源 ABI 匹配。
- [ ] SQLite、PostgreSQL、插件安装/恢复、源码升级/回滚和启动自愈通过。
- [ ] 动态 GIL API、启动日志、插件归因和前端告警保持一致。
- [ ] 安全扫描结果已按实际安装制品、利用面和上游修复状态审计。
- [ ] 上游已成熟的临时兼容已删除,仍保留的例外在本文有明确解除条件。