11 KiB
11 KiB
原生 LeAudit Worker 并发执行改造方案
这份文档不是纯方案稿,而是“已经落地到哪一步 + 当前系统为什么这么设计 + 后续接手时要看什么”的实施记录。后面如果忘了这套东西是干什么的,先看这份。
目标
- 把上传触发评查从 HTTP 同步执行切到
Run -> dispatch -> worker - 平台侧只做:
- 上传入库
- OSS / 规则文件解析
- run 状态编排
- 结果持久化
- 原生
/home/wren-dev/Porject/leaudit/src/leaudit继续负责:AuditCtxAuditService.audit(ctx)- OCR / extract / evaluate / rescue 主流程
为什么要改成 worker
- HTTP 请求里同步跑全链路会把 OCR、LLM、VLM、补救流程都塞进 Web 线程
- 后续存在并发上传、多文档批量处理、多 worker 扩容场景
- 同步模式下容易遇到:
- 请求超时
- 事件循环混用
- 多线程/多请求互相阻塞
- 状态不可追踪
- 改造成 worker 后,API 只负责“受理 + 排队”,重任务交给 Celery worker 进程
当前已落地
fastapi_admin/celery_app.py- 新增 Celery app,broker/backend 复用当前 Redis
fastapi_admin/config/_settings.py- 新增 worker 队列、并发、超时、OCR 视觉并发、signature probe 限时等配置
fastapi_admin/config/__init__.pyi- 暴露新的 LEAUDIT 配置项
app.toml- 正式加入
[LEAUDIT]worker/并发相关配置 - 补齐
[OCR].BASE_URL
- 正式加入
fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.pyRun()改为只创建leaudit_audit_runs并投递任务- 相同文档已有活动 run 时直接复用,避免重复创建
fastapi_modules/fastapi_leaudit/leaudit_bridge/tasks.py- 改为
runId驱动,而不是把整份文件内容直接塞到同步调用链 - 增加 Celery task 入口
- 增加 run 抢占,避免重复消费
- 增加队列路由
- 改为
fastapi_modules/fastapi_leaudit/leaudit_bridge/client_factory.py- OCR client 读取平台配置,避免
base_url is required
- OCR client 读取平台配置,避免
fastapi_modules/fastapi_leaudit/leaudit_bridge/auditServiceFactory.py- 原生服务装配读取平台并发配置
fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py- 修复原生
FieldValue新旧结构兼容 - 修复 JSONB 字段写库序列化
- 修复原生
scripts/start_worker.sh- worker 启动脚本改为动态读取配置
- worker nodename 改成唯一,避免 Celery
DuplicateNodenameWarning
docs/规则编辑/worker并发执行改造方案.md- 记录当前这套机制,避免后续自己忘记
执行链路
POST /api/uploadDocumentServiceImpl.Upload()写leaudit_documents / leaudit_document_files- 原始文件上传 OSS
AuditServiceImpl.Run()创建leaudit_audit_runsdispatch_leaudit_task(run_id)投递 Celery- worker 消费
leaudit.process_document - worker 按
runId查库,取:
- 文档
- 活跃文件版本
- 当前规则版本
- 规则 OSS 地址
- bridge 下载 OSS 文件 / 规则文件到本地临时文件
NativeRunner构建原生AuditCtx并调用原生 auditStorageAdapter把结果写回leaudit_*
当前实现涉及的关键文件
- 上传入口
fastapi_modules/fastapi_leaudit/controllers/documentController.pyfastapi_modules/fastapi_leaudit/services/documentService.pyfastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py
- bridge 层
fastapi_modules/fastapi_leaudit/leaudit_bridge/fileSourceResolver.pyfastapi_modules/fastapi_leaudit/leaudit_bridge/ruleVersionResolver.pyfastapi_modules/fastapi_leaudit/leaudit_bridge/auditCtxBuilder.pyfastapi_modules/fastapi_leaudit/leaudit_bridge/auditServiceFactory.pyfastapi_modules/fastapi_leaudit/leaudit_bridge/nativeRunner.pyfastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.pyfastapi_modules/fastapi_leaudit/leaudit_bridge/tasks.py
- worker 与配置
fastapi_admin/celery_app.pyfastapi_admin/config/_settings.pyapp.tomlscripts/start_worker.sh
并发配置正式化
LEAUDIT_LLM_MAX_CONCURRENCY- 单文档内 LLM 全局信号量上限
LEAUDIT_VLM_MAX_CONCURRENCY- 单文档内 VLM 主流程上限
LEAUDIT_OCR_VLM_CONCURRENCY- OCR 视觉增强 / 印章分类阶段的 VLM 并发上限
LEAUDIT_SIGNATURE_PROBE_CONCURRENCY- DOCX 签名候选区域补探测并发上限
LEAUDIT_SIGNATURE_PROBE_TIMEOUT- 单次签名 probe 的 VLM 超时,避免长时间卡住 OCR 尾部
LEAUDIT_WORKER_CONCURRENCY- 单 worker 进程可并发执行的任务数
LEAUDIT_WORKER_QUEUE_URGENT- 前端上传中的“紧急”队列
LEAUDIT_WORKER_QUEUE_NORMAL- 前端上传中的“普通”队列
LEAUDIT_TASK_SOFT_TIME_LIMIT- Celery 软超时
LEAUDIT_TASK_TIME_LIMIT- Celery 硬超时
LEAUDIT_RUN_LOCK_SECONDS- 为后续升级“租约锁”预留的锁超时配置
当前 app.toml 基线
[OCR]
BASE_URL = "http://i-2.gpushare.com:44112/"
[LEAUDIT]
LLM_MAX_CONCURRENCY = 5
VLM_MAX_CONCURRENCY = 3
OCR_VLM_CONCURRENCY = 3
SIGNATURE_PROBE_CONCURRENCY = 2
SIGNATURE_PROBE_TIMEOUT = 20
WORKER_QUEUE_URGENT = "leaudit.urgent"
WORKER_QUEUE_NORMAL = "leaudit.normal"
WORKER_CONCURRENCY = 2
RUN_LOCK_SECONDS = 1800
TASK_SOFT_TIME_LIMIT = 3300
TASK_TIME_LIMIT = 3600
当前队列路由策略
- 前端上传
speed=urgent->leaudit.urgent - 前端上传
speed=normal->leaudit.normal /audit/run也支持speed- 未识别值默认归到
normal
接口收口
- 上传接口:
POST /upload- 入参:
filetypeId/typeCoderegionfileRolecreatedByautoRunspeed=normal|urgent
- 已去掉旧系统
bizDocumentId输入
- 入参:
- 上传返回:
documentIdinternalDocumentNofileIdtypeIdtypeCoderegionfileNameossUrlspeedprocessingStatusautoRunTriggeredrun
- 手动触发:
POST /audit/run- 支持
speed=normal|urgent
- 支持
- 状态查询:
GET /audit/run/{runId}
- 结果查询:
GET /audit/result/{runId}
当前业务收敛结论
- 不再继续扩
high/default/batch三层方案 - 当前真实业务只有前端上传,并且只有“紧急 / 普通”两档速度需求
- 所以 worker 体系正式收敛成两级模型:
urgentnormal
- 这样既满足优先级需求,也避免过度设计
状态流转
- 新建 run
status=queuedphase=dispatch
- worker 抢占成功
status=runningphase=prepare
- 原生流程推进中
phase会继续流转为 OCR / extraction / evaluation / rescue / persist 等阶段
- 执行成功
status=completedresult_status由 bridge 汇总写回
- 执行失败
status=failedphase停在失败发生阶段leaudit_run_errors记录错误明细
避免重复消费
- API 触发
Run()时:- 若同一文档已有
queued/running/retrying的 run,且Force=False - 直接返回当前活动 run,不重复创建
- 若同一文档已有
- worker 开始执行前:
- 先按 run 状态执行原子抢占
- 仅允许
status in ('queued', 'pending', 'retrying')的 run 进入执行
- 抢占失败时:
- 当前 worker 直接跳过
- 返回
already_claimed
当前表语义
leaudit_documents- 平台内部文档主表
- 一次前端上传会创建一条新的 document
- 不再依赖旧系统
documents.id biz_document_id仅作为内部追踪号保留,避免立刻改库
leaudit_document_files- 具体上传文件记录
- 保存文件名、后缀、MIME、SHA256、OSS 地址等文件级信息
- 当前仍保留单独文件表,为后续是否合表留余地
已踩过并修掉的坑
- 事件循环混用
- 同步 HTTP 链路里直接执行原生异步流程,容易出现
Future attached to a different loop - 现在改成 worker 独立进程执行,HTTP 只排队
- 同步 HTTP 链路里直接执行原生异步流程,容易出现
- OCR base_url 缺失
- 原因是平台 bridge 没把
[OCR].BASE_URL正确喂给原生 client - 现已在
app.toml和client_factory.py补齐
- 原因是平台 bridge 没把
- 原生
FieldValue结构变化- 新版本
FieldValue不一定有.position storage_adapter.py已改为兼容:- 优先读
position - 没有时回退到
metadata.match_position / bbox / page_num
- 优先读
- 新版本
- asyncpg + JSONB 序列化
- 不能把
list/dict直接按字符串列方式塞给CAST(:x AS JSONB) - 需要先
json.dumps(..., ensure_ascii=False)再入库 - 当前已在这些路径处理:
leaudit_rule_results.stagesleaudit_rule_results.extracted_fieldsleaudit_rule_results.field_positionsleaudit_rule_results.remediationleaudit_rule_results.rule_metaleaudit_rescue_outcomes.payload
- 不能把
当前运行现状
POST /api/upload已经可以快速返回queued- worker 已能真正执行到:
- OCR
- extraction
- evaluation
- rescue
- 已验证真实样本端到端成功:
run_id=10document_id=9phase=executed24 passed / 4 failed / 0 skipped
- 当前主要耗时瓶颈明确在 OCR 阶段
- 一次验证中:
ocr=277.79sextraction=13.65sevaluation=11.81stotal=303.25s
- 一次验证中:
- 已针对 OCR 长尾补第一轮治理:
- DOCX
signature probe增加单独超时 - DOCX
signature probe增加并发限制 - 避免某个候选框失败时串行拖住整个尾段
- DOCX
- 如果代码刚修过但日志仍报旧错,第一怀疑项不是代码没改,而是 worker 进程还没重启,仍在跑旧代码
- worker 日志会明确打印:
run_id=xx 已投递到 worker 队列: queue=..., speed=...run_id=xx worker开始执行: queue=..., speed=..., filename=...
启动方式
后端 API:
.venv/bin/python run.py
worker:
bash scripts/start_worker.sh
脚本会自动读取:
LEAUDIT_WORKER_CONCURRENCYLEAUDIT_WORKER_QUEUE_URGENTLEAUDIT_WORKER_QUEUE_NORMAL
并拼成:
celery -A fastapi_admin.celery_app:celery_app worker \
-Q leaudit.urgent,leaudit.normal \
--concurrency=2
同时 worker nodename 会带主机名和进程号,避免多个 worker 使用同名节点。
联调时的最小操作顺序
- 启动 API:
.venv/bin/python run.py - 启动 worker:
bash scripts/start_worker.sh - 调用上传接口,确认返回
processingStatus=queued - 看 worker 日志是否出现:
- 抢占 run
- OCR result saved
- Extraction result saved
- Evaluation results saved
- finalize run
- 如果日志还是旧报错,先重启 worker 再测一次
后续明确待办
- 给 run 抢占补上真正的租约锁字段:
worker_idlock_expires_atattempt_count
- 如果后续紧急任务量上来,再考虑把
urgent单独拆成专属 worker - 补
GET /run/{id}/status或前端轮询说明 - 评估 OCR 阶段是否继续拆更细粒度并发,但前提是先确认外部 OCR / VLM 服务的限流上限