
@TOC
我之前做的一个项目,用的DolphinDB,从上线到现在很长时间,整个链路跑下来我最大的感受就是:这套引擎本身的稳定性和性能其实远超我们一开始的预期,绝大多数时间都安安稳稳,很少出问题。但从刚上线第一个月天天报警,到现在连续三四个月不出一次问题,我前前后后踩了不下十几个大大小小的坑,熬了无数个夜班排查,说起来很多坑其实都不是引擎本身的问题,都是我一开始对实时时序处理的特性不熟悉,凭着原来攒三件套的经验瞎调参数,或者业务设计不符合最佳实践导致的。

这篇文章我把这段时间遇到的影响最大的5类问题整理出来,每个问题都带真实的案例、完整的一步步排查过程、直接可以落地的解决方法,给刚做实时时序数据处理的朋友做个参考,少走点我踩过的弯路。
这个问题是我上线第一个月遇到的第一个大问题,到现在印象都特别深:那天开盘刚过10分钟,我盯着的监控平台突然就炸了,满屏的红色报警:tick流表写入平均延迟超过阈值200ms,峰值直接冲到了1.2s,而且系统日志刷出来一堆写入超时,粗略算了一下大概有万分之三的写入包没成功,再涨下去就要丢原始行情数据了,那可是天大的事。
当时我吓得直接从工位弹起来,一边赶紧临时切到备用的旧链路稳住交易,一边开始蹲下来排查问题。原来稳定运行的时候,写入延迟平均都在1-2ms左右,就算开盘高峰期也不会超过5ms,那天突然就涨了一百倍,完全没征兆。
我当时按照从易到难的顺序一步步排,过程分享给大家:
除了这个,还有个雪上加霜的问题:我那时候不懂流表的cacheSize参数到底是干嘛的,想当然觉得缓存越大性能越好,直接给cacheSize设成了1000万,意思就是流表要在内存里放1000万条最新数据。我们每个tick数据大概100字节,1000万条就是1G左右,其实看起来不大,但我们节点本身还要跑大量因子计算,剩下的内存不够用,系统开始频繁换页,相当于又给磁盘增加了一堆随机IO,本来就满负荷的磁盘直接扛不住了。
找到根因之后,我们做了三个调整,改完之后延迟直接回到了1-2ms的正常水平,DolphinDB一直稳定到现在,从来没再出过大问题:
cacheSize是流表内存缓存的最大条数,其实只要能覆盖高峰期1-2秒的写入量就够了,再多了也没用,反而平白占内存。我们高峰期一秒最多15万条写入,所以我最后把cacheSize调到了10万条,既够缓存峰值写入,又不占多余内存,再也不会出现内存不够换页的问题。调整之后的创建流表代码我贴在这,给做同类行情接入的朋友参考:
// A股实时tick流表生产环境配置
schema = table(
1:0,
`symbol`time`price`volume`bid1`ask1`bidSize1`askSize1,
[SYMBOL, NANOTIMESTAMP, DOUBLE, DOUBLE, DOUBLE, DOUBLE, INT, INT]
);
createStreamTable(
schema,
name=`tick_stream,
partitions=16, // 按标的哈希分16区,打散写入热点
cacheSize=100000, // 缓存10万条,刚好覆盖高峰期1秒写入量
persistenceDir='/ssd/data/persist/tick_stream/', // 实时持久化放独立SSD
retentionTime=3600 // 实时流只保留1小时,历史数据异步落冷盘
);实时写入延迟飙升这个问题,我后来跟同行交流,发现90%都是IO规划不对或者缓存参数配置不合理导致的,一体化引擎本身的写入性能其实很高,我们后来压测过,单节点SSD能跑到每秒百万级写入,完全能满足绝大多数实时场景的需求。只要提前把存储介质规划好,参数按照实际场景调到合理范围,基本不会出这个问题。
这个问题是我刚开期权夜盘交易的时候遇到的,现在想起来都头疼:那天是交易所刚开期权夜盘,我们跟着上线,我加班把所有测试都跑了一遍,确认没问题才下班走,结果凌晨两点电话突然响了,运维小哥打过来,说监控显示夜盘行情停更了一个多小时,因子都没输出,吓得我直接从床上弹起来。
我迷迷糊糊爬起来连VPN登服务器一看,果然,流表写入是正常的,一直在进新数据,但是下游订阅流表做因子计算的消费线程,已经一个多小时没输出结果了,完全断流了。我赶紧手动重启了消费节点,恢复了服务,那天后半夜我也没睡着,躺着想为什么好好的会断流。
第二天我开始仔细排查,把可能的原因一个个排除:
还有个次要原因:我当时为了做高可用,把订阅放在了两个不同的节点,原来的老节点用完没删除订阅,同一个流表有大量没用的闲置订阅,占了节点的连接数,也会导致正常的订阅被挤掉超时。
找到问题之后,只改了两个地方,DolphinDB到现在再也没出现过无故断流的问题:
订阅配置示例:
// 订阅流表,开自动重连,调整超时时间
subscribe(
table=tick_stream,
name=`tick_factor_consumer,
handler=processTick,
reconnect=true, // 必须开自动重连!默认是关闭的
timeout=180 // 超时时间设180秒,适配低峰长间隔场景
);这个坑其实真的很低级,完全是我一开始没仔细看配置文档,凭着想当然配置,测试的时候都是高峰期测,一直有数据,测不出来问题,一到生产低峰就暴露了。所以大家配置完订阅,一定要测试一下长时间没数据的场景,看看断了之后能不能自动重连,别等到凌晨出问题再爬起来改。
这个问题隐蔽性特别强,我们上线三个月才发现不对:我们开发的一个日内高频因子,历史回测的时候夏普比率能到2.3,收益非常稳定,结果上线实盘跑了三个月,收益一直比回测低快1个百分点,而且因子值每天都有一小部分明显不对,漂移得厉害,我们查了半个月才找到根因,就是乱序数据的问题。
什么是乱序数据?就是交易所发出来的行情,有时候因为网络延迟或者交易所的广播顺序问题,后发生的tick反而比先发生的先到我们的系统,我们测过,一般也就差几毫秒,最多差个几十毫秒,量也很少,不到千分之一,但就是这么点乱序,把我们的滚动窗口计算结果带偏了。
我当时把一周的实盘原始数据拉出来,把因子计算过程重放了一遍,对着结果一个个找:
举个我们实际遇到的例子:我们做的是1秒的滚动成交量窗口,正常顺序是:t0(0ms) → t1(500ms) → t2(1000ms),到1000ms的时候触发窗口计算,把0-1000ms的成交量加起来。但乱序之后变成了:t0(0ms) → t2(1000ms) → t1(500ms),t2到了之后立刻触发计算,t1后到,就没被加进去,窗口算出来的成交量就少了t1的量,结果直接错了。
找到问题之后,我们做了两个调整,之后因子结果就对了,实盘收益也追上了回测:
滚动窗口配置示例:
-- 1秒滚动成交量窗口,允许10ms乱序延迟
window = tumblingWindow(timeColumn=`time, windowSize=1000, delay=10)实时场景下乱序数据是非常常见的,不管你用什么架构,都会遇到,千万不要觉得交易所发出来的数据一定是有序的,一定要给自己留够冗余。如果对结果正确性要求高,牺牲一点点可接受的延迟换正确性,绝对是划算的。我们一开始就是太追求低延迟,把延迟设成0,才吃了亏,现在想想真的没必要,10ms的延迟换来完全正确的结果,太值了。
这个问题是上线第二个月遇到的:我们发现节点刚重启完,内存使用率大概25%左右,然后每天慢慢涨,涨到周五收盘的时候能到85%,周六日不开市,也不跌,周一开盘一跑峰值写入,直接就冲到95%以上,触发OOM保护自动重启,虽然重启之后就好了,但每周一次,还是挺吓人的,万一重启出问题,就是大事故。
明显就是内存泄漏,有内存一直没释放,攒了一周就满了。
排查内存泄漏其实有固定的步骤,我把我们的排查过程分享出来:
还有个小问题:我们测试的时候,经常重新发布自定义计算函数,每次发布都生成新的函数对象,原来的旧对象没人用了,但也没释放,也占了一部分内存,积少成多。
调整之后,我们节点现在连续跑一个月,内存使用率波动都不超过5%,再也不会OOM了:
状态配置示例:
// 日内因子状态,48小时自动过期清理
createStateTable(
schema=factor_schema,
name=`intraday_factor_state,
key=`symbol,
expireTime=48*3600 // 48小时过期
);内存泄漏其实都是资源没清理导致的,实时流计算跑的时间长了,一定会产生很多没用的状态,一定要提前规划好清理规则,不要想着内存够大就不清理,再大的内存也架不住一天天攒,早晚满。养成定时清理资源的习惯,能避免90%的内存问题。
我们为了高可用,做了主备两个消费节点,主节点出问题自动切到备节点,结果第一次模拟主备切换的时候,出问题了:切换完之后,备节点把过去一个小时的tick全部重新处理了一遍,因子值全部重复计算,导致策略发出了双倍的委托,幸好是模拟切换,没真的下单,不然就出大事了。
问题其实很好找:我们原来主备两个节点是各自管理自己的消费offset,也就是各自记录自己消费到哪个位置了,主节点一直跑,offset一直更新,备节点平时不工作,offset停在切换前的位置,一切换之后,备节点就从自己记录的旧位置开始消费,就把已经处理过的数据又处理了一遍,导致重复消费。
改完之后,我们做了十几次主备切换测试,再也没出现过重复消费的问题:
我们把消费offset从原来的节点本地存储,改成了存在共享的元数据表里,主备两个节点都从这个共享表读offset,写offset,不管哪个节点跑,都会更新共享的offset,切换之后,备节点直接从共享表拿到最新的offset,接着主节点的进度继续消费,不会重复处理之前已经处理过的数据。
核心配置就是订阅的时候把offset存储位置改成共享表:
// 共享offset存储,支持主备高可用切换
subscribe(
table=tick_stream,
name=`tick_factor_consumer,
handler=processTick,
offsetTable=shared.offset_storage, // 共享offset表
reconnect=true
);做高可用架构,一定要提前想清楚offset的管理方式,不要各自存各自的,共享存储是最简单可靠的方案,不要嫌麻烦,不然出一次重复消费的问题,可能就是真金白银的损失。
我最大的感受就是:做DolphinDB实时时序数据处理,大部分坑其实都不是引擎本身的问题,都是我们自己对实时处理的特性不熟悉,凭着旧经验瞎配置,业务设计没贴合最佳实践导致的。
最后给大家总结几个核心的避坑建议:
整体来说,用一体化的时序处理引擎做实时数据处理,比原来自己拼三件套真的省心太多了,开发效率高,性能也强,只要踩过这几个坑,把该调的参数调好,就能跑的非常稳定。我整理这篇文章,就是希望刚入门的朋友能少走点我走过的弯路,毕竟熬夜排障的滋味真的不好受。
如果大家遇到其他问题,欢迎在评论区交流。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。