Files
geMoldInsight/docs/topics/performance/OCC_THROUGHPUT.md
T
cjw 0e6b3b1811 后端设计治理:批次 0-4 全部完成(安全/部署/一致性/结构/架构)
按 ROADMAP §3.1 治理批次推进的后端设计审查整改:

- 批次 0(安全):/api/status/{task_id} 补 JWT 鉴权与任务归属校验;
  pythonocc_available 真实探测;bcrypt 超 72 字节显式拒绝;
  SECRET_KEY/RUSTFS_* 惰性校验,代码侧弱默认移除
- 批次 1(部署正确性):主处理链路改走 RustFS(分派入参 stp_file_id 化,
  worker 按 object_key 下载);AUTO_MIGRATE 开关 + 迁移目录 alembic/→migrations/
  修复包遮蔽(自动迁移此前从未真正生效);OCC 镜像改 conda 原生执行 +
  基础镜像 tag 锁定;compose 关键项改 ${VAR:?} 强制显式配置
- 批次 2(任务一致性):删除 Redis 进程内存回退,PG 为任务状态单一事实源;
  批量元数据入库(processing_tasks.batch_id,迁移 a3f8c2d91e47);
  型腔失败任务标 failed 不再静默 completed;事务边界收口
  (数据本体写 flush-only、失败先回滚再置 failed、进度更新保留即时 commit)
- 批次 3(API 与代码结构):592 行 advanced_router 拆为 design/cost/machining/
  export 四子路由,请求体全量 Pydantic 化;ROUTE_MODULES + route_registry
  (/api/health 呈现 degraded,DEBUG fail fast);纯计算端点统一 to_thread;
  StorageIntegrationService 按职责三拆;MAX_FILE_SIZE 接线生效、
  celery 复用 Settings.redis_url;管理员重置密码改 JSON body(端到端断裂修复);
  openapi.json 重导出(76 paths)+ 前端 gen:api
- 批次 4(架构演进):共享 ORM 按模块拆分(shared/models/base.py + identity.py、
  moldinsight/models/、inventory/models/,删除三条无使用方的跨模块
  relationship,跨模块桥接收敛为裸 FK 硬规则,无兼容 facade);
  OCC executor 重建补 cancel_futures=True(消除旧队列被慢恢复线程
  并行消化的数据竞争);OCC 吞吐方案设计先行
  (docs/topics/performance/OCC_THROUGHPUT.md);顺手清偿 D15
  (vite.config.ts 未用参数致 npm run build 失败)

测试基线:125 passed, 2 skipped(pytest + sqlite+aiosqlite;归属边界、
路由契约、配置治理、鉴权回归等随批新增)
文档同步:STATUS / TECH_DEBT / ROADMAP / ARCHITECTURE / API_CONTRACT /
OPERATIONS / AGENTS

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 16:15:49 +08:00

6.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 解析 / 布尔运算 / 三角化 / 倒扣检测等)统一经 processing_service.py 的 run_occ 投入 进程内 ThreadPoolExecutor(max_workers=1) 串行执行——OCC 非线程安全,串行是正确性要求,不是实现偷懒。
  • Celery worker 为 prefork 模式,processing_service 是模块级单例:每个 worker 子进程各持一个串行 OCC 通道。因此 OCC 并行度 = worker 子进程数,与 web 进程数无关(web 侧 run_occ 仅服务于轻量同步调用,如倒扣检测)。
  • 型腔生成超时后 _reset_occ_executor 重建 executor;已在运行的 C++ 线程在 Python 层不可杀,每次超时滞留 1 个线程(2026-09-17 起排队任务随 cancel_futures=True 丢弃,见 §4)。

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-17 完成;部署参数随下次 compose/镜像评审落地
中期 方案 B:run_occ(op_name, payload) 接口演进 + 常驻进程池,kill-on-timeout 根治泄漏 待排期(独立批次,工作量集中在调用点迁移与序列化设计)
长期 方案 C:sidecar,仅在出现独立伸缩需求时启动 暂不启动

4. 本次已落地的缓解(2026-09-17,批次 4)

_reset_occ_executor 的 shutdown(wait=False) 补 cancel_futures=True。这不只是卫生问题:旧实现下旧 executor 的排队任务不会消失——若挂死线程后来"慢恢复",旧线程会继续消化旧队列,与新 executor 并发操作非线程安全的 OCC(数据竞争 / 崩溃风险)。补参后排队任务即被丢弃,残留问题收敛为"运行中线程滞留 1 个",由方案 A 的进程回收兜底。