① D11 HTML 报告 RustFS 单源化(TECH_DEBT P2 清偿):可视化产物写任务临时目录后
裸传报告键 html/reports/{filename}(文件名寻址),/html StaticFiles 挂载删除,
新增 html_report_router 根路径代理(报告键→遗留 JSON 包装→本地卷兜底→404,
防穿越);URL 形状 /html/{filename} 不变,持久化引用零迁移;celery 摘除
html_data 卷,镜像不再烤入陈旧报告;顺带删除 get_stp_file_with_data 死数据块
② OCC 方案 B(D10 清偿):run_occ(op_name, payload) 契约 + 常驻工作进程池
(occ_process_pool + occ_worker 操作注册表),超时/崩溃 terminate 换新补位、
任务级超时 recover 整体重建,残留线程泄漏根治;TopoDS 不跨进程(generate_cavity
分模 + 方案 STEP 持久化全在子进程内,返回 export_manifest);删除内存形状缓存链、
CADExporter.export_mold_results、shape_loader(→ stp_materializer)
③ OCC 方案 A 部署参数:CELERY_CONCURRENCY / CELERY_MAX_TASKS_PER_CHILD 进
Dockerfile.celery + compose + .env.example
④ D2 诚实标注:铝价响应带 source: "simulated",前端按来源渲染标注(原硬编码
"上海期货交易所"属虚假声明),死代码 getAluminumPrice 删除
⑤ CI 门禁:.gitea/workflows/ci.yml 三 job(pytest / 前端构建含 vue-tsc /
openapi 漂移检测)
接口变更三件套随批完成(openapi 76→77 paths + gen:api + 前端构建通过;方案 B
接口面零变化)。测试基线 143 passed, 0 skipped(新增 16 项)。文档六处同步。
Co-Authored-By: Claude Code <noreply@anthropic.com>
9.1 KiB
OCC 处理吞吐与隔离方案设计(OCC_THROUGHPUT)
文档定位:OCC(PythonOCC)处理吞吐与故障隔离的专题设计文档。 本文回答"OCC 串行瓶颈与超时线程泄漏的根治路线";现状事实以 ../../../STATUS.md 为准,债务归属 ../../TECH_DEBT.md D10。 2026-09-17 随批次 4 产出:方案设计先行,短期项(方案 A)零代码可用,中期的接口演进与进程池实施留待后续批次。
1. 现状与硬约束
1.1 运行时事实
- 所有 OCC 操作(STEP 解析 / 布尔运算 / 三角化 / 倒扣检测 / STEP 转换等)统一经 processing_service.py 的
run_occ(op_name, payload)投入 常驻 OCC 工作进程池(occ_process_pool.py,默认 1 进程 = 1 串行通道)——OCC 非线程安全,通道内串行是正确性要求,不是实现偷懒。 - 操作在 occ_worker.py 以注册表形式实现(parse_stp / generate_mesh / generate_cavity / analyze_mold_design / detect_undercuts / convert_component_step 等);输入输出全部是文件路径 + 普通字典,TopoDS_Shape 不跨进程传输(方案 B 硬约束,见 §1.2)。
- Celery worker 为 prefork 模式,
processing_service是模块级单例:每个 worker 子进程各持一个常驻 OCC 工作进程。因此 OCC 并行度 = worker 子进程数,与 web 进程数无关(web 侧run_occ仅服务于轻量同步调用,如倒扣检测)。 - 超时/崩溃 =
terminate()该工作进程并换新补位——进程边界干净回收,无线程滞留。
1.2 硬约束(决定方案边界)
| 约束 | 含义 |
|---|---|
| OCC 非线程安全 | 任何方案中,一个进程内 OCC 操作必须串行;并行只能靠多进程 |
| C++ 栈不可中断 | 线程级超时只能"抛弃"不能"击杀";只有进程级 kill 是干净的故障恢复 |
run_occ(fn, *args) 传闭包/绑定方法 |
函数对象不可跨进程 pickle——进程化方案必须改接口为"操作名 + 可序列化参数" |
| STEP 重载成本 | 进程间不共享 OCC 形状对象;跨进程方案每次调用需重新读文件/传 BRep(几秒级) |
2. 方案对比
方案 A:Celery prefork 并行伸缩(短期,零新代码)
做法:承认"每子进程一个串行 OCC 通道"的既有事实,把 OCC 吞吐问题转化为 worker 进程数问题:celery -A celery_app worker --concurrency=N,N = 期望的并行分析数(受 CPU 核数与每进程内存约束)。
- 优点:零代码改动;进程边界天然兜住线程泄漏——泄漏线程随子进程存亡,配合
--max-tasks-per-child=M(子进程处理 M 个任务后重启回收)可把滞留线程的存续时间限制在一个批次内。 - 代价:每个子进程常驻完整 Python + OCC 运行时(数百 MB),N 不能无脑调大;DB 连接按 celery 角色池(pool_size=5)随子进程倍增,PG
max_connections需要相应预算。 - 不解决:单任务超时后该子进程内的线程滞留(被 max-tasks-per-child 兜底回收);单任务无加速(串行本质不变)。
结论:立即可用的推荐做法。部署侧调整(concurrency / max-tasks-per-child)随下次镜像与 compose 评审落地,先在 OPERATIONS.md 记录启动参数建议。
方案 B:常驻 OCC 进程池 + kill-on-timeout(中期,推荐演进方向)
做法:在 run_occ 接口之下替换执行器——不再是 ThreadPoolExecutor,而是常驻的单线程 OCC 工作进程池(每进程一个事件循环:接任务 → 执行 → 回报)。超时由主进程 terminate() 工作进程并更换新进程补位。
- 接口演进:
run_occ(fn, *args)→run_occ(op_name: str, payload: dict),操作名注册表映射到模块级函数(STEP 文件路径进、JSON/BRep 文件出,杜绝 pickle 大对象);各调用点(解析、型腔、倒扣、导出三角化……)逐一迁移。 - 优点:超时 = 杀进程,故障恢复干净彻底(D10 残留泄漏根治);OCC 崩溃(segfault)不再波及 API/worker 主进程;进程池大小与 celery 并发解耦。
- 代价:一次明确的接口迁移(所有
run_occ调用点 + 结果序列化);进程池自管理(补位、健康检查、启动预热——spawn 下 import OCC 秒级,需常驻而非按任务拉起);跨进程只传文件路径 + JSON,现有"传形状对象"的内部调用要改为落盘中转。 - 风险:自建进程池的运维复杂度;Windows 开发环境 spawn 语义与 Linux fork 差异需测试覆盖。
方案 C:OCC sidecar 服务(长期,视伸缩需求)
做法:OCC 能力独立成进程/容器(HTTP 或 gRPC),API 与 worker 都是客户端;STEP 按路径/对象键传入,返回 JSON 摘要 + 产物对象键。
- 优点:隔离最彻底;OCC 可独立伸缩、独立发布、独立扩容 GPU/内存型节点;多语言可复用。
- 代价:新增一个部署单元与序列化边界(大网格/形状数据传输设计);超出当前"单 compose 栈"的部署叙事,需与 DEPLOYMENT 文档体系一起演进。
结论:除非出现独立伸缩/隔离性硬需求,暂不启动。
3. 决策与路线
| 阶段 | 动作 | 状态 |
|---|---|---|
| 短期 | 方案 A:--concurrency 伸缩 + --max-tasks-per-child 兜底回收;cancel_futures=True 修复重建并发风险 |
✅ 部署参数 2026-09-18 落地(CELERY_CONCURRENCY / CELERY_MAX_TASKS_PER_CHILD 进 Dockerfile.celery + compose + .env.example) |
| 中期 | 方案 B:run_occ(op_name, payload) 接口演进 + 常驻进程池,kill-on-timeout 根治泄漏 |
✅ 2026-09-18 实施完成(见 §5;回归测试 tests/test_occ_process_pool.py) |
| 长期 | 方案 C:sidecar,仅在出现独立伸缩需求时启动 | 暂不启动 |
4. 已落地的缓解(2026-09-17,批次 4)
_reset_occ_executor 的 shutdown(wait=False) 补 cancel_futures=True。这不只是卫生问题:旧实现下旧 executor 的排队任务不会消失——若挂死线程后来"慢恢复",旧线程会继续消化旧队列,与新 executor 并发操作非线程安全的 OCC(数据竞争 / 崩溃风险)。补参后排队任务即被丢弃,残留问题收敛为"运行中线程滞留 1 个",由方案 A 的进程回收兜底。
5. 方案 B 实施记录(2026-09-18)
接口:run_occ(fn, *args) → run_occ(op_name, payload);执行器由进程内线程池替换为常驻进程池。
- 新增 occ_process_pool.py:
OccProcessPool(默认 size=1)。每个_OccWorker是一个 spawn 出的常驻子进程 + 双工管道 + 独立asyncio.Lock(通道串行);阻塞收发经asyncio.to_thread不卡事件循环。操作超时或进程死亡 →terminate()+ 换新补位;任务级整体超时(process_file_with_storage外层 wait_for)→recover()整体重建。shutdown()供应用退出/测试清理。 - 新增 occ_worker.py:操作注册表 +
worker_main消息循环。OCC 模块在 handler 内惰性导入(父进程 pip 环境无 OCC 也可 import),进程内单例缓存(parser/planner/analyzer/mesh_gen 等)。全部操作输入输出为文件路径 + 普通字典,TopoDS 不跨进程。 - 调用点迁移(processing_service):
parse_stp= 原 load_step_file + analyze_geometry 两步合一(形状在子进程内即生即用)generate_mesh/analyze_mold_design/detect_undercuts/convert_component_step同名对位generate_cavity= 分模 + 方案形状持久化 STEP 导出全在子进程内;_export_shapes(TopoDS)不再回主进程,返回 export_manifest(与旧_persist_step_exports结构一致,主进程原样存 export_artifacts)- 旧
_cache_export_shapes/get_export_shapes/_persist_step_exports/_export_shapes_cache及线程 executor /_reset_occ_executor全部删除(跨进程本就不存在内存形状缓存,export_router 相应移除export_mold_results内存分支) - shape_loader.py → stp_materializer.py:只把 STP 原件落盘临时文件,OCC 解析交给子进程操作
- 成本确认:每个 spawn 子进程首次操作需 import OCC(秒级);进程常驻后后续操作复用缓存实例。每个操作从 STP 原件重新加载形状(STEP 重载成本,见 §1.2)——原线程方案跨步骤共享 shape 的内存优势让位于进程隔离,符合方案 B 设计取舍。
- 测试:tests/test_occ_process_pool.py(OCC-gated,6 例):spawn+管道往返 / 未知操作错误回传 / 子进程异常浮出 / 超时换新补位 / 真实盒体 STP 解析端到端 / generate_cavity 分模+STEP 落盘端到端。