Files
geMoldInsight/docs/topics/performance/OCC_THROUGHPUT.md
T
cjw e728dcd226 批次4后续专项完成:D11清偿 + OCC方案B实施 + 部署参数 + D2诚实标注 + CI门禁
① 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>
2026-09-18 17:22:01 +08:00

9.1 KiB
Raw Blame History

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 落盘端到端。