☰
tick-stock-panel 回测子进程隔离架构深度解析:spawn worker、持久 run ID 与刷新重连完整指南
2026/9/28 21:19:18 网站建设 项目流程

tick-stock-panel 回测子进程隔离架构深度解析:spawn worker、持久 run ID 与刷新重连完整指南

【免费下载链接】tick-stock-panelTSP自托管、零运维的 A 股「选股 + 监控 + 回测」量化工作台 | LLM能力驱使策略定制+个股分析+复盘 | 自由接入第三方数据源与个性化扩展数据 | 个人开源项目地址: https://gitcode.com/GitHub_Trending/ti/tick-stock-panel

tick-stock-panel 是一款自托管、零运维的 A 股「选股 + 监控 + 回测」量化工作台。它的回测系统有一个常被忽视却至关重要的设计:重活全部扔给独立子进程干——主 Web 服务只当"调度员",回测、参数优化、滚动验证、因子挖掘在 spawn 出的全新 Python 进程里跑;长任务再落盘成带持久 run ID 的任务档案,配合 SSE 断线重连,你刷新页面、切换标签页,进度都不会丢。本文带你完整看懂这套「子进程隔离 + 持久 run ID + 刷新重连」的架构。

为什么不在主进程里直接跑回测?

一次策略回测可能要吃掉几个 GB 内存、跑几分钟。如果它和 API 服务挤在同一个进程里,会发生三件事:

  1. 内存爆炸连累全家:回测 OOM 时整个 Web 服务一起挂掉,连自选股列表都打不开;
  2. 状态污染:NumPy/Polars 等扩展模块被回测加载后,主进程的内存再也回不干净;
  3. 卡死无解:重计算占住 GIL,HTTP 请求全部排队。

tick-stock-panel 的解法很直接:worker.py 里的run_worker_task统一使用mp.get_context("spawn")创建全新解释器进程(而非 fork 复制),回测结束后进程直接销毁,内存干干净净还给操作系统。

spawn worker:一次任务、一个全新进程

任务如何打包进子进程

主进程通过 make_worker_task 把配置序列化成纯 JSON 字典,包含四种任务类型:backtest(策略回测)、optimize(参数优化)、walkforward(滚动验证)、mining(因子挖掘)。日期等不可直接序列化的对象都会先转成 ISO 字符串,子进程启动后在 _decode 系列函数 里还原。

子进程入口 _worker_entry 做了几件值得学习的事:

  • 不继承任何状态:数据仓库、策略引擎、因子注册表都在子进程里从头加载,自定义因子通过load_into_registry从磁盘恢复,与主进程行为完全一致;
  • 全程采样内存峰值:_PeakRssSampler 每 50ms 记录一次 RSS,任务报告里会附带peak_rss_bytes、serialized_result_bytes等指标,帮你量化"这个回测到底吃了多少内存";
  • 快速收尸:结果入队后先close + join_thread冲刷管道,再os._exit(0)跳过解释器收尾,避免慢退出撞上父进程的 10 秒等待预算。

父子进程之间只有三种消息

子进程与父进程只通过一条队列通信,消息类型固定为progress(进度)、result(结果)、error(错误 + 完整 traceback)。父进程在 run_worker_task 的主循环 中以 0.1 秒为步长轮询,把进度透传给上层,把结果原样返回。

两个容易翻车的边界它都做了兜底:

  • 取消:上层触发取消事件后,子进程会优雅退出;若 5 秒宽限期(_CANCEL_GRACE_SECONDS)内仍未停止,父进程直接terminate()并抛出明确报错;
  • 子进程"活得太慢":结果已送达但进程 10 秒内没退出时,父进程强制结束它,但依然采纳已送达的成功结果(结果中标记worker_exit_forcibly: true),绝不把跑成功的回测当失败丢弃。对应行为可参考 test_worker_process.py 中的两个用例。

持久 run ID:挖掘任务刷新页面后还在跑

短任务靠内存就够了,但因子挖掘动辄几十分钟,必须能活过"刷新"甚至"服务重启"。tick-stock-panel 为此把任务档案落盘到磁盘,核心是 mining_jobs.py 里的MiningRunStore:

  • run_id 就是目录名:每次任务用校验过的 UUID hex 作为 run ID,独占一个目录,里面有manifest.json(状态机:queued → running → succeeded / failed / cancelled)、summary.json(最新进度摘要)、events.jsonl(单调递增编号的紧凑事件流)和若干 Parquet 产物;
  • 事件带自增 ID:append_event 给每条事件分配递增id,只保留最近一批(MAX_EVENTS),天然支持"从第 N 条继续读"的游标语义;
  • 签名去重:相同请求 + 相同数据指纹的任务由find_by_signature直接复用,不会重复烧算力;
  • 重启自愈:服务启动时 recover_interrupted 扫描所有仍处于活跃状态的 run,统一标记为 interrupted,避免"僵尸任务"。

后台编排则由 mining_manager.py 的MiningJobManager负责:每个 run 一个守护线程 + 一个取消事件,worker 的每帧进度都会实时写进events.jsonl。

刷新重连:SSE 双通道让进度永不丢失

回测流:job key 去重 + 进度回放

策略回测走 /api/backtest/strategy/stream 这个 SSE 接口,重连能力来自 模块级任务表:

  • 相同参数的请求会算出同一个 job key(全部回测参数做 MD5 截取 12 位),第二次连接直接订阅已有任务,回测只会启动一次;
  • 每帧进度都追加进job.progress历史列表,新连接用游标从头回放,刷新后进度条直接从当前位置接上;
  • 断开连接不会取消任务(除非显式调用 cancel),结果完成后再保留 5 分钟供重连取回。

挖掘事件流:Last-Event-ID 游标重连

挖掘任务的事件流是 /api/backtest/mining/runs/{run_id}/events,重连机制更优雅:浏览器 EventSource 断开重连时会自动带上Last-Event-ID请求头,服务端据此调用read_events(run_id, after_id=cursor),只补发游标之后的事件;若发现中间有缺口,还会先补推一份summary.json里的最新进度,保证画面永远连续。配合 15 秒心跳,长任务期间连接不会被代理或浏览器掐断。

一句话总结两条链路:

能力回测流挖掘事件流
任务身份参数哈希 job key持久 run ID
进度存储内存列表(TTL 5 分钟)磁盘 events.jsonl
重连续传方式进度历史全量回放Last-Event-ID 游标补发
断线是否取消任务否否

想深入源码?从这三个文件开始

  • 子进程调度核心:backend/app/backtest/worker.py
  • 回测 SSE 流与重连:backend/app/api/backtest.py
  • 持久任务存储与事件游标:backend/app/services/mining_jobs.py

配套的挖掘设计文档见 docs/mining.md,端到端行为测试见 backend/tests/backtest/test_worker_process.py。这套「spawn 隔离 + 磁盘 run 档案 + SSE 游标重连」的组合,本质上把"内存里的进程"和"磁盘上的任务"解耦了:进程可以随时死,任务状态永远可恢复——这正是自托管量化工作台在低配服务器上也能稳定长跑的关键。

【免费下载链接】tick-stock-panelTSP自托管、零运维的 A 股「选股 + 监控 + 回测」量化工作台 | LLM能力驱使策略定制+个股分析+复盘 | 自由接入第三方数据源与个性化扩展数据 | 个人开源项目地址: https://gitcode.com/GitHub_Trending/ti/tick-stock-panel

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询