业务场景
我司于2019年自研发了一套股市信号回测平台,使用到了TDengine1.6版本。在今年TDengine发布了2.0版本之后,我司正好也要开发股市策略交易平台,就实装上了TDengine2.0。该平台沿袭了回测平台模块化、去代码的策略构建方式,以及分布式的实时计算,针对股市的时效性要求和策略的复杂性实现做了大量的工作和优化,时效性是主要采用TDengine2.0来作为tick数据的存储,在查询中自动聚合实时的K线用于计算,速度上还是很快的。该平台目前已经上线使用。
平台的整体架构就不多做介绍,简单的介绍一下策略信号计算这块的架构,后面的交易层面并未用到TDengine2.0。
系统架构图
可以看到,整体的架构比较清晰。底层有MongoDB和TDengine共同提供数据支持,MongoDB存储历史股票数据,TDengine来提供实时行情Tick数据。Master服务器根据平台配置的任务参数,把计算任务写入Redis中间件的队列,Slave服务器组从队列中取出任务,根据历史数据和实时行情数据,进行实时计算,最后把计算结果写入elasticsearch,计算出来的信号供后面的交易模块使用。
为什么选择TDengine作为实时行情数据库
我们选择用TDengine来作为实时行情的数据库有如下几个原因:
1.写入快。经过数据库比较和使用体验,TDengine的写入速度远超其他数据库,这都依赖于TDengine的列式存储设计。
2.时间序列设计。金融数据是时间序列数据,对时间要求强一致,这一点与TDengine的设计初衷是完全一致的,由于其时间序列的设计,数据的写入非常方便,如果由于网络抖动,后面的时间写进去了,前面的时间数据再写入也能被接收,支持乱序写入,这就保证了时间序列的强一致性。
3.聚合方便。TDengine的聚合函数目前已经比较丰富,用其中的聚合函数,我们能直接聚合出K线。
4.读出快。TDengine多核写入,多核读出,对于查询请求,也是以map/reduce类似的形式去查询,效率高。
5.免费,而且是集群版免费。毋庸置疑,这是巨大的优势,其他的金融数据库,一个节点需要15W甚至30W,而TDengine做到了集群免费,这一点不得不要respect一下涛思数据的陶总,这种勇气和见识非一般人能达到。
基于以上几点,TDengine是我们目前最好的选择。
设计
在深入了解了TDengine的特性以及使用原则后,我们设计了如下的数据模型:
- 对Tick数据建库。库下一个Tick超级表,以超级表做模板建子表,一只股票一张子表。共有4022个子表。每只股票的历史Tick都统一以时间戳为主键写入分别的一张表。每只股票以股票代码作为表名,设有6个标签:股票聚宽代码、拼音缩写、名称、上市日期、退市日期、类型。
- 超级表共有26个字段。包含了每一个Tick的市场信息,当前价,最高价,最低价,成交量,成交金额,买五卖五的挂单情况和均价线。
- 超级表结构
Field | Type | Length | Note |
=======================================================================================================
ts |TIMESTAMP | 8| |
current |FLOAT | 4| |
high |FLOAT | 4| |
low |FLOAT | 4| |
volume |FLOAT | 4| |
money |FLOAT | 4| |
a1_p |FLOAT | 4| |
a1_v |FLOAT | 4| |
a2_p |FLOAT | 4| |
a2_v |FLOAT | 4| |
a3_p |FLOAT | 4| |
a3_v |FLOAT | 4| |
a4_p |FLOAT | 4| |
a4_v |FLOAT | 4| |
a5_p |FLOAT | 4| |
a5_v |FLOAT | 4| |
b1_p |FLOAT | 4| |
b1_v |FLOAT | 4| |
b2_p |FLOAT | 4| |
b2_v |FLOAT | 4| |
b3_p |FLOAT | 4| |
b3_v |FLOAT | 4| |
b4_p |FLOAT | 4| |
b4_v |FLOAT | 4| |
b5_p |FLOAT | 4| |
b5_v |FLOAT | 4| |
average |FLOAT | 4| |
code_jq |BINARY | 11|tag |
abbr |BINARY | 5|tag |
name |NCHAR | 4|tag |
start_date |BINARY | 10|tag |
end_date |BINARY | 10|tag |
type |BINARY | 5|tag |
实践
- 数据写入
写入数据在0.01秒左右,写完4000多张表。对于这个速度,我们已经比较满意。相较于MongoDB,做了索引的表,写入要在0.5秒左右,性能差距巨大。 - 数据查询
由于业务特殊性,要求一次性查出并聚合指定的若干只股票在当前时间下的K线数据。为此,在深入研究了超级表查询语法规则下,我们设计了如下的查询语句:
第一种是对超级表下的子表的全查询:
select first(current),MAX(current),MIN(current), last(current), MAX(volume), MAX(money) from tick.tick where ts >= \'2020-10-28 09:30:00\' and ts <= now INTERVAL(30m) FILL(null) group by code_jq
这个语句会找到每一张子表(股票)今天早上开盘到当前时刻的所有数据,然后聚合出开、高、低、收、量、金额,输出记录。
第二种是对超级表下的部分子表的查询:
select first(current),MAX(current),MIN(current), last(current), MAX(volume), MAX(money) from tick.tick where (code_jq = '000001.XSHE' or code_jq = '600304.XSHG' ...) and (ts >= \'2020-10-28 09:30:00\' and ts <= now) INTERVAL(30m) FILL(null) group by code_jq
这个语句是对第一种语句的变种,通过对TAG的过滤查询,可以做到对部分子表的查询。
TDengine应用情况
在最初的设想中,每个Slave节点都要多核高并发的去请求TDengine数据库,请求不同的数据。目前,我们拥有6个slave节点,每个节点16核32线程。所以在满负荷下,同时会有将近200个进程同时向数据库请求数据。这极大考验TDengine的高并发能力。可惜的是,在单节点测试中,也就是32进程高并发下,进程偶尔会发生异常,进入僵死。僵死时没有报错信息,通过日志定位,我们定位到某一个进程中TDengine的连接异常,不报错也不退出。历时几天,在涛思数据技术工程师的协助下,我们找到了问题所在,但发现不可解,并且经过沟通,目前涛思也没有足够的资源来解决这个问题,最后只能认清TDengine不能多进程并发请求的现实,放弃这个方案。这个方案一次性请求所有tick数据的效率最高,时间为1.33秒。
在认识到多进程方案不行之后,我们改为多线程获取全部数据。发现开了20多个线程同时请求时,仍旧会出现线程僵死的情况,无奈之下还是放弃。多线程方案一次性请求所有tick数据的效率次之,时间为2.66秒。
无奈之下,我们只能改用单线程方案。这下终于没问题了,一次性请求所有tick数据的效率最差,时间为3.77秒。
平心而论,最后的方案是令人不满意的,时间上面相差了2秒,如果对于金融的高频策略来说,这几乎是不可忍受的。但是由于我们主要不是高频策略,因此还勉强能用。目前,该方案已正式上线使用,已正常运行10多天,偶尔会突然无法建立连接,但通过异常处理机制重新创建连接也能处理好。
结语
这次使用TDengine2.0,第一个感受就是巨变,与1.0版本已经截然不同,很多参数都已经改变,据了解,使用了新的架构。第二个感受就是涛思团队的专业和负责任的态度,我们虽然不是一个付费用户,但涛思团队的苏晓慰技术工程师尽心尽力的协助我们上线TDengine,碰到问题不推诿,竭尽全力协助我们确定问题,协调公司技术资源一起排查,虽然最终发现问题一时无法解决,但是我们相信,有这样的工程师,这样的团队,涛思数据的未来,中国数据库的未来,肯定是不可限量的。