为什么从 demo 到生产路还很长?一次数据拉取功能的复杂度演进复盘

我在做一个 A 股量化研究系统,行情、市值、财务这些数据都要从 Tushare Pro 拉,而且是全市场、三年历史。需求听起来很简单------调接口、存数据。第一版确实就一个 for 循环:逐只股票调接口、拼 DataFrame、落盘。跑 50 只样本,几十秒,一切正常。

后来这个功能长出了八套机制:按日批量、积分限速、多线程并行、分片断点、增量更新、全局共享限流、跨进程互斥、流式合并。回看这段演进,最有意思的不是这些机制本身,而是它们是一层一层被逼出来的,每一层的解决都打开了下一层。这篇文章复盘这条演进链,以及为什么 demo 阶段什么都看不见。

第一层:规模------调用量

从 50 只到 5695 只,第一个撞墙的是调用量。逐股循环放大到全市场:5000 只 × 4 接口 = 2 万次调用,按 Tushare 5000 积分的限速(800 次/分钟)一算,要 50 分钟往上。这个还没跑就算出来了,暴露得最早,解决得也最干脆:行情接口支持按交易日批量查,一次调用取回全市场单日数据。拉取维度从"按股票"翻转为"按日期",879 个交易日,调用量降到 3500 次,少了 90%。

这层的认知是:同样的数据,取法不同,成本差一个数量级。而取法是规模逼着你重新审视的------50 只的时候,没有任何动机去研究接口的批量参数。

第二层:数据量------内存

按日批量让数据真的进来了,紧接着撞的是内存。879 天的数据帧全堆在内存列表里一次性 concat,进程被 OOM killer 静默杀死,没有任何报错。系统不是告诉你"这里有问题",而是直接消失,死因得自己推断。

解法是分批:每攒 60 天合并一次、释放中间帧,把内存峰值从"全量 × 2"压到"全量 + 单日 × 60"。这一层暴露了一个规律:内存峰值由"数据规模 × 操作方式"共同决定,换操作就换峰值------这个规律后来还会再坑我一次。

插曲:真实数据击穿校验规则

全量数据跑校验时报了 12 行"后复权价跳变异常",查下来是两只股票:一只 302 开头的创业板新股连续涨停被误报,因为阈值只认 300/301/688/689 开头,漏了 302 这个新代码段;另一只是退市整理期的股票,单日跌 52%,那是真实行情。修法:补 302,跳变行只要收盘价在涨跌停价范围内就放行。

这层没有新机制,但它说明了一件事:规则是没见过真实数据就写不对的。校验规则、阈值、白名单,全都是在真实数据的边角料上长出来的。

第三层:失败------从"能跑"到"能跑完"

任务能跑了,但跑不跑得完是另一回事。全量任务在后台丢了两次,任务系统报 lost,没有任何错误信息。根因还是内存,但暴露的事实比内存更本质:长任务失败 = 全部白干,879 天拉了一半,重来又是几十分钟。

解法是分片断点:879 天切 15 片,每片独立拉取、独立落盘、独立校验,重跑时已完成片直接跳过;写盘用临时文件加 rename 保证原子性,杀在半路也不会留下损坏文件被断点误判成"已完成"。

这层引入了一个新维度的复杂度------状态管理。任务从"一次性的脚本"变成了"可恢复的工作单元",从此所有长任务都要回答一个问题:中断了怎么办?

第四层:速度------并行

能跑完,但串行要 75 分钟。压时间的方向是并行:6 个 worker 共享一个限流闸门,压到 15 分钟。这里有个值得澄清的认知:拉取是 IO 密集任务,99% 的时间在等网络响应,socket 等待时线程会释放 GIL,所以多线程真的能并行------实测 32 次调用从 40 秒压到 9 秒。多进程反而更麻烦:限流器没法跨进程共享,几个进程各按 800 次/分钟冲,直接超限被拒。

并行的副作用是:限流从"每个模块自己的事"变成了"所有任务共享的事"。这为后面那层复杂度埋了伏笔。

第五层:成本------从"拉一次"到"天天拉"

数据开始天天跑了,全量重拉的成本变得不可接受。于是有了增量更新:只拉新交易日,合并写回,日常耗时从 75 分钟降到 1 分钟。

这层的认知反转是:单次任务的时间不是问题,周期性任务的时间才是。数据从"拉一次"变成"天天拉",成本语义就变了。增量不是为了变快,是为了让"天天拉"这件事成立。

