金融市场 L1/L2 的报价和交易数据是量化交易研究非常重要的数据。国内全市场 L1/L2 的历史数据约为 20~50T,每日新增的数据量约为 20~50G。传统的关系数据库如 MS SQL Server 或 MySQL 均无法支撑这样的数据量级,即便分库分表,查询性能也远远无法达到要求。例如 Impala 和 Greenplum 的数据仓库,以及例如 HBase 的 NoSQL 数据库,可以解决这个数据量级的存储,但是这类通用的存储引擎缺乏对时序数据的友好支持,在查询和计算方面都存在严重的不足,对量化金融普遍采用的 Python 的支持也极为有限。
数据库的局限性使得一部分用户转向文件存储。HDF5,Parquet 和 pickle 是常用的二进制文件格式,其中 pickle 作为 Python 对象序列化/反序列的协议非常高效。由于 Python 是量化金融和数据分析的常用工具,因此许多用户使用 pickle 存储高频数据。但文件存储存在明显的缺陷,譬如大量的数据冗余,不同版本之间的管理困难,不提供权限控制,无法利用多个节点的资源,不同数据间的关联不便,数据管理粒度太粗,检索和查询不便等等。
目前,越来越多的券商和私募开始采用高性能时序数据库 DolphinDB 来处理高频数据。DolphinDB 采用列式存储,并提供多种灵活的分区机制,可充分利用集群中每个节点的资源。DolphinDB 的大量内置函数对时序数据的处理和计算非常友好,解决了传统关系数据库或 NoSQL 数据库处理时序数据方面的局限性。使用 DolphinDB 处理高频数据,既可以保证查询与计算的超高性能,又可以提供数据管理、权限控制、并行计算、数据关联等数据库的优势。
本文测试 DolphinDB 和 pickle 在数据读取方面的性能。与使用 pickle 文件存储相比,直接使用 DolphinDB 数据库,数据读取速度可最多可提升 10 倍以上;若为了考虑与现有 Python 系统的集成,使用 DolphinDB 提供的 Python API 读取数据,速度最多有 2~3 倍的提升。有关 DolphinDB 数据库在数据管理等方面的功能,读者可参考 DolphinDB 的在线文档或教程。
1. 测试场景和测试数据
本次测试使用了以下两个数据集。
数据集 1 是美国股市一天(2007.08.23) Level 1 的报价和交易数据。该数据共 10 列,其中 2 列是字符串类型,其余是整型或浮点数类型,存储在 dolphindb 中的表结构如下表,一天的数据约为 2 亿 3000 万行。csv 文件大小为 9.5G,转换为 pickle 文件后大小为 11.8G。
数据集 2 是中国股市 3 天(2019.09.10~2019.09.12)的 Level 2 报价数据。数据集总共 78 列,其中 2 列是字符串类型,存储在 dolphindb 中的表结构如下表,一天的数据约为 2170 万行。一天的 csv 文件大小为 11.6G,转换为 pickle 文件后大小为 12.1G。
DolphinDB database 的数据副本数设为 2。将这两个数据集写入 DolphinDB 后,磁盘占用空间为 10.6G,单份数据仅占用 5.3G,压缩比约为 8:1。pickle 文件没有采用压缩存储。测试发现 pickle 文件压缩后,加载时间大幅延长。
对比测试查询一天的数据。对 DolphinDB Python API 与 pickle,记录从客户端发出查询到接收到数据并转换成 Python pandas 的 DataFrame 对象的耗时。
对于 DolphinDB Python API,整个过程包括三个步骤:
从 DolphinDB 数据库查询数据耗时,即若不使用 Python API 而直接使用 DolphinDB 查询所需耗时;
把查询到的数据从 DolphinDB 数据节点发送到 python API 客户端需要的时间;
在客户端将数据反序列化成 pandas DataFrame 需要的时间。
对于 pickle,耗时为使用 pickle 模块加载 pickle 数据文件所需要的时间。
2. 测试环境
测试使用的三台服务器硬件配置如下:
主机:PowerEdge R730xd
CPU:E5-2650 24cores 48 线程
内存:512G
硬盘:HDD 1.8T * 12
网络:万兆以太网
OS:CentOS Linux release 7.6.1810
本次测试使用的是 DolphinDB 多服务器集群模式。共使用 3 台服务器,每台服务器部署 2 个数据节点,每个节点分配 2 个 10K RPM 的 HDD 磁盘,内存使用限制为 32G,线程数设置为 16。数据库的副本数设置为 2。测试的客户机安排在其中的一台服务器上。
本次测试的 DolphinDB 服务器版本是 1.30.0,Python API for DolphinDB 的版本是 1.30.0.4。
3. 测试方法
读写文件之后,操作系统会缓存相应的文件。从缓存读取数据,相当于从内存读取数据,这会影响测试结果。因此每一次测试前,都会清除操作系统的缓存和 DolphinDB 的缓存。为了对比,也测试有缓存时的性能,即不存在磁盘 IO 瓶颈时的性能。
3.1 测试 DolphinDB
测试代码如下:
测试步骤如下:
使用 Linux 命令
sudo sh -c "echo 1 > /proc/sys/vm/drop_caches"
清理操作系统缓存,可能需要在 root 用户下执行。执行
pnodeRun(clearAllCache)
清理 DolphinDB 的数据库缓存。执行查询脚本,此时为无操作系统缓存时的结果。
再次执行
pnodeRun(clearAllCache)
清理 DolphinDB 的数据库缓存。再次执行查询脚本,此时为有操作系统缓存时的结果。
3.2 测试 DolphinDB 的 Python API
测试代码如下:
测试步骤如下:
使用 Linux 命令
sudo sh -c "echo 1 > /proc/sys/vm/drop_caches"
清理操作系统缓存,可能需要在 root 用户下执行。执行
pnodeRun(clearAllCache)
清理 DolphinDB 的数据库缓存。执行查询脚本,此时为无操作系统缓存时的结果。
再次执行
pnodeRun(clearAllCache)
清理 DolphinDB 的数据库缓存。再次执行查询脚本,此时为有操作系统缓存时的结果。
3.3 测试 pickle 文件
pickle 测试代码如下:
测试步骤:
第一次执行测试代码,此为 pickle 无操作系统缓存时的结果。
第二次执行 pickle 模块读取数据的脚本,此时为 pickle 有操作系统缓存时的结果。
4. 测试结果分析
以下是读取美国股市 Level 1 数据集的测试结果:
以下读取中国股市 Level 2 数据集的测试结果:
4.1 DolphinDB 的性能优势来源
从测试结果看,直接从 DolphinDB 数据库查询,速度最快,超过 pickle 查询的 10 倍以上。在没有操作系统缓存的情况下(大部分的实际场景),DolphinDB Python API 的查询速度明显优于 pickle。在有缓存的情况下,两者相差无几。有无缓存,对 DolphinDB Python API 没有显著的影响,但是对 pickle 却有显著的影响。这些结果从 DolphinDB Python API 和 pickle 的耗时构成,可以得到进一步的解释。
pickle 文件存储在单个 HDD 裸盘上,读取性能的极限速度在每秒 150MB~200MB 之间。读取一个 12G 大小的 pickle 文件,需要 70 秒左右的时间。可见在当前配置下,pickle 文件读取的瓶颈在磁盘 IO。因此当有操作系统缓存时(等价于从内存读取数据),性能会有大幅提升,耗时主要是 pickle 文件的反序列化。要提高读取 pickle 文件的性能,关键在于提升存储介质的吞吐量,譬如改用 SSD 或者磁盘阵列。
DolphinDB 数据库与 Python API 客户端之间采用了改良的 pickle 协议。如前所述,使用 DolphinDB Python API 进行查询可分为 3 个步骤,其中步骤 2 和 3 是可以同时进行的,即一边传输一边反序列化。因此使用 Python API 从 DolphinDB 数据库查询的总耗时约等于第 1 个步骤查询耗时与第 2 和 3 个步骤的较大值之和。在两个数据集的测试中,无论是否有缓存,DolphinDB 数据库查询部分的耗时均在 5~6 秒左右。一天的数据量在 DolphinDB 数据库中约为 8G,分三个节点存储,每个节点的数据量约为 2.7G。从一个节点查询,需要传输的数据量约为 5.4G(本地节点不需要网络传输),对万兆以太网而言,对应 5~6 秒的传输时间。因此当前的配置下,DolphinDB 数据库查询的瓶颈在于网络,而不是磁盘 IO。一天的数据量压缩之后约 1.4G,分布于 12 个磁盘中,按照每个磁盘 100mb/s 的吞吐量,加载一天数据的磁盘时间约在 1.2 秒,远远低于网络需要的 5~6 秒。简而言之,DolphinDB 时序数据库通过压缩技术和分布式技术,大大缩短了加载一天的金融市场数据的时间,使得磁盘 IO 不再成为数据查询的瓶颈。
如前所述,DolphinDB Python API 的反序列化也采用了 pickle 协议,但是进行了改良,比原版的 pickle 协议节约了 5~6 秒时间,正好抵消了数据库查询消耗的 5~6 秒时间。所以在有缓存的情况下,pickle 和 DolphinDB Python API 耗时几乎相等。如果要进一步提升 DolphinDB Python API 的查询性能,有两个方向:
采用更高速的网络,譬如从 10G 升级到 100G,第一步查询的耗时可能从现在的 5~6 秒缩减到 2 秒。
继续改良 pickle 协议。
4.2 字符串对性能的影响
以数据集 1 为例,在没有缓存的情况下,DolphinDB Python API 总共耗时 38 秒,但是数据库端的耗时仅 5~6 秒,80%的时间耗费在 pickle 反序列化上。通过进一步分析,我们发现字符串类型对 pickle 的反序列化有极大的影响。如果查询时,剔除两个字符串类型字段,数据集 1 的时间缩短到 19 秒,减少了一半。也就是说两个字符串字段,以 20%的数据量,占了 50%的耗时。数据集 2 总共 78 个字段,其中 2 个字段是字符串类型,如果不查询这两个字段,时间可从 22 秒缩减到 20 秒。
以下是读取美国股市 Level 1(去除字符串字段)数据集的测试结果:
以下是读取中国股市 Level 2(去除字符串字段)数据集的测试结果:
剔除字符串字段,对提升 pickle 的查询也有帮助。在数据集 1 有缓存的情况下,耗时缩短一半。但是在没有缓存的情况下,提升有限,原因是瓶颈在磁盘 IO。
这两个数据集中的字符串类型数据分别是股票 ID 和交易所名称。股票 ID 和交易所名称的个数极其有限,重复度非常高。DolphinDB 数据库专门提供了一个数据类型 SYMBOL 用于优化存储此类数据。SYMBOL 类型为一个具体的 Vector 或 Table 配备一个字典,存储全部不重复的字符串,Vector 内部只存储字符串在字典中的索引。在前面的测试中,字符串类型数据已经启用了 SYMBOL 类型。如果改用 STRING 类型,DolphinDB 的性能会降低。尽管通过 SYMBOL 类型的优化,为 DolphinDB 服务端的查询以及 pickle 的序列化节约了不少时间,但是 pickle 的反序列化这个步骤并没有充分利用 SYMBOL 带来的优势,存在大量的 python 对象多次 copy,这是进一步优化的方向之一。
4.3 多任务并发下的性能对比
在实际工作中,经常会多个用户同时提交多个查询。为此我们进一步测试了 DolphinDB Python API 和 pickle 在并发查询下的性能。测试采用了数据集 2。对于 DolphinDB Python API,我们分别测试了连接本地服务器的数据节点进行并发查询,以及连接不同服务器的数据节点进行并发查询的性能。对于 pickle,我们并发查询了同一个节点同一个磁盘的不同文件。由于全局锁的限制,测试时,开启多个 Python 进程从 pickle 文件或 DolphinDB 数据库加载数据。下表是三个连接并发查询数据集 2 中不同日期的 Level 2 数据的耗时。
pickle 的耗时线性的从 70 秒增加了到了 220 秒。由于磁盘 IO 是 pickle 的瓶颈,在不增加磁盘吞吐量的情况下,读任务从 1 个增加到 3 个,耗时自然也增加到原先的 3 倍。当同一个客户端节点的三个连接连到不同的服务器数据节点时,耗时增加了 50%(10s 左右),主要原因是客户端节点的网络达到了瓶颈。当同一个客户端节点的三个连接接入到同一个服务器数据节点时,耗时增加了 100%(22s 左右),主要原因是客户端节点和接入的数据节点的网络同时达到了瓶颈。
如果要进一步提升 DolphinDB 并发的多任务查询性能,最直接的办法就是升级网络,譬如将万兆以太网升级成 10 万兆以太网。另一个方法是在数据节点之间以及数据节点和客户端之间传输数据时引入压缩技术,通过额外的 CPU 开销提升网络传输的效率,推迟瓶颈的到来。
5. 库内分析的必要性
从前面的测试我们可以看到,无论单任务还是多任务并发,DolphinDB 数据库端的查询耗时占整个查询耗时的 20%左右。因此,如果分析计算能够在数据库内直接完成,或者数据清洗工作在数据库内完成并降低需要传输的数据量,可以大大降低耗时。例如对数据集 1 的查询耗时 38 秒,而在 DolphinDB 数据库端的查询只需 5~6 秒。即使这 5~6 秒也是因为网络瓶颈造成的,在数据库集群的每一个节点上取数据的时间小于 2 秒。如果在每一个节点上完成相应的统计分析,只把最后少量的结果合并,总耗时约为 2 秒左右。但是使用 Python API 从 DolphinDB 数据库获取数据,然后再用 pandas 的单线程来完成数据分析,总耗时约为 50 秒左右。由此可见,面对海量的结构化数据,库内分析可以大幅提高系统的性能。
为支持时序数据的库内分析,DolphinDB 内置了一门完整的多范式脚本语言,包括千余个内置函数,对时间序列、面板数据、矩阵的各种操作如聚合、滑动窗口分析、关联、Pivoting、机器学习等量化金融常用功能均可在 DolphinDB 数据库内直接完成。
6. 结论和展望
数据库在提供数据管理的便捷、安全、可靠等优势的同时,在性能上超越操作系统裸文件业已可行。这主要得益于分布式技术、压缩技术、列式存储技术的应用以及应用层协议的改进,使得磁盘 IO 可能不再成为一个数据库系统最先遇到的瓶颈。
金融市场高频数据的时序特性,使得时序数据库成为最有前景的解决方案。DolphinDB 相比 pickle 这样的文件解决方案,不仅为金融市场高频数据带来了管理上的便利和性能上的突破,其内置的强大的时间序列数据、面板数据处理能力更为金融的应用开发带来了极大的便利。
使用 Python API 进行数据查询的整个链路中,DolphinDB 数据库查询的耗时只占了很小的一部分,大部分时间耗费在最后一公里,即客户端的网络传输和数据序列化/反序列化。要突破这个瓶颈,有几个发展思路:(1)采用数据库内分析技术,数据清洗和基本的数据分析功能选择在数据库内完成,使用分布式数据库的计算能力缩短数据处理时间,并且大幅降低网络传输的数据量。(2)启用类似 Apache Arrow 类似的通用内存数据格式,降低各应用之间序列化/反序列化的开销。
随着数据库技术的发展,尤其是分布式技术的推进,万兆(10G)网络会比预期更早成为数据库系统的瓶颈。在建设企业内网,尤其在部署高性能数据库集群时,可以开始考虑使用 10 万兆(100G)以太网。
数据压缩可以提升磁盘 IO 和网络 IO 的效率,无论在大数据的存储还是传输过程中都非常重要。
字符串类型对数据系统性能有非常大的负面影响。由于字符串类型长度不一致,在内存中不能连续存储(通常每个元素使用独立的对象存储),内存分配和释放的压力巨大,内存使用效率不高,CPU 处理效率低下。长度不一致,也导致无法在磁盘存储中随机读取字符串元素。因此,数据系统的设计中尽可能避免使用字符串类型。如果字符串类型的重复度较高,建议使用类似 DolphinDB 中的 SYMBOL 类型替代。
评论