ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

为什么从 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 没有经历过你的失败也推演不了你的使用方式。它能帮你在假设内把代码写对但假设的设定和检验是人的事。
返回列表