第六层:并发------超限流的不是单个任务

这是最典型的"想想就会出事"的一层。各模块各建限流器,各自不超 800 次/分钟,逻辑上完全正确。但"如果同时点两个刷新"呢?合计 1600 次/分钟------超限流的不是任何单个任务,是任务之间的并发。局部正确、全局错误。

解法分两层:进程内全局共享限流器(所有任务共用一个线程安全闸门),再加一把跨进程的 flock 文件锁,保证任何时候全局只有一个拉取任务在跑,第二个排队。这一层不是失败暴露的,是推演暴露的------系统永远不会替你发现"并发"这个维度,只有你主动去想使用场景才会撞到它。

第七层:内存,第二次

分片机制本身是好的,但合并 15 个分片的时候又 OOM 了一次:350 万行一次性 concat 加 sort。这次换了流式引擎(scan 到 sink 边读边写),顺带砍掉全局排序------分片内本来就排好序,下游自己会排。

这层是第二层的重演,但墙的位置不同:拉取阶段和合并阶段各有各的内存峰值。只解决过一次 OOM 的人,会误以为"分批 concat 就够了";数据管道的每个环节都要单独过一遍内存这道关。

复盘:复杂度是怎么演进出来的

回头看,这条链是:规模 → 数据量 → 真实数据 → 失败 → 速度 → 成本 → 并发 → 内存。每一层都建立在前一层的解决之上------没有按日批量就没有数据进来,没有数据就没有内存问题,没有长任务就没有失败恢复,没有并行就没有限流共享,没有天天跑就没有成本问题,没有多任务就没有并发问题。复杂度不是并列的八个问题,是一条链,环环相扣。

驱动演进的变量有两个:先是规模 (数量级的放大),后是用法(单次 → 周期 → 并发)。规模可以估算,用法只能"用起来"才知道------这就是为什么 demo 阶段什么都看不见:demo 是单次的、小规模的、理想环境的、手工操作的,它把"功能正确"之外的所有维度都屏蔽了。

还有一个值得记住的点:不同维度用不同的方式暴露。调用量靠估算(动手前),内存靠崩溃(跑起来),失败靠事故(反复跑),并发靠推演(想到了),成本靠习惯(天天跑)。系统不会统一告诉你"这里有个复杂度",每种复杂度有自己的探测方式,你得全部掌握。

回到 AI 编程

让 AI 写这个功能,它会在假设内写得很正确------事实上第一版就是标准的"假设内正确"代码。AI 的问题不在代码质量,在于它默认的假设和 demo 一样理想:单次、小规模、不失败、无并发。而假设是隐性的,你甚至意识不到"逐只循环"里藏着一个"股票数量很小"的假设,直到规模把它掀翻。假设的挖掘靠真实使用、失败复盘和对自己系统的推演------AI 没有经历过你的失败,也推演不了你的使用方式。它能帮你在假设内把代码写对,但假设的设定和检验,是人的事。

相关推荐
鲁邦通物联网18 小时前
出海物联网设备的全球化网络接入与合规脱敏架构:基于 Node-RED 与 C++ 零拷贝的边缘系统设计
网络·物联网·系统架构·边缘计算·边缘计算网关·5g数采·工业级边缘计算网关
huihui36119 小时前
校友网管理软件技术选型框架:基于功能核验与系统架构的量化评估
unity·系统架构·游戏引擎
ZJU_统一阿萨姆21 小时前
【算子开发】环境搭建与第一个CUDA程序
开发语言·人工智能·系统架构
郑州光合科技余经理1 天前
海外版多语言团购系统架构:主数据互通与核销边界
java·开发语言·前端·后端·系统架构·php·ai编程
ly76891 天前
Linux 从入门到实践:系统架构、常用命令、服务管理与故障排查详解
linux·运维·系统架构
老郑聊AI业财智造1 天前
Qwen技术架构与源码深度剖析
人工智能·语言模型·架构·系统架构·软件工程
学习星球1 天前
Astro 全栈实战:拆解 RealWorld 项目
前端·javascript·架构·系统架构·前端框架·c5全栈
老郑聊AI业财智造1 天前
DeepSeek技术架构与源码分析
人工智能·语言模型·架构·系统架构·软件工程
ly76891 天前
Windows 操作系统详解:从系统架构、文件管理到安全与运维
windows·安全·系统架构