实时流处理一般是将业务系统产生的数据进行实时收集,交由流处理框架进行数据清洗,统计,入库,并可以通过可视化的方式对统计结果进行实时的展示。传统的面向静态数据表的计算引擎无法胜任流数据领域的分析和计算任务。在金融交易、物联网、互联网/移动互联网等应用场景中,复杂的业务需求对大数据处理的实时性提出了更高的要求。对于这一类高实时性需求的场景,需要一个快速、高效、灵活可控的流式大数据处理平台来支撑。
DolphinDB内置的流数据框架支持流数据发布、订阅、流数据预处理、实时内存计算、复杂指标的滚动窗口计算等,是一个运行高效,使用便捷的流数据处理框架。
与其它流数据系统相比,DolphinDB database 流数据处理系统的优点在于:
本教程包含以下内容:
流数据框架对流数据的管理和应用是基于发布-订阅-消费的模式,通过流数据表来发布数据,数据节点或者第三方的应用可以通过DolphinDB脚本或者 API来订阅消费流数据。
上图展示了DolphinDB的流数据处理框架。把实时数据注入到发布节点流数据表后,发布的数据可以同时供多方订阅消费:
1.1 实时流数据表
DolphinDB实时流数据表可以作为发布和订阅流数据的载体,发布一条消息等价于往流数据表插入一条记录,同时它也可以通过SQL来进行查询和分析。
1.2 发布和订阅
DolphinDB的流数据框架使用了经典的订阅发布模式。每当有新的流数据写入时,发布方会通知所有的订阅方去处理新的流数据。数据节点通过subscribeTable函数来订阅发布的流数据。
1.3 实时聚合引擎
实时聚合引擎指的是专门用于处理流数据实时计算和分析的模块。DolphinDB提供createStreamAggregator函数用于持续地对流数据做实时聚合计算,并且将计算结果持续输出到指定的数据表中,具体如何使用聚合引擎可以参考流数据聚合引擎教程。
要开启DolphinDB支持流数据功能的模块,需要对DolphinDB数据节点增加配置项。
对于发布节点需要的配置项:
maxPubConnections:发布信息节点能够连接的最大节点数。如果maxPubConnections>0,节点可以作为信息发布节点。默认值为0。
persisitenceDir:共享的流数据表保存的路径。如果需要保存流数据表,必须指定该参数。
persistenceWorkerNum:负责以异步模式保存流数据表的工作线程数。默认值为0。
maxPersistenceQueueDepth:异步保存流数据表时,允许的最大消息队列深度。
maxMsgNumPerBlock:当服务器发布或组合消息时,消息块中的最大记录数。
maxPubQueueDepthPerSite:发布节点可容许的最大消息队列深度。
对于订阅节点需要的配置项:
subPort:订阅线程监听的端口号。当节点作为订阅节点时,该参数必须指定。默认值为0。
subExecutors:订阅节点中消息处理线程的数量。默认值为0,表示解析消息线程也处理消息。
maxSubConnections:服务器能够接收的最大的订阅连接数。默认值是64。
subExecutorPooling: 表示执行流计算的线程是否处于pooling模式的布尔值。默认值是false。
maxSubQueueDepth:订阅节点可容许的最大消息队列深度。
2.1 流数据发布
定义一个streamTable,向其写入数据即意味着发布流数据,由于流数据表需要被不同的会话访问,所以要使用share,将流数据表进行共享。下面的例子中,定义并共享流数据表pubTable,向pubTable表写入数据即意味着发布数据:
share streamTable(10000:0,`timestamp`temperature, [TIMESTAMP,DOUBLE]) as pubTable
2.2 流数据订阅
订阅数据通过subscribeTable函数来实现。
语法如下:
subscribeTable([server], tableName, [actionName], [offset=-1], handler, [msgAsTable=false], [batchSize=0], [throttle=1], [hash=-1], [reconnect=false], [filter], [persistOffset=false])
参数说明:
实际情况中,发布者与订阅者的关系有三种可能。下面的例子解释这三种情况的server参数如何构造:
subscribeTable(, "pubTable", "actionName", 0, subTable , true)
发布者与订阅者是同一集群内的不同节点。此处发布节点别名为“NODE2”。
subscribeTable("NODE2", "pubTable", "actionName", 0, subTable , true)
发布者与订阅者不在同一个集群内。此处发布者节点为 (host="192.168.1.13",port=8891)。
pubNodeHandler=xdb("192.168.1.13",8891)
subscribeTable(pubNodeHandler, "pubTable", "actionName", 0, subTable , true)
share streamTable(10000:0,`ts`temp, [TIMESTAMP,DOUBLE]) as subTable
subscribeTable(, "pubTable", "actionName", 0, subTable , true)
share streamTable(10000:0,`ts`temp, [TIMESTAMP,DOUBLE]) as subTable
topic1 = subscribeTable(, "pubTable", "actionName_realtimeAnalytics", 0, subTable , true)
topic2 = subscribeTable(, "pubTable", "actionName_saveToDataWarehouse", 0, subTable , true)
subscribeTable函数的返回值是订阅主题,它是订阅表所在节点的别名,流数据表名称和订阅任务名称(如果指定了actionName)的组合,使用下划线分隔。如果订阅主题已经存在,函数将会抛出异常。当前节点别名为:NODE1,上述例子返回的两个topic内容如下:
topic1:
NODE1/pubTable/actionName_realtimeAnalytics
topic2:
NODE1/pubTable/actionName_saveToDataWarehouse
share streamTable(10000:0,`timestamp`temperature, [TIMESTAMP,DOUBLE]) as pubTable
share streamTable(10000:0,`ts`temp, [TIMESTAMP,DOUBLE]) as subTable1
share streamTable(10000:0,`ts`temp, [TIMESTAMP,DOUBLE]) as subTable2
share streamTable(10000:0,`ts`temp, [TIMESTAMP,DOUBLE]) as subTable3
vtimestamp = 1..100
vtemp = norm(2,0.4,100)
tableInsert(pubTable,vtimestamp,vtemp)
topic1 = subscribeTable(, "pubTable", "actionName1", 102,subTable1 , true)
topic1 = subscribeTable(, "pubTable", "actionName2", -1, subTable2 , true)
topic1 = subscribeTable(, "pubTable", "actionName3", 50,subTable3 , true)//50
从结果看到,subTable1,subTable2都没有数据,subTable3有50条数据,说明只有当offset在从0到数据集记录数之间才能正常起作用,否则订阅会从当前行开始,只有当新数据进入发布表时才能订阅到数据。
def myhandler(msg){
t = select * from msg where temperature>0.2
if(size(t)>0)
subTable2.append!(t)
}
share streamTable(10000:0,`timestamp`temperature, [TIMESTAMP,DOUBLE]) as pubTable
share streamTable(10000:0,`ts`temp, [TIMESTAMP,DOUBLE]) as subTable1
share streamTable(10000:0,`ts`temp, [TIMESTAMP,DOUBLE]) as subTable2
topic1 = subscribeTable(, "pubTable", "actionName1", -1, subTable1 , true)
topic1 = subscribeTable(, "pubTable", "actionName2", -1, myhandler , true)
vtimestamp = 1..10
vtemp = 2.0 2.2 2.3 2.4 2.5 2.6 2.7 0.13 0.23 2.9
tableInsert(pubTable,vtimestamp,vtemp)
从结果可以看到pubTable写入10条数据,subTable1是全部接收了,而subTable2经过myhandler过滤掉了0.13,收到9条数据。
def myhandler1(table){
subTable1.append!(table)
}
def myhandler2(tuple){
tableInsert(subTable2,tuple[0],tuple[1])
}
share streamTable(10000:0,`timestamp`temperature, [TIMESTAMP,DOUBLE]) as pubTable
share streamTable(10000:0,`ts`temp, [TIMESTAMP,DOUBLE]) as subTable1
share streamTable(10000:0,`ts`temp, [TIMESTAMP,DOUBLE]) as subTable2
//msgAsTable = true
topic1 = subscribeTable(, "pubTable", "actionName1", -1, myhandler1 , true)
//msgAsTable = false
topic2 = subscribeTable(, "pubTable", "actionName2", -1, myhandler2 , false)
vtimestamp = 1..10
vtemp = 2.0 2.2 2.3 2.4 2.5 2.6 2.7 0.13 0.23 2.9
tableInsert(pubTable,vtimestamp,vtemp)
//batchSize
share streamTable(10000:0,`timestamp`temperature, [TIMESTAMP,DOUBLE]) as pubTable
share streamTable(10000:0,`ts`temp, [TIMESTAMP,DOUBLE]) as subTable1
topic1 = subscribeTable(, "pubTable", "actionName1", -1, subTable1 , true, 11)
vtimestamp = 1..10
vtemp = 2.0 2.2 2.3 2.4 2.5 2.6 2.7 0.13 0.23 2.9
tableInsert(pubTable,vtimestamp,vtemp)
print size(subTable1)//0
insert into pubTable values(11,3.1)
print size(subTable1)//11
从结果可以看到,当发布数据累计到11条时,数据才进入到subTable1。
batchSize是用来做数据缓冲使用,有时候流数据的写入频率非常高,当消费能力跟不上数据进入的速度时,需要进行流量控制,否者订阅端缓冲区很快会堆积数据并耗光所有的内存。 throttle设定一个时间,根据订阅端的消费速度定时放一批数据进来,保障订阅端的缓冲区数据量稳定。
setStreamTableFilterColumn
函数一起使用。使用setStreamTableFilterColumn
指定流数据表的过滤列,流数据表过滤列在filter中的数据才会发布到订阅端,不在filter中的数据不会发布。filter不支持过滤BOOL类型数据。getTopicProcessedOffset
函数获取。例如,订阅时把persistOffset设置为true。
share streamTable(1000:0, `time`sym`qty, [TIMESTAMP, SYMBOL, INT]) as trades
trades_slave = streamTable(1000:0, `time`sym`qty, [TIMESTAMP, SYMBOL, INT])
topic=subscribeTable(, "trades", "trades_slave", 0, append!{trades_slave}, true,0,1,-1,false,,true)
def writeData(n){
timev = 2018.10.08T01:01:01.001 + timestamp(1..n)
symv =take(`A`B, n)
qtyv = take(1, n)
insert into trades values(timev, symv, qtyv)
}
writeData(6);
select * from trades_slave
time sym qty
----------------------- --- ---
2018.10.08T01:01:01.002 A 1
2018.10.08T01:01:01.003 B 1
2018.10.08T01:01:01.004 A 1
2018.10.08T01:01:01.005 B 1
2018.10.08T01:01:01.006 A 1
2018.10.08T01:01:01.007 B 1
getTopicProcessedOffset(topic);
5
2.3 断线重连
DolphinDB的流数据订阅提供了自动重连的功能,当reconnect设为true时,订阅端会记录流数据的offset,连接中断时订阅端会从offset开始重新订阅。但是,自动重连机制具有一定的局限性。如果订阅端崩溃或者发布端没有对流数据持久化,订阅端无法自动重连。因此,如果要启用自动重连,发布端必须对流数据持久化。启用持久化请参考2.6节。
2.4 发布端数据过滤
发布端可以过滤数据,只发布符合条件的数据。使用setStreamTableFilterColumn
指定流数据表的过滤列,流数据表过滤列在filter中的数据才会发布到订阅端,不在filter中的数据不会发布。目前仅支持对一个列进行过滤。例如,发布端上的流数据表trades只发布symbol为IBM或GOOG的数据:
share streamTable(10000:0,`time`symbol`price, [TIMESTAMP,SYMBOL,INT]) as trades
setStreamTableFilterColumn(trades, `symbol)
trades_slave=table(10000:0,`time`symbol`price, [TIMESTAMP,SYMBOL,INT])
filter=symbol(`IBM`GOOG)
subscribeTable(, `trades,`trades_slave,,append!{trades_slave},true,,,,,filter)
2.5 取消订阅
每一次订阅都由一个订阅主题topic作为唯一标识。如果订阅时topic已存在,那么会订阅失败。这时需要通过unsubscribeTable函数取消订阅才能再次订阅。取消订阅示例如下:
//unsubscribe a local table
unsubscribeTable(,"pubTable","actionName1")
//unsubscribe a remote table
unsubscribeTable("NODE_1","pubTable","actionName1")
若要删除共享的流数据表,可以使用undef函数。
undef("pubStreamTable",SHARED)
2.6 数据持久化
默认情况下,流计算的表把所有数据保存在内存中。随着流数据持续写入,系统可能会出现内存不足的情况。为了避免这个问题,我们可以设置流数据持久化到磁盘。如果流数据表的行数达到设定的界限值,前面一半的记录行会从内存转移到磁盘。持久化的数据支持重订阅,当订阅指定数据下标时,下标的计算包含持久化的数据。流数据持久化另一个重要的功能是流数据的备份和回复,当节点出现异常重启时,持久化的数据会在重启时自动载入到流数据表。
要启动数据持久化,首先要在节点的配置文件中添加持久化路径:
persisitenceDir = /data/streamCache
在脚本中使用enableTablePersistence函数设置针对某一个流数据表启用持久化。 下面的示例针对pubTable表启用持久化,其中asyn=true,compress=true, cacheSize=1000000,即当流数据表达到100万行数据时启用持久化,采用异步方式压缩保存。
对于持久化是否启用异步,需要对持久化数据一致性和性能之间作权衡,当流数据的一致性要求极高时,可以使用同步方式,这样只有保证持久化做完,数据才会进入发布队列;若对实时性要求极高,不希望磁盘IO影响到流数据的实时性,那么可以启用异步方式,只有启用异步方式时,持久化工作线程数persistenceWorkerNum配置项才会起作用,当有多个publisher表需要持久化,增加persistenceWorkerNum可以提升异步保存的效率。
enableTablePersistence(pubTable,true, true, 1000000)
当不需要保存在磁盘上的流数据时,通过clearTablePersistence函数可以删除持久化数据。
clearTablePersistence(pubTable)
当整个流数据写入结束时,可以使用disableTablePersistence命令关闭持久化。
disableTablePersistence(pubTable)
使用getPersistenceMeta函数获取流数据表的持久化细节情况:
getPersistenceMeta(pubTable)
输出的结果是一个字典。
//内存中保留数据记录数
sizeInMemory->0
//启用异步持久化
asynWrite->true
//流数据表总记录数
totalSize->0
//启用压缩存储
compress->true
//当前内存中数据相对总记录数的偏移量,在持久化运行过程中遵循公式 memoryOffset = totalSize - sizeInMemory
memoryOffset->0
//已经持久化到磁盘的数据记录数
sizeOnDisk->0
//持久化路径
persistenceDir->/hdd/persistencePath/pubTable
//hashValue是对本表做持久化的工作线程标识,当配置项persistenceWorkerNum>1时,hashValue可能不为0
hashValue->0
当流数据进入DolphinDB并发布之后,数据的消费者可能是DolphinDB本身的聚合引擎,也可能是第三方的消息队列或者第三方程序。所以DolphinDB提供了Streaming API供第三方程序来订阅流数据。当有新数据进入时,这些通过API的订阅者能够及时的接收到通知,这使得DolphinDB的流数据框架可与第三方的应用做深入的整合。目前DolphinDB提供Java流数据API,后续会逐步支持C++、C#等流数据API。
Java API处理数据的方式有两种:轮询方式(Polling)和事件方式(EventHandler)。
轮询方式示例代码(Java):
PollingClient client = new PollingClient(subscribePort);
TopicPoller poller1 = client.subscribe(serverIP, serverPort, tableName, offset);
while (true) {
ArrayList msgs = poller1.poll(1000);
if (msgs.size() > 0) {
BasicInt value = msgs.get(0).getEntity(2); //取数据中第一行第二个字段
}
}
当每次流数据表有新数据发布时,poller1会拉取到新数据,否则会阻塞在poller1.poll方法这里等待。
事件方式示例代码:
Java API使用预先设定的MessageHandler获取和处理新数据。 首先需要调用者先定义数据处理器Handler,Handler需要实现com.xxdb.streaming.client.MessageHandler接口。
Handler实现示例如下:
public class MyHandler implements MessageHandler {
public void doEvent(IMessage msg) {
BasicInt qty = msg.getValue(2);
//..处理数据
}
}
在启动订阅时,把handler实例作为参数传入订阅函数。
ThreadedClient client = new ThreadedClient(subscribePort);
client.subscribe(serverIP, serverPort, tableName, new MyHandler(), offsetInt);
当每次流数据表有新数据发布时,API会调用MyHandler方法,并将新数据通过msg参数传入。
当流数据通过订阅方式进行数据的实时处理,所有的计算都在后台进行,用户无法直观的看到运行的情况。DolphinDB提供getStreamingStat函数,可以全方位监控流数据状态。
getStreamingStat函数返回的是一个tuple结构,其中包含了pubConns, subConns, persistWorker, subWorkers四个表。
4.1 pubConns
pubConns表是流数据发布者状态监控。每个发布者线程的最大队列深度默认是1000万。
列名称 说明
client 发布端信息,记录发布端IP和端口
queueDepthLimit 发布端数据缓冲区队列最大限制
queueDepth 发布端数据缓冲区队列深度
tables 发布的流数据表,多表通过,号分隔
查看表内容:
getStreamingStat().pubConns
client queueDepthLimit queueDepth tables
192.168.1.61:8086 10,000,000 0 st1,st
pubConns表会列出当前所有的publisher节点,发布队列情况,以及发布的流数据表名称。
4.2 subConns
subConns表是流数据订阅者链接状态监控。
列名称 说明
publisher 发布端信息,记录发布端IP和端口
cumMsgCount 累计订阅消息数
cumMsgLatency 累计消息延迟时间(毫秒)
lastMsgLatency 最后一次订阅数据延迟时间(毫秒)
lastUpdate 最后一次数据更新时间
查看表内容:
getStreamingStat().subConns
publisher cumMsgCount cumMsgLatency lastMsgLatency lastUpdate
local8081 199,980 19,799 10,990 2018.11.21T07:19:59.767945044
这张表列出所有非本地订阅方的链接状态和消息统计信息。
4.3 persistWorkers
persistWorkers 表是持久化工作线程监控。每个持久化工作线程的最大队列深度默认是1000万。
列名称 说明
workerId worker编号
queueDepthLimit 队列深度限制
queueDepth 队列深度
tables 持久化表
只有当持久化启用时,才能通过getStreamingStat获取这张表,这里记录了所有持久化的表信息,这张表的记录数等于persistenceWorkerNum配置数。比如持久化两张数据表,可以通过getStreamingStat().persistWorkers查看。
当persistenceWorkerNum=1时,结果为:
getStreamingStat().persistWorkers
workerId queueDepthLimit queueDepth tables
0 10,000,000 0 st1,st
当persistenceWorkerNum=3时,结果为:
getStreamingStat().persistWorkers
workerId queueDepthLimit queueDepth tables
0 10,000,000 0 st
1 10,000,000 0 st1
2 10,000,000 0
从结果可以看出,persistenceWorkerNum为持久化数据表提供并行化能力。
4.4 subWorkers
subWorkers表是流数据订阅者工作线程监控,这张表每条记录代表一个订阅工作线程。每个订阅者线程的最大队列深度默认是1000万。
列名称 说明
workerId worker编号
queueDepthLimit 订阅端数据缓冲区队列最大限制
queueDepth 订阅端数据缓冲区队列深度
processedMsgCount 已处理消息数量
failedMsgCount 处理失败消息数量
lastErrMsg 上次失败的消息
topics 已订阅主题
配置项subExecutors, subExecutorPooling这两个配置项的对流数据处理的影响,在这张表上可以得到充分的展现。在订阅两张流数据表st、st1时,可以通过getStreamingStat().subWorkers查看。
当subExecutorPooling=false, subExecutors=1时,结果如下:
getStreamingStat().subWorkers
workerId queueDepthLimit queueDepth processedMsgCount failedMsgCount lastErrMsg topics
0 10,000,000 0 0 0 local8081/st/st_act,local8081/st1/st1_act
此时,所有表的订阅消息共用一个线程队列。
当subExecutorPooling=false, subExecutors=2时,结果如下:
getStreamingStat().subWorkers
workerId queueDepthLimit queueDepth processedMsgCount failedMsgCount lastErrMsg topics
0 10,000,000 0 0 0 local8081/st/st_act
0 10,000,000 0 0 0 local8081/st/st_act
此时,各个表订阅消息分配到两个线程队列独立处理。
当subExecutorPooling=true, subExecutors=2时,结果如下:
getStreamingStat().subWorkers
workerId queueDepthLimit queueDepth processedMsgCount failedMsgCount lastErrMsg topics
0 10,000,000 0 0 0 local8081/st/st_act,local8081/st1/st1_act
0 10,000,000 0 0 0 local8081/st/st_act,local8081/st1/st1_act
此时,各个表的订阅消息共享由两个线程组成的线程池。
当有流数据进入时,可以通过这个表观察到已处理数据量等信息:
getStreamingStat().subWorkers
workerId queueDepthLimit queueDepth processedMsgCount failedMsgCount lastErrMsg topics
0 10,000,000 0 100,621 0 local8081/st/st_act,local8081/st1/st1_act
0 10,000,000 0 99,359 0 local8081/st/st_act,local8081/st1/st1_act
当数据流量极大而系统来不及处理时,系统监控中会看到订阅端subWorkers表的queueDepth数值极高,此时系统会按照从订阅端队列-发布端队列-数据注入端逐级反馈数据压力。当订阅端队列深度达到上限时开始阻止发布端数据进入,此时发布端的队列开始累积,当发布端的队列深度达到上限时,系统会阻止流数据注入端写入数据。这时可以通过以下几种方式来调整,使得系统对流数据的处理性能达到最优化。
流数据可视化按功能可以分为两种可视化类型:
现有很多数据可视化的平台都能支持流数据的实时监控,比如当前流行的开源数据可视化框架Grafana, 它可以设定固定时间间隔去请求流数据表,并把数据以动态更新的数字或图表方式展示出来。DolphinDB已经实现了Grafana的服务端和客户端的接口,具体将配置可以参考教程Grafana如何连接 DolphinDB database。