时序数据库分区教程(一)介绍了DolphinDB Database 的几种分区方式,本文将会详细讲解DolphinDB的分区原则、特殊的分区方案,让用户对DolphinDB分区数据库有更深入的了解。
1.分区原则
分区的总原则是让数据管理更加高效,提高查询和计算的性能,达到低延时和高吞吐量。
1.1 选择合适的分区字段
DolphinDB分区字段的数据类型可以是整型、日期类型和SYMBOL类型。注意,STRING、FLOAT和DOUBLE数据类型不可以作为分区字段。
虽然DolphinDB支持TIME、SECOND、DATETIME类型字段的分区,但是在实际使用中要谨慎使用,避免采用值分区,以免分区粒度过细,将大量时间耗费在创建或查询几百上千万个只包含几条记录的文件目录。
分区字段应当是在业务中相当重要的。例如在证券交易领域,许多任务都与股票交易日期或股票代码相关,因此使用这两个字段来分区比较合理。
1.2 分区粒度不要过大
DolphinDB单个分区支持最大记录条数是20亿条。但合理的记录条数应该远远小于这个数。一个分区内的多个列以文件形式独立存储在磁盘上,通常数据是经过压缩的。使用的时候,系统从磁盘读取所需要的列,解压后加载到内存。如果分区粒度过大,可能会造成多个工作线程并行时内存不足,或者导致系统频繁地在磁盘和工作内存之间切换,影响性能。一个经验公式是,数据节点的可用内存是S,工作线程(worker)的的数量是W,则建议每个分区解压后在内存中的大小不超过S/8W。假设工作内存上限32GB,8工作线程,建议单个分区解压后的大小不超过512MB。
DolphinDB的子任务以分区为单位。因此分区粒度过大会造成无法有效利用多节点多分区的优势,将本来可以并行计算的任务转化成了顺序计算任务。
DolphinDB是为OLAP的场景优化设计的,支持添加数据,不支持对个别行进行删除或更新。如果要修改数据,以分区为单位覆盖全部数据。如果分区过大,降低效率。DolphinDB在节点之间复制副本数据时,同样以分区为单位,分区过大,不利于数据在节点之间的复制。
综上各种因素,建议一个分区未压缩前的原始数据大小控制在100M~1G之间。当然这个数字可结合实际情况调整。譬如在大数据应用中,我们经常看到宽表设计,一个表达到几百个字段,但是在单个应用中只会使用一部分字段。这种情况下,可以适当放大上限的范围。
如果发现分区粒度过大,可以采用几种方法,(1)采用组合分区(COMPO),(2)增加分区个数,(3)将范围分区改为值分区。
1.3 分区粒度不要过小
分区粒度过小,一个查询和计算作业往往会生成大量的子任务,这会增加数据节点和控制节点,以及控制节点之间的通讯和调度成本。分区粒度过小,也会造成很多低效的磁盘访问(小文件读写),造成系统负荷过重。另外,所有的分区的元数据都会驻留在控制节点的内存中。分区粒度过小,分区数过多,可能会导致控制节点内存不足。我们建议每个分区未压缩前的数据量不要小于100M。
譬如股票的高频交易数据若按交易日期和股票代码的值做组合分区,会导致许多极小的分区,因为许多交易不活跃的股票的交易数据量太少。如果将股票代码的维度按照范围分区的方法来切分数据,将多个交易不活跃的股票组合在一个分区内,则可以有效解决分区粒度过小的问题,提高系统的性能。
2.如何把数据均匀分区
当各个分区的数据量差异很大时,会造成系统负荷不均衡,部分节点任务过重,而其他节点处于闲置等待状态。当一个任务有多个子任务时,只有最后一个子任务完成了,才会把结果返回给用户。由于一个子任务对应一个分区,如果数据分布不均匀,可能会增大作业延时,影响用户体验。
为了方便根据数据的分布进行分区,DolphinDB提供了一个非常有用的函数cutPoints(X, N, [freq])。X是一个数据,N表示产生的分组数,freq是与X等长的数组,每个元素对应X中元素出现的频率。这个函数返回具有(N+1)个元素的数组,使得X中的数据均匀地分布在N个组中。
下面的例子中,需要对股票的报价数据按日期和股票代码两个维度做数据分区。如果简单的按股票的首字母进行范围分区,极易造成数据分布不均,因为极少量的股票代码以U, V, X,Y,Z等字母开头。建议使用cutPoints函数根据样本数据来划分分区。
1. // 将2007.08.01这天数据导入
2. t = ploadText(WORK_DIR+"/TAQ20070801.csv")
4. // 选择2007.08.01这天数据的股票代码的分布来计算分组规则
5. t=select count(*) as ct from t where date=2007.08.01 group by symbol
7. // 按照股票代码字母顺序产生128个区间。每个区间内部的数据行数在2007.08.01这天是相当的。
8. buckets = cutPoints(t.symbol, 128, t.ct)
10. // 最后一个区间的结束边界由2007.08.01的数据决定。为排除2007.08.01之后之后有新的将最后一个区间的结束边界替换成不会出现的最大的股票代码。
11. buckets[size(buckets)-1] = `ZZZZZ
13. //buckets的结果如下:
14. //["A",'ABA','ACEC','ADP','AFN','AII','ALTU','AMK',..., 'XEL','XLG','XLPRACL','XOMA','ZZZZZ']
16. dateDomain = database("", VALUE, 2017.07.01..2018.06.30)
17. symDomain = database("", RANGE, buckets)
18. stockDB = database("dfs://stockDBTest", COMPO, [dateDomain, symDomain])
除了使用范围分区的方法,列表分区也是解决数据分布不均匀的有效方法。
3.时序类型分区
时间是实际数据中最常见的一个维度。DolphinDB提供了丰富时间类型以满足用户的需求。当我们以时间类型字段作为分区字段时,在时间取值上需要预留足够的空间以容纳将来的数据。下面的例子,我们创建一个数据库,以天为单位,将2000.01.01到2030.01.01的日期分区。注意,只有当实际数据写入数据库时,数据库才会真正创建需要的分区。
dateDB = database("dfs://testDate", VALUE, 2000.01.01 .. 2030.01.01)
DolphinDB使用时间类型作为分区字段时,还有一个特殊的优点。数据库定义的分区字段类型和数据表实际采用的时间类型可以不一致,只要保证定义的分区字段数据类型精度小于等于实际数据类型即可。比如说,如果数据库是按月(month)分区,数据表的字段可以是month, date, datetime, timestamp和 nanotimestamp。系统自动会作数据类型的转换。
4.不同表相同分区的数据存放于同一节点
在分布式数据库中,如果多个分区的数据表要连接(join)通常十分耗时,因为涉及到的分区可能在不同的节点上,需要在不同节点之间复制数据。为解决这个问题,DolphinDB推出了共存储位置的分区机制。DolphinDB确保同一个分布式数据库里所有表在相同分区的数据存储在相同的节点上。这样的安排,保证了这些表在连接的时候非常高效。DolphinDB当前版本对采用不同分区机制的多个分区表不提供连接功能。
1. dateDomain = database("", VALUE, 2018.05.01..2018.07.01)
2. symDomain = database("", RANGE, string('A'..'Z') join `ZZZZZ)
3. stockDB = database("dfs://stockDB", COMPO, [dateDomain, symDomain])
5. quoteSchema = table(10:0, `sym`date`time`bid`bidSize`ask`askSize, [SYMBOL,DATE,TIME,DOUBLE,INT,DOUBLE,INT])
6. stockDB.createPartitionedTable(quoteSchema, "quotes", `date`sym)
8. tradeSchema = table(10:0, `sym`date`time`price`vol, [SYMBOL,DATE,TIME,DOUBLE,INT])
9. stockDB.createPartitionedTable(tradeSchema, "trades", `date`sym)
上面的例子中,quotes和trades两个分区表采用同一个分区机制。
DolphinDB是为OLAP设计的系统,主要是解决海量结构化数据的快速存储和计算,以及通过内存数据库和流数据实现高性能的数据处理。DolphinDB不适合数据频繁更改的OLTP业务系统。DolphinDB的数据写入与Hadoop HDFS类似,快速在每个分区或文件的末尾批量插入数据。插入的数据会压缩存储到磁盘,一般压缩比例在20%~25%。数据一旦追加到基于磁盘的数据表后,不能快速更新或删除某些符合条件的记录,必须以分区为单位对数据表进行修改。这也是分区原则中提到单个分区不宜过大的原因之一。
5.多副本机制
DolphinDB允许为每一个分区保留多个副本,默认的副本个数是2,可以修改控制节点的参数dfsReplicationFactor来设置副本数量。
设置冗余数据的目的有两个: (1)当某个数据节点失效或者或磁盘数据损坏时,系统提供容错功能继续提供服务; (2)当大量并发用户访问时,多副本提供负载均衡的功能,提高系统吞吐量,降低访问延时。
DolphinDB通过两阶段事务提交机制,确保数据写入时,同一副本在多节点之间的数据强一致性。
在控制节点的参数文件controller.cfg中,还有一个非常重要的参数dfsReplicaReliabilityLevel。 该参数决定是否允许多个副本驻留在同一台物理服务器的多个数据节点上。在development阶段,允许在一个机器上配置多个节点,同时允许多个副本驻留在同一台物理服务器(dfsReplicaReliabilityLevel=0), 但是production阶段需要设置成为1,否则起不到容错备份的作用。
1. // 每个表分区或文件块的副本数量。默认值是2。
2. dfsReplicationFactor=2
4. // 多个副本是否可以驻留在同一台物理服务器上。 Level 0:允许; Level 1:不运行。默认值是0。
5. dfsReplicaReliabilityLevel=0
6.事务机制
DolphinDB对基于磁盘(分布式文件系统)的数据库表的读写支持事务,也就是说确保事务的原子性,一致性,隔离性和持久化。DolphinDB采用多版本机制实现快照级别的隔离。在这种隔离机制下,数据的读操作和写操作互相不阻塞,可以最大程度优化数据仓库读的性能。
为了最大程序优化数据仓库查询、分析、计算的性能,DolphinDB对事务作了一些限制:
首先,一个事务只能包含写或者读,不能同时进行写和读。
其次,一个写事务可以跨越多个分区,但是同一个分区不能被多个writer并发写入。也就是说当一个分区被某一个事务A锁定了,另一个事务B试图再次去锁定这个分区时,系统立刻会抛出异常导致事务B失败回滚。
7.多Writer并行写入
DolphinDB提供了强大的分区机制,单个数据表可以支持几百万的分区数量,这为高性能的并行数据加载创造了条件。特别是当将海量的数据从别的系统导入到DolphinDB时,或者需要将实时数据以准实时的方式写入到数据仓库时,并行加载显得尤为重要。
下面的例子将股票报价数据(quotes)并行加载到数据库stockDB。stockDB以日期和股票代码做复合分区。数据存储在csv文件中,每个文件保存一天的quotes数据。
1. //创建数据库和数据表
2. dateDomain = database("", VALUE, 2018.05.01..2018.07.01)
3. symDomain = database("", RANGE, string('A'..'Z') join `ZZZZZ)
4. stockDB = database("dfs://stockDB", COMPO, [dateDomain, symDomain])
5. quoteSchema = table(10:0, `sym`date`time`bid`bidSize`ask`askSize, [SYMBOL,DATE,TIME,DOUBLE,INT,DOUBLE,INT])
6. stockDB.createPartitionedTable(quoteSchema, "quotes", `date`sym)
8. def loadJob(){
9. fileDir='/stockData'
11. // 到路径下取出数据文件名
12. filenames = exec filename from files(fileDir)
14. // 加载数据库
15. db = database("dfs://stockDB")
17. // 对每个文件,通过文件名产生jobId前缀。
18. // 通过函数submitJob提交后台程序调用loadTextEx将数据加载到stockDB数据库中。
19. for(fname in filenames){
20. jobId = fname.strReplace(".txt", "")
21. submitJob(jobId,, loadTextEx{db, "quotes", `date`sym, fileDir+'/'+fname})
22. }
23. }
25. //通过pnodeRun将loadJob这个任务发送到集群的每个数据节点进行并行加载。
26. pnodeRun(loadJob)
当多个writer并行加载数据时,要确保这些writer不会同时往同一个分区写入数据,否则会导致事务失败。在上面的例子中,每一个文件存储了一天的数据,而quotes表的一个分区字段是日期,从而确保所有加载数据的作业不会产生有重叠的事务。