对于需要处理大量市场数据的量化研究者来说,选择合适的数据库可以显著提高其工作效率。我们将介绍在数据处理中常见的MySQL、HBase、ES的主要特点和应用场景。
MySQL:关系型数据库,主要面向OLTP(联机事务处理),支持事务,支持二级索引,支持sql,支持主从、Group Replication架构模型(本文全部以Innodb为例,不涉及别的存储引擎)。
HBase:基于HDFS,支持海量数据读写(尤其是写),支持上亿行、上百万列的,面向列的分布式NoSql数据库。天然分布式,主从架构,不支持事务,不支持二级索引,不支持sql。
ElasticSearch:ES是一款分布式的全文检索框架,底层基于Lucene实现,虽然ES也提供存储,检索功能,但许多开发者一直不认为ES是一款数据库,但是随着ES功能越来越强大,与数据库的界限也越来越模糊。天然分布式,p2p架构,不支持事务,采用倒排索引提供全文检索。
数据存储方式
假设有这样一张数据表:
MySQL中要提前定义表结构,也就是说表共有多少列(属性)需要提前定义好,并且同时需要定义好每个列所占用的存储空间。数据以行为单位组织在一起的,假如某一行的某一列没有数据,也需要占用存储空间。
HBase则是以列为单位存储数据,每一列就是一个key-value,HBase的表列(属性)不用提前定义,而且列可以动态扩展,比如人员信息表中需要添加一个新的“address”字段,MySQL需要提前alter表,HBase的话直接插入即可。
ES比较灵活,索引中的field类型可以提前定义(定义mapping),也可以不定义,如果不定义,会有一个默认类型,不过出于可控性考虑,关键字段最好提前定义好。(Solr中必须提前定义好schema.xml文件)
上图简单的展示了数据在MySQL和HBase中存储差异(和真实的情况还有差距),可以看到即使第二条记录的“合约交易量”字段为空,MySQL依然会为该字段保留空间,因为后续有可能会有update语句来更新该记录,补上“合约交易量”内容。而HBase则是把每一列都看做是一条记录,row+列名作为key,data作为value,依次存放。假如某一行的某一个列没有数据,则直接跳过该列。对于稀疏矩阵的大表,HBase能节省空间。
看到这里,读者是否会有一个疑问:使用HBase存储时,假如此时需要添加第二行的“合约交易量”内容,如何实现呢,数据是否连续?后面介绍读写流程会解释。
不一样的ES
说完MySQL、HBase,这里要重点说一下ES,ES的存储方式和上面两个都不一样,MySQL和HBase是将数据按不同的方式进行存储,好歹它们存的还是数据,而ES则存的是倒排索引。我们先来了解一下什么是倒排索引,以及为什么需要倒排索引(Inverted Index):
我们肯定都会这样的经历:偶然看到一段很好的文字,但是却不知道出处,这时候去图书馆,一个一个翻找,无疑是大海捞针,这个时候怎么办呢,于是便有了全文检索这项技术,而它最核心的就是倒排索引。假如有如下文档:
我们想要知道有哪些文档含有you这个关键字,首先可以创建一个倒排索引,格式如下:
我们把前面的部分叫做dictionary(字典),里面的每个单词叫做term,后面的文档列表叫做psoting-list,list中记录了所有含有该term的文档id,两个组合起来就是一个完成的倒排索引(Inverted Index)。能够看出,假如需要查找含有“you”的文档时,根据dictionary然后找到对应的posting-list即可。
而全文检索中,创建Inverted Index是最关键也是最耗时的过程,而且真正的Inverted Index结构也远比图中展示的复杂,不仅需要对文档进行分词(ES里中文可以自定义分词器),还要计算TF-IDF,方便评分排序(当查找you时,评分决定哪个doc显示在前面,也就是所谓的搜索排名),压缩等操作。每接收一个document,ES就会将其信息更新在倒排索引中。
从这里我们就可以看出ES和MySQL、HBase的存储还是有很大的区别。而且ES不仅包含倒排索引,默认同时还会把文档doc存储起来,所以当我们使用ES时,也能拿到完整的文档信息,所以某种程度上,感觉就像在使用数据库一样,但是也可以配置不存储文档信息,这时只能根据查询条件得到文档id,并不能拿到完整的文档内容。
总结:
MySQL行存储的方式比较适合OLTP(联机事务处理)业务。列存储的方式比较适合OLAP(联机分析处理)业务,而HBase采用了列族的方式平衡了OLTP和OLAP,支持水平扩展,如果数据量比较大、对性能要求没有那么高、并且对事务没有要求的话,HBase也是个不错的考虑。ES默认对所有字段都建了索引,所以比较适合复杂的检索或全文检索,例如在真格量化中对交易标的tick数据的检索。
读写方式
数据存储方式和读写方式很大程度上决定了系统的吞吐,本节主要介绍MySQL、HBase、ES各自是如何读写数据的。
Mysql
先说说MySQL,MySQL的Innodb中的数据是按主键的顺序依次存放,主键即为聚簇索引,索引采用B+树结构进行组织。
从图中可以看出,数据是按聚簇索引顺序依次存放,假设下面一些场景:
1.查询
Innodb中主键即为聚簇索引,假如根据主键查询,聚簇索引的叶子节点存放就是真正的数据,可以直接查到相应的记录。
假如是二级索引查询,那么需要先通过二级索引找到该记录的主键,然后根据主键通过聚簇索引找到对应的记录,这里多了一个索引查找的过程。
2.插入
顺序插入:因为Innodb的数据是按聚簇索引的顺序依次存放的,如果是根据主键索引的顺序插入,即插入的数据的主键是连续的,因为是顺序io,所以插入效率会较高。
随机插入:假如每次插入的数据主键是不连续的,MySQL需要取出每条记录对应的物理block,会引起大量的随机io,随机io操作和顺序io的性能差距很大,尤其是机械盘。
注意,这也是为什么MySQL的主键通常定义为自增id,不涉及业务逻辑,这样新数据插入时能保证是顺序io。另外MySQL为了提高随机io的性能,提供了insert buffer的功能。
3.更新 & 删除
update和delete如果不是顺序的话,也会包含大量的随机io,当然MySQL都针对随机io都进行了一些优化,尽量减少随机io带来的性能损失。
HBase
HBase不支持二级索引,它只有一个主键索引,采用LSM树。
HBase是一个分布式系统,这点跟MySQL不同,它的数据是分散不同的server上,每个table由一个或多个region组成,region分散在集群中的server上,一个server可以负责多个region。
这里有一点需要特别注意:table中各个region的存放数据的rowkey(主键)范围是不会重叠的,可以认为region上数据基于rowkey全局有序,每个region负责它自己的那一部分的数据。
1.查询
假如我们要查询rowkey=150的这条记录,首先从zk中获取hbase:meta表(存放region和key的对应关系的元数据表)的位置,通过查询meta表得知rowkey=150的数据在哪个server的哪个region上。
2.插入
上图粗略的展示了HBase的region的结构,region不单单是一个文件,它是由一个memstore和多个storeFile组成(storeFile上的上限可以配置)。插入数据时首先将数据写入memstore,当memstore大小达到一定阈值,将memstore flush到硬盘,变成一个新的storeFile。flush的时候会对memstore中的数据进行排序,压缩等操作。可以看到单个storeFile中的数据是有序的,但是region中的storeFile间的数据不是全局有序的。
这样有的好处就是:不管主键是否连续,所有的插入一律变成顺序写,大大提高了写入性能。
看到这里读者可能会有一个疑问:这种写入方式导致了一条记录如果不是一次性插入,很可能分散在不同的storeFile中,那在该region上面查询一条记录时,怎么知道去找哪个storeFile呢?答案就是:全部查询。HBase会采用多路归并的方式,对该region上的所有storeFile进行查询,直到找到符合条件的记录。所以HBase的拥有很好的写入性能,但是读性能较差。
当然HBase也做了很多优化,比如每个storeFile都有自己的index、用于过滤的bloom filter、compaction:按可配置的方式将多个storeFile合并成一个,减少检索时打开的文件数。
3.更新 & 删除
HBase将更新和删除也全部看做插入操作,用timestamp和delete marker来区分该记录是否是最新记录、是否需要删除。也正是因为这样,除了查询,其他的操作统一转换成了顺序写,保证了HBase高效的写性能。
ES
ES的也是一个分布式系统,与ES类似的还有一个叫Solr的项目,都是基于Lucene的全文检索分布式框架。
上如展示了ES和传统数据库的概念对比。下面的介绍中,统一使用index对应DB中table,doc对应table中的记录,field对应row中的一列。
ES集群由一个或多个node组成,一个node即为一个ES服务进程。一个index由多个分片shard组成,shard分散在各个node上面,每个shard都采用Lucene来创建倒排索引,维护各自的索引数据。
图中的一个小方框即为一个shard,出于容灾考虑,每个shard都会有多副本,副本个数可以配置,默认为2,绿色的即为primary shard,灰色的即为replica shard。
1.插入
对于写入,由于有多个shard,请求过来时,如何判断写入到哪个shard呢,ES中每个doc都会有一个唯一id,默认会对id取hash值,根据shard的个数mode到对应的shard上,默认情况下shard中的数据id不是全局有序的,这点和Mysql、HBase有很大区别。
ES的写入和HBase有些类似,也是将所有的写操作变成顺序写,也是先将数据写入内存,然后一段时间后会将内存数据flush到磁盘,磁盘的索引文件会定时进行merge,保证索引文件不会过多而影响检索性能。
另外提一点,数据存入ES后并不是立马就能检索到,这点跟MySQL和HBase,或者说跟数据库系统是完全不一样的。主要是因为由于Inverted Index结构的复杂,需要一个专门的indexReader来查询数据,但是indexReader是以snapshot的方式打开的索引,也就是说indexReader看不到之后的新数据。所以ES提供了一个refresh功能,refresh会重新打开indexReader,使其能够读到最新的数据。默认refresh的间隔是1s,所以ES自称是近实时检索功能。
说到顺序写,这时候读者可能会想:那ES的写入速度和HBase是否就差不多?并非如此,不止不如而且差的还不是一点点,因为ES多了两个最关键的步骤:build index和refresh index!这两个过程是很耗时的: build index时需要分词、计算权重等复杂的操作(对inverted index创建,检索感兴趣的,可以参考《信息检索导论》)。而refresh会重新打开index,这两个过程加起来导致ES接收文档的速率并不高(可以通过bulk方式来加快数据导入)。但也正是因为这些过程才使ES有强大的检索功能。(虽然insert慢,但是支持的检索功能多)
2.读取
每个node都可以接收读request,然后该node会把request分发到含有该index的shard的节点上,对应的节点会查询、并计算出符合条件的文档,排序后结果汇聚到分发request的node(所以查询请求默认会轮循的将发送到各个节点上,防止请求全部打到一个节点),由该node将数据返回给client。(ES也支持指定shard查询,默认是根据文档id进行路由,相当于主键查询,但是假如不能确定数据在哪个shard上时,还是需要查询所有shard)
这里要强调一下,由于ES支持全文检索,根据Inverted Index的特性,大部分情况下,一个关键字对应了很多的doc,如果全部返回,数据量较大,会对集群造成较大压力,所以ES默认只返回权重最高的前20条记录(可配置),也可以通过scroll功能获取全部数据。类似的场景跟我们平时使用baidu、google是一样的,我们使用搜索引擎时,往往是希望得到关联性最强的top N文档,并不关心全部文档有多少个,这也是为什么要计算权重的原因。
现在的ES的功能越来越丰富,不仅仅包含全文检索的功能,而且还有统计分析等功能,说它是全文检索框架吧,它比全文检索功能要丰富,说它是数据库吧,但是它不支持事务,只能说现在各个框架之间的界限越来越模糊了。
3.更新 &删除
ES的更新和删除和HBase类似,也是全部看做是插入操作,通过timestamp和delete marker来区分。
有人会问,既然这种将更新删除统一变成顺序写的方式能够提高写性能,那它难道没有什么坏处吗?
答案是还是会有,这种方式能够有效的提升写性能,但是存在一个很大的问题就是后台经常会需要merge,而merge是一个非常耗资源的过程,对于某些稳定性要求较高的业务来说,这是不能接受的,但是不merge的话,又会降低查询性能(过多的小文件影响查询性能)。目前通用的做法是尽量选择业务低峰期进行merge操作。
使用场景
说了这么多,其实还是希望对MySQL,HBase,ES各自的实现做下对比,方便我们根据业务特点选择最合适的存储、检索方案。下面说一下开发者在工作中使用的经验:
MySQL在三款中最为成熟,而且支持事务,支持二级索引,容灾备份方案也最为成熟,所以线上核心业务Mysql是不二之选(当然如果不差钱,Oracle也挺不错)。
HBase因为其强大的写入能力和水平扩展能力,比较适合存储日志,用户行为等数据量比较大的数据,这种数据一般不涉及事务级别的读写,对二级索引的需求也不是很高。而且HBase的主键不像Mysql,往往是涉及到业务逻辑的,如果查询条件单一的话,可以把直接把需要查询的字段作为主键的一部分,类似MySQL的联合索引,来提供检索功能。
ES现在不仅提供全文检索,还提供统计功能,并且提供的Restful接口非常好用,配上Kibana还可以进行图形化展示,第三方插件也很丰富。虽然ES可以水平扩展,但是考虑到ES的大部分检索都会检索该index的所有shard,如果单个index数据过大,性能多少也会受到影响,所以单个index的大小最好控制在一定的范围,比如存储用户行为日志的index,可以每隔一段时间归一次档,创建新的index,做到冷热分离。而且ES也可以作为MySQL或HBase的索引来使用,虽然Mysql也有索引功能,但是过多的索引往往会拖累MySQL的性能,并且线上MySQL数据库一般也不允许执行统计类的sql,这时可以用ES辅助实现统计,HBase因为只有主键检索,所以更需要二级索引的功能。
— — — — — — E N D — — — — — —
真格量化可访问:
https://quant.pobo.net.cn
真格量化微信公众号,长按关注:
遇到了技术问题?欢迎加入真格量化Python技术交流QQ群 726895887
往期文章:
Numpy处理tick级别数据技巧
真正赚钱的期权策略曲线是这样的
多品种历史波动率计算
如何实现全市场自动盯盘
AI是怎样看懂研报的
真格量化策略debug秘籍
真格量化对接实盘交易
常见高频交易策略简介
如何用撤单函数改进套利成交
Deque提高处理队列效率
策略编程选Python还是C++
如何用Python继承机制节约代码量
十大机器学习算法
如何调用策略附件数据
如何使用智能单
如何扫描全市场跨月价差
如何筛选策略最适合的品种
活用订单类型规避频繁撤单风险
真格量化回测撮合机制简介
如何调用外部数据
如何处理回测与实盘差别
如何利用趋势必然终结获利
常见量化策略介绍
期权交易“七宗罪”
波动率交易介绍
推高波动率的因素
波动率的预测之道
趋势交易面临挑战
如何构建知识图谱
机器学习就是现代统计学
AI技术在金融行业的应用
如何避免模型过拟合
低延迟交易介绍
架构设计中的编程范式
交易所视角下的套利指令撮合
距离概念与特征识别
气象风险与天气衍生品
设计量化策略的七个“大坑”
云计算在金融行业的应用
机器学习模型评估方法
真格量化制作期权HV-IV价差
另类数据介绍
TensorFlow中的Tensor是什么?
机器学习的经验之谈
用yfinance调用雅虎财经数据
容器技术如何改进交易系统
Python调用C++
如何选择数据库代理
统计套利揭秘
一个Call搅动市场?让我们温习一下波动率策略
如何用真格量化设计持仓排名跟踪策略
还不理解真格量化API设计?我们不妨参考一下CTP平台
理解同步、异步、阻塞与非阻塞
隐波相关系数和偏度——高维风险的守望者
Delta中性还不够?——看看如何设计Gamma中性期权策略
Python的多线程和多进程——从一个爬虫任务谈起
线程与进程的区别
皮尔逊相关系数与历史K线匹配
Python2和Python3的兼容写法
Python代码优化技巧
理解Python的上下文管理器
如何写出更好的Python代码?这是Python软件基金会的建议
评估程序化模型时我们容易忽视的指标
看看如何定位Python程序性能瓶颈
什么是Python的GIL
投资研究中的大数据分析趋势及应用
理解CTP中的回调函数
如何围绕隐含波动率设计期权交易策略
看看如何用Python进行英文文本的情感分析
算法交易的分类
Python编码的最佳实践总结
什么是波动率锥?如何用波动率锥设计期权策略?
期权的波动率策略与时间价值收集策略对比
期权用于套期保值和无风险套利
隐含波动率对期权策略的影响
卖出期权交易的风险管理原则和技巧
期权交易中的“大头针”风险
期权做市商策略简介
精细化您的交易——交易成本评估与交易执行策略
海外市场交易执行策略的实践
设计期权套期保值方案时应注意的问题
美式期权、欧式期权比较分析——定价与风险管理
构建您的AI时代武器库——常用的机器学习相关Python库
期权波动率“微笑曲线”之谜
运算任务愈发繁重,如何加速Python程序运行?
证券市场微观结构理论模型是什么
是瞬间成交还是漫长等待?——如何衡量市场流动性
波动率指数及其衍生品介绍
Python的异常处理技巧
Python中的阻塞、异步与协程
“香草”之外的更多选择——几种常见的路径依赖奇异期权
什么是CTP?——了解上期所CTP快速交易系统
了解季节性——以谷物和油籽为例
是前因还是后果?——在真格量化中进行格兰杰因果检验
Python导入模块的技巧
Python程序员常犯的十个错误
搜索数据泄露天机?——舆情指数与期货行情关联性分析思路
机器学习常见算法分类汇总
如何使用Data Pipeline 自动化数据处理工作?
CTP API的委托介绍和在真格量化中的订单流控制
高频交易对市场的影响
期货行情及其组织形式——以上期所为例
理解并行与并发
郑商所和大商所套利指令及在真格量化的实现
机器学习用于金融市场预测面临的挑战
高频交易中风险控制的常用措施
查询结果偏离预期?来了解CTP的报单函数及委托状态查询
Python中的ftplib模块
理解真格量化的Python编程范式