DolphinDB 是一款高性能时序数据库。DolphinDB 集成了功能强大的编程语言和高容量高速度的批流一体数据分析系统,为海量数据(特别是时间序列数据)的快速存储、检索、计算及分析提供一站式解决方案。在实际生产环境中,经常存在数据导入、转换、查询计算,更新等一系列流程任务,各个部分之间存在依赖,如何将这些 DolphinDB 任务按照需求准确、有效率地调度,可以借用 DolphinScheduler 任务调度器。

本文将从生产环境中的一个 ETL 场景出发,将 DolphinScheduler 引入到 DolphinDB 的高可用集群中,通过使用 DolphinScheduler 提供的功能来调度 DolphinDB 的数据 ETL 作业。

1. Apache DolphinScheduler

Apache DolphinScheduler 是一个分布式易扩展的可视化 DAG 工作流任务调度开源系统。该系统适用于企业级场景,提供了一个支持可视化操作任务、工作流和全生命周期数据处理的解决方案,解决了数据研发 ETL 依赖错综复杂,无法监控任务健康状态的问题。 DolphinScheduler 以 DAG(Directed Acyclic Graph,DAG)流式方式组装任务,可以及时监控任务的执行状态,支持重试、指定节点恢复失败、暂停、恢复、终止任务等操作。

1.1 特性

  • 执行定时任务:在生产环境中,一个普遍的需求是周期性地从数据源中提取、转换、加载数据到 DolphinDB,如每一天、每个小时等,DolphinScheduler 可以方便地进行定时管理,满足需求。
  • 执行历史任务:有时候我们由于业务变动,需要将历史数据进行重新计算和加载入 DolphinDB。在这种情况下,我们仅需在 DolphinScheduler Web 界面上定义相应任务的工作流,并定义和传入开始时间和结束时间的参数。通过这个步骤,我们可以处理任意时间段的数据。
  • 并行执行任务:在业务中,我们可能需要同时处理多项数据ETL任务,如同时导入数据到 DolphinDB 的不同表或同一个表的不同分区。DolphinScheduler 允许我们并行执行多项工作流任务,提高执行效率。
  • 高效处理编排:在生产环境中,大多数情况下存在任务之间、工作流之间有条件地执行,比如在 DolphinDB 的数据ETL中,下游任务依赖于上游任务的执行状态做不同的操作,有的任务之间关系错综复杂。可以在 DolphinScheduler 中按照逻辑对工作流进行定义,轻松编排任务之间的关系。

1.2 安装部署

DolphinScheduler 可在单机、单服务器集群、多服务器集群、K8S环境下部署,本节内容将以单机部署流程作为演示内容,仅供参考。

前置条件

  • JDK:安装JDK(1.8+)并配置JAVA_HOME环境变量,DolphinScheduler的启动依赖于该环境变量,同时将其下的bin目录追加到PATH环境变量中。
  • 二进制包:已配置DolphinDB数据源的DolphinScheduler版本,下载链接在https://cdn.dolphindb.cn/downloads/apache-dolphinscheduler-3.1.7-bin.tar.gz
  • 本教程将MySQL作为 DolphinScheduler 持久化的元数据库,因此要保证服务器已安装好MySQL,若没有,需要下载安装,所有平台的mysql下载地址为:MySQL 下载地址

元数据持久化配置

单机服务使用H2数据库来存储元数据,而H2数据库是一种内存级别的数据库,因此当DolphinScheduler程序重启时,会导致之前定义的工作流等内容全部丢失,需要重新定义,造成效率低下和不必要的麻烦。因此,将元数据持久化是非常有必要的,DolphinScheduler支持MySQLPostgreSQL作为元数据的存储数据库,本文以配置MySQL为例,主要有以下流程:

  • 解压DolphinScheduler程序包
tar -xvzf apache-dolphinscheduler-3.1.7-bin.tar.gz
cd apache-dolphinscheduler-3.1.7-bin
  • 进入MySQL,创建数据库和用户
// 创建数据库
CREATE DATABASE dolphinscheduler DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
// 创建用户,并设置密码
CREATE USER 'dolphinscheduler'@'%' IDENTIFIED BY '密码';
// 给用户赋予库的权限
GRANT ALL PRIVILEGES ON dolphinscheduler.* TO 'dolphinscheduler'@'%';
flush privileges;
  • 修改apache-dolphinscheduler-3.1.7-bin/bin/env/dolphinscheduler_env.sh文件设定环境变量,将{user}{password}改为上一步创建的用户名和密码
export DATABASE=mysql
export SPRING_PROFILES_ACTIVE=${DATABASE}
export SPRING_DATASOURCE_URL="jdbc:mysql://127.0.0.1:3306/dolphinscheduler?useUnicode=true&characterEncoding=UTF-8&useSSL=false"
export SPRING_DATASOURCE_USERNAME={user}
export SPRING_DATASOURCE_PASSWORD={password}
  • 修改apache-dolphinscheduler-3.1.7-bin/standalone-server/conf/application.yaml文件中的配置(在文件尾部),上半部分由于这里用的不是postgresql,直接注释掉就好。将{user}{password}改为上面创建的用户名和密码
---
#spring:
# config:
# activate:
# on-profile: postgresql
# quartz:
# properties:
# org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
# datasource:
# driver-class-name: org.postgresql.Driver
# url: jdbc:postgresql://127.0.0.1:5432/dolphinscheduler
# username: root
# password: root ---
spring:
config:
activate:
on-profile: mysql
sql:
init:
schema-locations: classpath:sql/dolphinscheduler_mysql.sql
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/dolphinscheduler?useUnicode=true&characterEncoding=UTF-8
username: {user}
password: {password}
  • 初始化数据库,如果上述步骤没有问题,这里就不会报错:
bash apache-dolphinscheduler-3.1.7-bin/tools/bin/upgrade-schema.sh
  • 执行完成之后进入 MySQL 查询会发现名称为dolphinscheduler的数据库已经生成了很多表格

启动DolphinScheduler单机服务器

注意:启动DolphinScheduler需要依赖多个端口号,分别是:12345、50052、25333、25334、1234、5678

使用lsof -i:<port>检查以上端口号是否被占用,如果有端口被别的进程占用,修改apache-dolphinscheduler-3.1.7-bin/standalone-server/conf/application.yaml中对应的端口

  1. 当配置好以上内容之后,进入执行apache-dolphinscheduler-3.1.7-bin目录并执行以下命令启动:
bash ./bin/dolphinscheduler-daemon.sh start standalone-server

2. 输入以下命令查看是否执行成功

​    a. 运行jps查看相应实例是否已在进程中

​    b. 运行bash ./bin/dolphinscheduler-daemon.sh status standalone-server查看 standalone-server的运行状态

3. 停止运行

bash ./bin/dolphinscheduler-daemon.sh stop standalone-server

4. 启动成功后,在浏览器中输入服务器IP:12345/dolphinscheduler/ui/login进行登录

    a. 默认用户名:admin

​    b. 默认密码:dolphinscheduler123

登陆成功后将会看到如下页面:

  • 重启dolphinscheduler服务,测试已经连接好数据库

1.3 DolphinDB 与 DolphinScheduler 结合

DolphinDB 作为强大的高性能时序数据库,能够高效存储和处理 PB 级的海量数据集,可以通过编写脚本实现数据的处理、存储、因子计算、建模、回测等任务。在实际生产中,源数据下载、数据处理、数据入库、数据校验、指标计算等任务之间存在先后关系和条件关系,如果在 DolphinDB 脚本中编写相关的逻辑关系代码,一来会造成与实际任务不相关的脚本冗余,二来如果实际业务变动,需要增加或删减部分任务,相互依赖的任务之间的关系代码也需要变动,造成更新迭代效率低下。

考虑到这种情况,如果能够将 DolphinDB 与 DolphinScheduler 结合起来,在 DolphinDB 中编写相关任务代码模块,在 DolphinScheduler 上将这些任务按照逻辑编排调度,这样,就能够将任务代码和任务之间逻辑关系分开,每个部分专注于发挥自己的作用,实现更高效地运行维护。

1.3.1 如何创建 DolphinDB 数据源

  1. 在安装部署好DolphinScheduler之后,登录其Web界面,点击数据源中心并点击创建数据源

2. 输入相关参数定义,创建DolphinDB数据源

注意:数据库名和jdbc连接参数不用填,不然会报错 JDBC connect failed。

1.3.2 如何调度 DolphinDB 任务

创建DolphinDB数据源后,需要创建租户、项目、工作流,具体流程可参考:DolphinScheduler 部署流程

定义好工作流之后,点击该工作流进入操作界面,接着在左端拖拽SQL 节点进行DolphinDB任务定义

注意:SQL类型分为查询类型和非查询类型,这两种类型分别适用于不同的使用场景,在本文 2.5.2 小节中会详细介绍。

由于在SQL任务节点中,每次只能执行一行DolphinDB代码。因此,调度DolphinDB任务主要有以下两种途径:

  1. run 函数

假设DolphinDB脚本在服务器上的路径是:/data/script.dos,那么,在SQL语句上可以输入:

run("/data/script.dos");

2. 函数视图

​    a. 在DolphinDB编写函数:

// 在DolphinDB中定义一个函数,用于创建数据库表
def createTable(dbName, tbName){
login("admin", "123456") if(!existsDatabase(dbName)){
db1 = database(, VALUE, 2020.01.01..2021.01.01)
db2 = database(, HASH, [SYMBOL, 10])
db = database(dbName, COMPO, [db1, db2], , "TSDB")
}else{
db = database(dbName)
}
if(!existsTable(dbName,tbName)){
name =`SecurityID`ChannelNo`ApplSeqNum`MDStreamID`SecurityIDSource`Price
`OrderQty`Side`TradeTIme`OrderType`OrderIndex`LocalTime`SeqNo
`Market`DataStatus`BizIndex
type = [SYMBOL, INT, LONG, INT, INT, DOUBLE, INT,
SYMBOL, TIMESTAMP, SYMBOL, INT, TIME, LONG, SYMBOL,INT,LONG]
schemaTable = table(1:0, name, type)
db.createPartitionedTable(table=schemaTable, tableName=tbName,
partitionColumns=`TradeTime`SecurityID,
compressMethods={TradeTime:"delta"},
sortColumns=`Market`SecurityID`TradeTime, keepDuplicates=ALL)
}
else{
writeRunLog("数据库:" + dbName + " 数据表:" + tbName + " 已存在...")
}
}

​    b. 将函数添加为函数视图,作用是使得该函数视图全局可用

// 添加为函数视图
addFunctionView(createTable)

​    c. 在dolphinScheduler上调用该函数视图

// 在SQL语句内输入以下内容:
createTable("dfs://testDb", "testTb");

上述两种方法的区别是:

  • 使用run执行脚本不能传递参数,灵活性较差
  • 使用函数视图可以在DolphinScheduler上面传入参数,以上面为例,可以在SQL语句中输入:
createTable(${dbName}, ${tbName});

传入参数的方法有局部参数和全局参数两种方法,它们的区别是全局参数可以在创建工作流实例时直接进行修改,而局部参数需要进入到工作流中的具体任务节点进行修改:

  1. 局部参数在定义任务节点时定义

2. 全局参数在保存工作流时定义

2. 调度 DolphinDB 数据 ETL 任务

2.1 任务流程结构

  • 文件结构

2.1.1 DolphinDB 功能模块部分

  • stockData: 数据导入模块

    • createStockTable.dos: 定义创建相应的股票数据库表的函数。
    • stockDataProcess.dos: 定义将源数据进行清洗、处理与格式转换的函数。
    • stockDataLoad.dos: 定义将数据导入相应的DolphinDB库表的函数。
  • minFactor: 分钟线因子指标相关模块

    • createMinFactorTable.dos: 定义创建分钟线因子指标库表的函数。
    • computeMinFactor.dos: 定义计算分钟线因子指标的函数。
  • dataCheck: 数据校验模块

    • stockCheck.dos: 定义校验导入的股票逐笔委托、快照行情、逐笔成交数据的函数。
    • minFactorCheck.dos: 定义校验分钟线因子指标数据的函数。

2.1.2 DolphinDB 脚本部分

  • initTable.dos: 执行该脚本可以创建相应的股票数据库表和分钟因子指标库表。
  • createFuncView.dos: 执行该脚本可以定义需要在dolphinScheduler上用到的函数视图。

2.2 数据介绍

本文选取了 20230201 上交所某股票 level 2 委托数据、快照数据、成交数据作为演示。以下是逐笔委托表在DolphinDB的结构。快照数据和成交数据结构可在附件中查看:

字段名 字段含义 数据类型(DolphinDB)
ChannelNo 通道代码 INT
ApplSeqNum 消息记录号 LONG
MDStreamID 行情类别 INT
SecurityID 证券代码 SYMBOL
SecurityIDSource 证券代码源 INT
Price 委托价格 DOUBLE
OrderQty 委托数量 INT
Side 委托买卖方向 SYMBOL
TradeTime 委托时间 TIMESTAMP
OrderType 委托类型 SYMBOL
OrderIndex 委托序号 INT
LocalTime 本地接受时间戳 TIME
SeqNo 消息序列号 LONG
Market 交易市场 SYMBOL
DataStatus 数据状态 INT
BizIndex 业务序列号 LONG

2.3 数据导入、指标计算与校验任务

注意:以下各部分内容均将相关函数定义在模块中,以方便进行工程化管理,关于DolphinDB模块的创建、加载、调用方法,请参照:DolphinDB 模块复用教程

2.3.1 数据清洗、处理、入表

由于源数据的字段结构有时候不符合我们的业务需求,因此需要增删、处理一些字段后再导入数据库,下面以逐笔委托数据为例,介绍源数据经清洗、处理后再导入库表的过程,快照和成交数据的处理逻辑与委托数据相同,详细内容可以在附件中查看。

  • 创建数据库表

逐笔委托、快照、逐笔成交数据都保存在同一个库中,本文采用了组合分区作为分区方案,第一层按天分区,第二层对股票代码分25个哈希分区。如何确定数据分区请参照:DolphinDB数据库分区教程

module createStockTable

// 创建逐笔委托数据存储库表
def createEntrust(dbName, tbName,userName = "admin",password = "123456")
{
login(userName, password)
if(!existsDatabase(dbName))
{
db1 = database(, VALUE, 2020.01.01..2021.01.01)
db2 = database(, HASH, [SYMBOL, 25])
// 按天和股票组合分区
db = database(dbName, COMPO, [db1, db2], , "TSDB")
}
else
{
db = database(dbName)
}
name=`ChannelNo`ApplSeqNum`MDStreamID`SecurityID`SecurityIDSource`Price`OrderQty`Side`TradeTIme`OrderType`OrderIndex`LocalTime`SeqNo`Market`DataStatus`BizIndex
type = [INT, LONG, INT, SYMBOL, INT, DOUBLE, INT, SYMBOL, TIMESTAMP, SYMBOL, INT, TIME, LONG, SYMBOL,INT,LONG]
schemaTable = table(1:0, name, type)
// 创建分区表
db.createPartitionedTable(table=schemaTable, tableName=tbName, partitionColumns=`TradeTime`SecurityID, compressMethods={TradeTime:"delta"}, sortColumns=`Market`SecurityID`TradeTime, keepDuplicates=ALL)
}
  • csv数据清洗与处理
module stockData::stockDataProcess

// 定义逐笔委托csv数据文件中各个字段的名称和字段类型
def schemaEntrust()
{
name = `DataStatus`OrderIndex`ChannelNo`SecurityID`TradeTime`OrderType`ApplSeqNum`Price`OrderQty`Side`BizIndex`LocalTime`SeqNo
typeString = `INT`LONG`INT`SYMBOL`TIME`SYMBOL`INT`DOUBLE`INT`SYMBOL`INT`TIME`INT
return table(name, typeString)
} // 数据处理函数,包括字段增加,数据去重等操作
def processEntrust(loadDate, mutable t)
{
// 字段名替换
t.replaceColumn!(`TradeTime, concatDateTime(day, t.TradeTime))
n1 = t.size()
// 数据去重
t = select * from t where isDuplicated([DataStatus, OrderIndex, ChannelNo, SecurityID, TradeTime, OrderType, ApplSeqNum, Price, OrderQty, Side, BizIndex],FIRST)=false
n2 = t.size()
// 增加字段
update t set Market = `sh
update t set MDStreamID = int(NULL)
update t set SecurityIDSource = int(NULL)
reorderColumns!(t, `ChannelNo`ApplSeqNum`MDStreamID`SecurityID`SecurityIDSource`Price`OrderQty`Side`TradeTime`OrderType`OrderIndex`LocalTime`SeqNo`Market`DataStatus`BizIndex)
return t,n1,n2
}
  • 将处理后的数据导入数据库表
module stockData::stockDataLoad
use stockData::stockDataProcess def loadEntrust(userName, userPassword, startDate, endDate, dbName, tbName, filePath, loadType,mutable infoTb)
{
for(loadDate in startDate..endDate)
{
// 删除已有数据
dateString = temporalFormat(loadDate,"yyyyMMdd")
dataCount = exec count(*) from loadTable(dbName, tbName) where date(tradeTime)=loadDate
// 如果表里面已经存在当天要处理的数据,删除库里面已有数据
if(dataCount != 0){
msg = "Start to delete the entrust data, the delete date is: " + dateString
print(msg)
infoTb.tableInsert(msg) dropPartition(database(dbName), loadDate, tbName)
msg = "Successfully deleted the entrust data, the delete date is: " + dateString
print(msg)
infoTb.tableInsert(msg)
}
// 数据导入
// 判断数据csv文件是否存在
fileName = filePath + "/" + dateString + "/" + "entrust.csv"
if(!exists(fileName))
{
throw fileName + "不存在!请检查数据源!"
}
// 如果是全市场数据,数据量较大,因此分批导入
schemaTB = schemaEntrust()
tmpData1 = loadText(filename=fileName, schema=schemaTB)
tmpData1,n1,n2 = processEntrust(loadDate,tmpData1)
pt = loadTable(dbName,tbName)
msg = "the data size in csv file is :" + n2 + ", the duplicated count is " + (n1 - n2)
print(msg)
infoTb.tableInsert(msg)
for(i in 0..23)
{
startTime = 08:00:00.000 + 1200 * 1000 * i
tmpData2 = select * from tmpData1 where time(TradeTime)>=startTime and time(TradeTime)<(startTime+ 1200 * 1000)
if(size(tmpData2) < 1)
{
continue
}
//数据入库
pt.append!(tmpData2)
}
msg = "successfully loaded!"
print(msg)
infoTb.tableInsert(msg)
}
}

2.3.2 K 分钟线因子指标计算

当导入数据之后,我们希望根据业务、策略对数据进行进一步加工,形成分钟线级别的因子指标,让数据产生价值,驱动业务发展。下面,我们以计算成交数据K分钟线因子指标为例,介绍该任务流程:

  • 创建K分钟线结果存储表
module minFactor::createMinFactorTable

def createMinuteFactor(dbName, tbName)
{
if(existsDatabase(dbName)){
dropDatabase(dbName)
}
//按天分区
db = database(dbName, VALUE, 2021.01.01..2021.01.03,engine = `TSDB)
colName = `TradeDate`TradeTime`SecurityID`Open`High`Low`Close`Volume`Amount`Vwap
colType =[DATE, MINUTE, SYMBOL, DOUBLE, DOUBLE, DOUBLE, DOUBLE, LONG, DOUBLE, DOUBLE]
tbSchema = table(1:0, colName, colType)
db.createPartitionedTable(table=tbSchema,tableName=tbName,partitionColumns=`TradeDate,sortColumns=`SecurityID`TradeTime,keepDuplicates=ALL)
}
  • 计算K分钟线因子指标并入库
module minFactor::computeMinFactor

def calFactorOneMinute(dbName, startDate, endDate, mutable factorTb,mutable infoTb)
{
pt = loadTable(dbName, "trade")
dayList = startDate..endDate
if(dayList.size()>12) dayList = dayList.cut(12)
for(days in dayList){
//计算分钟 K 线
res = select first(TradePrice) as open, max(TradePrice) as high, min(TradePrice) as low, last(TradePrice) as close, sum(tradeQty) as volume,sum(TradePrice*TradeQty) as amount,sum(TradePrice*TradeQty)\sum(TradeQty) as vwap from pt where date(tradeTime) in days group by date(tradeTime) as TradeDate,minute(tradeTime) as TradeTime, SecurityID
msg = "Start to append minute factor result , the days is: [" + concat(days, ",")+"]"
print(msg)
infoTb.tableInsert(msg)
//分钟 K 线入库
factorTb.append!(res)
msg = "Successfully append the minute factor result to databse, the days is: [" + concat(days, ",")+"]"
print(msg)
infoTb.tableInsert(msg)
}
}

2.3.3 数据校验与 K 分钟线指标校验

由于K分钟线因子指标计算依赖于上游导入数据的正确性,而业务中又依赖于K分钟线指标数据的正确性,因此,对这两部分数据进行校验是有必要的,下面介绍部分校验步骤,详细校验内容请参照附件

  • 股票数据校验
module dataCheck::stockCheck 

def checkStockCounts(idate,dbName)
{
// 校验逐笔委托、快照行情、逐笔成交表的股票个数是否一致 getCodes = def (dbName,tbName,idate) {
tb = loadTable(dbName,tbName)
return exec distinct(SecurityID) from tb where date(tradetime)=idate and ((Market=`sh and SecurityID like "6%")or(Market=`sz and (SecurityID like "0%" or SecurityID like "3%" ) ))
}
entrustCodes = getCodes(dbName,"entrust",idate)
tradeCodes = getCodes(dbName,"trade",idate)
snapshotCodes = exec distinct(SecurityID) from loadTable(dbName,"snapshot") where date(tradetime)=idate and ((Market=`sh and SecurityID like "6%")or(Market=`sz and (SecurityID like "0%" or SecurityID like "3%" ))) and HighPrice != 0
if(entrustCodes.size() != snapshotCodes.size() or entrustCodes.size() != tradeCodes.size() or snapshotCodes.size() != tradeCodes.size())
{
throw "逐笔委托股票数量:" + size(entrustCodes) + " 快照行情股票数量:" + size(snapshotCodes) + " 逐笔成交股票数量:" + size(tradeCodes) + ", 它们数量不一致!"
}
}
  • K分钟线指标校验
module dataCheck::minFactorCheck

def checkHighLowPrice(idate,dbName,tbName)
{
// 分钟线最高价指标与最低价指标校验
tb= loadTable(dbName,tbName)
temp=select * from tb where tradedate=idate and High < Low
if(size(temp)>0)
{
throw "分钟线计算错误!分钟线最高价小于最低价!"
}
} def checkVolumeAmount(idate,dbName,tbName)
{
// 分钟线交易量与交易额指标校验
tb = loadTable(dbName,tbName)
temp = select * from loadTable(dbName,tbName) where tradedate=idate and ((Volume == 0 and Amount != 0) or (Volume != 0 and Amount == 0))
if(size(temp)>0)
{
throw "分钟线计算错误!交易量和交易额不同时为0!"
}
}

2.4 实现 DolphinDB 任务调度

在定义好数据ETL模块之后,就可以通过以下步骤实现DolphinDB ETL任务的调度:

  • 创建相关库表

第一次执行时,需要创建相关数据库表

use stockData::createStockTable
use minFactor::createMinFactorTable // 创建数据库表,库表名可以根据实际需要修改
createEntrust("dfs://stockData", "entrust")
createSnapshot("dfs://stockData", "snapshot")
createTrade("dfs://stockData", "trade")
createMinuteFactor("dfs://factorData", "stockMinFactor")
  • 定义函数视图并执行

由于每日定时ETL任务与历史批量ETL任务的整体处理逻辑相同,因此,在定义函数视图时通过传入参数来区分不同类型的任务,以定义逐笔委托数据导入任务函数视图为例,具体如下(全部函数视图定义请参照附件):

use stockData::stockDataLoad
// 定义函数
def loadEntrustFV(userName="admin" , userPassword="123456", startDate = 2023.02.01, endDate = 2023.02.01, dbName = "dfs://stockData", tbName = "entrust", filePath = "/hdd/hdd8/ymchen", loadType = "daily")
{
infoTb = table(1:0,["info"] ,[STRING])
if(loadType == "daily")
{
sDate = today()
eDate = today()
loadEntrust(userName, userPassword, sDate, eDate, dbName, tbName, filePath, loadType,infoTb)
}
else if(loadType == "batch")
{
loadEntrust(userName, userPassword, date(startDate), date(endDate), dbName, tbName, filePath, loadType,infoTb)
}
return infoTb
}

注意:在定义函数视图时,一些默认参数如数据存放路径:filePath 需要根据实际情况进行更改

  • 创建DolphinDB任务节点

创建好每个任务的函数视图之后,每个函数视图对应于DolphinScheduler上的一个任务节点,以逐笔委托数据导入任务为例,我们分以下两种情况:

  1. 对于每日定时任务,由于我们在定义函数视图时已经针对每日定时任务做了默认参数处理,因此仅需在DolphinScheduler任务节点的SQL语句中输入:
loadEntrustFV();

2. 对于历史批量任务,我们需要传入三个参数:开始时间_startDate_,结束时间_endDate_以及任务类型_loadType_,在DolphinScheduler任务节点的SQL语句中需要输入:

loadEntrustFV(startDate=${startDate},endDate=${endDate},loadType="batch");

注意:需要在DolphinScheduler上面定义局部参数或全局参数_startDate_和_endDate_,如何定义请参照本文 1.3.2 小节

  • 创建DolphinDB任务工作流

我们需要在DolphinScheduler上创建两个工作流,一个是定时任务工作流,一个是历史批量任务工作流。在每个工作流中需要根据ETL流程编排具有逻辑关系的任务节点。以历史批量任务为例,创建如下工作流:

创建任务工作流之后,点击运行按钮就可以开始执行,点击定时按钮就可以进行定时管理

运行任务后,工作流实例为绿色代表整个工作流运行成功;黑色则表示存在失败任务,可以通过双击失败的工作流实例查看具体是哪个任务执行失败。

在DolphinScheduler中,可以导入工作流导出工作流,以上介绍的DolphinDB每日任务与批量任务,可以直接通过附件中对应的json文件直接导入。

  • 调度DolphinDB ETL 工作流

导入工作流之后,点击执行按钮就可以开始执行

2.5 获取 DolphinDB 任务调度结果

2.5.1 查看 DolphinDB 任务执行情况

在DolphinScheduler上运行DolphinDB工作流任务之后,可以通过以下步骤查看工作流任务运行情况:

  • 进入工作流界面,可以看到所有工作流实例的状态,在状态栏,齿轮形状代表正在运行,绿色打勾代表工作流任务成功运行,黑色打叉代表工作流任务运行失败。

  • 在工作流实例名称下,点击想要查看的工作流实例,进入该工作流详情界面:

  • 如上图所示,我们可以看到股票委托、快照、成交数据导入任务成功了,但是股票数据校验任务失败了,导致整个工作流任务执行失败。在该任务节点上点击鼠标右键,然后点击查看日志,就可以查看该任务节点具体的报错信息:

2.5.2 获取 DolphinDB 任务运行过程中的信息

在任务执行过程中,在 DolphinScheduler 日志中并不能显示 DolphinDB 脚本中通过print函数输出的信息,但是在实践中大多数情况下存在保存任务运行信息以查看任务具体执行情况的需求。以下内容首先介绍DolphinDB SQL任务节点查询类型非查询类型的特点,然后讲述如何在DolphinScheduler 任务节点的日志信息中显示DolphinDB任务的运行信息。

SQL 任务节点非查询类型

非查询类型主要用于无结果返回的函数,它的特点主要有:

  1. 可以以分段执行符号为界,执行多段代码。
  2. 在任务节点的日志只能在报错时查看报错信息,在任务执行成功时并没有详细的运行信息。

SQL 任务节点查询类型

查询类型主要用于有结果集返回的函数,它的特点主要有:

  1. 在任务节点的日志不仅能在报错时查看报错信息,而且当任务执行成功时,能够在节点日志中查看在DolphinDB脚本中设定的运行信息。
  2. 只能执行一行语句,不能执行多段代码。

如何获取 DolphinDB 任务运行过程中的信息

下面,以股票委托数据导入任务为例,介绍如何在DolphinDB脚本中在不同运行阶段设定运行日志信息,以及在DolphinScheduler上当任务执行完毕之后在日志中显示完整的运行信息。

  1. 在股票委托数据导入模块的函数中引入一个参数,该参数为内存表_infoTb_,当任务执行过程中,将需要记录的运行信息写入该表。
// 模块 stockData::stockDataLoad 的 loadEntrust 函数定义如下:

module stockData::stockDataLoad
use stockData::stockDataProcess def loadEntrust(userName, userPassword, startDate, endDate, dbName, tbName, filePath, loadType,mutable infoTb)
{
for(loadDate in startDate..endDate)
{
// 删除已有数据
dateString = temporalFormat(loadDate,"yyyyMMdd")
dataCount = exec count(*) from loadTable(dbName, tbName) where date(tradeTime)=loadDate
// 如果表里面已经存在当天要处理的数据,删除库里面已有数据
if(dataCount != 0){
msg = "Start to delete the entrust data, the delete date is: " + dateString
print(msg)
// 将运行信息添加到表中
infoTb.tableInsert(msg) dropPartition(database(dbName), loadDate, tbName)
msg = "Successfully deleted the entrust data, the delete date is: " + dateString
print(msg)
infoTb.tableInsert(msg)
}
// 数据导入
// 判断数据csv文件是否存在
fileName = filePath + "/" + dateString + "/" + "entrust.csv"
if(!exists(fileName))
{
throw fileName + "不存在!请检查数据源!"
}
// 如果是全市场数据,数据量较大,因此分批导入
schemaTB = schemaEntrust()
tmpData1 = loadText(filename=fileName, schema=schemaTB)
tmpData1,n1,n2 = processEntrust(loadDate,tmpData1)
pt = loadTable(dbName,tbName)
msg = "the data size in csv file is :" + n2 + ", the duplicated count is " + (n1 - n2)
print(msg)
infoTb.tableInsert(msg)
for(i in 0..23)
{
startTime = 08:00:00.000 + 1200 * 1000 * i
tmpData2 = select * from tmpData1 where time(TradeTime)>=startTime and time(TradeTime)<(startTime+ 1200 * 1000)
if(size(tmpData2) < 1)
{
continue
}
//数据入库
pt.append!(tmpData2)
}
msg = "successfully loaded!"
print(msg)
infoTb.tableInsert(msg)
}
}

2. 在 DolphinDB 脚本中每个函数视图中定义一个内存表,在任务执行过程中,产生一条运行信息时便写入该表,函数执行完毕后返回该表 :

use stockData::stockDataLoad
def loadEntrustFV(userName="admin" , userPassword="123456", startDate = 2023.02.01, endDate = 2023.02.01, dbName = "dfs://stockData", tbName = "entrust", filePath = "/hdd/hdd8/ymchen", loadType = "daily")
{
// 定义运行信息表
infoTb = table(1:0,["info"] ,[STRING])
if(loadType == "daily")
{
sDate = today()
eDate = today()
// 将运行信息表作为参数传入数据导入函数中,将每次需要输出的信息写入该表
loadEntrust(userName, userPassword, sDate, eDate, dbName, tbName, filePath, loadType,infoTb)
}
else if(loadType == "batch")
{
loadEntrust(userName, userPassword, date(startDate), date(endDate), dbName, tbName, filePath, loadType,infoTb)
}
// 返回运行信息表
return infoTb
}

3. 在 DolphinScheduler 工作流中添加SQL查询节点,用于获取运行信息表的内容。主要实现逻辑如下:a. 通过执行股票委托数据导入函数视图,获取运行信息表对象;b. 通过SQL查询节点,将这个运行信息表对象转化格式,以更直观的形式显示在任务节点的日志界面中。为实现以上步骤,可通过两种方法,以下将详细介绍:

​    a. 直接在SQL语句中输入以下代码,将以上介绍的两个步骤整合在一行代码中:

"\n[DOLPHINDB INFO] " + concat(exec * from loadEntrustFV(startDate=${startDate},endDate=${endDate},loadType="batch"),"\n[DOLPHINDB INFO] ");

​    b. 在SQL查询类型的前置任务中用于获取运行信息表,在SQL语句中将该表转化成目标格式。

4. 整个工作流结构图如下所示:

5. 通过鼠标右键点击相应任务节点,选择查看日志选项,可以查看对应DolphinDB任务节点的运行信息。

2.6 DolphinDB 脚本开发注意事项

  1. 当在模块中调用插件的函数时,需要提前在多个数据节点和控制节点导入该插件。
  2. 在 DolphinScheduler 上传入的参数,是以字符串类型存在的。因此,在 DolphinDB 上需要进一步将其改成预期的数据类型,比如:
// 在DolphinScheduler上执行的语句为:
loadSnapshotFV(startDate=${startDate},endDate=${endDate},loadType="batch"); // 由于传入的 startDate 是字符串类型,因此在DolphinDB上定义该函数时需要先转成 Date 类型
use stockData::stockDataLoad
def loadEntrustFV(userName="admin" , userPassword="123456", startDate = 2023.02.01, endDate = 2023.02.01, dbName = "dfs://stockData", tbName = "entrust", filePath = "/hdd/hdd/ymchen", loadType = "daily")
{
if(loadType == "batch")
{
// 使用 date(startDate) 转成 Date 类型
loadEntrust(userName, userPassword, date(startDate), date(endDate), dbName, tbName, filePath, loadType)
}
} // 创建函数视图
addFunctionView(loadEntrustFV)

3. 当模块文件内容更改时,需要按照以下步骤更新函数视图

  • 使用clearCachedModules函数或者重新连接 session 清除之前缓存的模块。
  • 使用dropFunctionView函数删除指定的函数视图。
  • 使用use语句重新导入更改后的模块。
  • 使用addFunctionView函数添加新的视图。

3. DolphinScheduler 与 Airflow 对比

Airflow也是一款具有不错性能的调度软件,关于它与DolphinDB相结合的教程可参照:DolphinDB与Airflow最佳实践。以下是DolphinScheduler与Airflow在一些方面的对比:

功能 Airflow DolphinScheduler
调度模块 自实现 Quartz任务调度库
Job类型 Python、Bash、HTTP、Mysql等,支持Operator的自定义扩展 支持传统的Shell任务,同时支持大数据平台任务调度:MR、Spark、SQL、Python等
Executor触发 Restful Restful
工作流 dag → tasks project → flows → tasks
部署运维 较复杂,包括WebServer、Scheduler、Worker 简单
单点故障 Scheduler存在单点故障风险 去中心化的多Master和多Worker
高可用额外要求 Celery、Dask、Mesos + Load Balancer + DB 不需要(本来就支持)
过载处理 任务太多时会卡死服务器 任务队列机制,单个机器上可调度的任务数量可以灵活配置,当任务过多时会缓存在任务队列中,不会造成机器卡死
DAG监控界面 不能直观区分任务类型 任务状态、任务类型、重试次数、任务运行机器、可视化变量等关键信息一目了然
可视化流程定义 否。通过python代码来绘制DAG,使用不便,对于缺乏编码基础的业务人员存在较高使用门槛 是。所有流程定义都是可视化的,通过拖拽任务来绘制DAG,配置数据源及资源,同时对于第三方系统,提供api方式的操作。
快速部署 集群化部署复杂 一键部署
是否能暂停和恢复 否。只能先将工作流杀死再重新运行 支持暂停、恢复操作,支持停止操作
是否支持集群扩展 是,但是仅支持复杂Executor的水平扩展 是,调度器使用分布式调度,整体的调度能力会随集群的规模线性增长,Master和Worker支持动态上下线
开发语言 Python Java

4.常见问题

1. DolphinScheduler status 显示 running,web 的登陆页面却无法登录

查看standalone-server/logs/目录下对应日期的日志中具体情况

2. DolphinScheduler 设置开机自启动后,服务器重启无法正常启动

正常情况下,由于dolphinScheduler的standalone模式源码原因,需要在服务器重启前到scheduler部署目录执行停止脚本bash ./bin/dolphinscheduler-daemon.sh stop standalone-server; 如果无法正常启动,请通过ps aux | grep dolphinscheduler找到dolphinScheduler的所有进程,并通过kill -15 进程ID结束这些进程后,再到scheduler部署目录执行启动脚本bash ./bin/dolphinscheduler-daemon.sh start standalone-server

5. 附录

本文由 白鲸开源 提供发布支持!

海豚²来了丨DolphinDB 集成 DolphinScheduler,任务调度更轻松的更多相关文章

  1. SpringCloud微服务实战——搭建企业级开发框架(四十二):集成分布式任务调度平台XXL-JOB,实现定时任务功能

      定时任务几乎是每个业务系统必不可少的功能,计算到期时间.过期时间等,定时触发某项任务操作.在使用单体应用时,基本使用Spring提供的注解即可实现定时任务,而在使用微服务集群时,这种方式就要考虑添 ...

  2. SpringBoot 1.5.x 集成 Quartz 任务调度框架

    Quartz 有分 内存方式 和 数据库方式 内存方式任务信息保存在内存中, 停机会丢失, 需手动重新执行, 数据库方式: 任务信息保存在数据库中, 重点是支持集群. 内存方式 RAMJobStore ...

  3. Solon集成(02)- 轻松吃下小馒头 Dubbo

    Solon详解系列文章: Solon详解(一)- 快速入门 Solon详解(二)- Solon的核心 Solon详解(三)- Solon的web开发 Solon详解(四)- Solon的事务传播机制 ...

  4. SpringBoot集成Swagger2,3分钟轻松入手!

    一.引入maven <dependency> <groupId>io.springfox</groupId> <artifactId>springfox ...

  5. DTSE Tech Talk丨第3期:解密数据隔离方案,让SaaS应用开发更轻松

    摘要:解读云上前沿技术,畅聊开发应用实践.专家团队授课,答疑解惑,助力开发者使用华为云开放能力进行应用构建.技术创新. 围绕当下许多企业青睐的SaaS应用开发,华为云DTSE技术布道师李良龙为大家带来 ...

  6. Science Word丨快速创建各种数学、物理、化学图形,让老师的工作更轻松!

    很多老师,尤其是理科老师,在出试卷或做课件时需要绘制图像,很多老师会直接在PPT上绘制,但要画得专业点很是费时费力.ScienceWord提供了大量的数学.物理和化学常用的图形,能快速创建各种数学图形 ...

  7. 9.4用WebApi去连接外部认证服务

    原文链接:http://www.asp.net/web-api/overview/security/external-authentication-services VS2013和Asp.Net4.5 ...

  8. Apache DolphinScheduler(海豚调度) - 1.3 系列核心表结构剖析

    Apache DolphinScheduler 是一个分布式去中心化,易扩展的可视化 DAG 工作流任务调度系统.致力于解决数据处理流程中错综复杂的依赖关系,使调度系统在数据处理流程中开箱即用. 近日 ...

  9. 在生鲜零售业,DolphinScheduler 还能这么玩!

    点击上方 蓝字关注我们 ✎ 编 者 按 2021 年,Apache DolphinScheduler 社区又迎来了新的蓬勃发展,社区活跃度持续提高.目前,项目 GitHub Star 已达 6.7k, ...

  10. 我与Apache DolphinScheduler的成长之路

    关于 Apache DolphinScheduler社区 Apache DolphinScheduler(incubator) 于17年在易观数科立项,19年3月开源, 19 年8月进入Apache ...

随机推荐

  1. idea导入maven项目发现有jar或插件无法下载检查idea中的maven配置,maven配置文件中需配置阿里云的镜像地址

    D:\apache-maven-3.2.3\conf\settings.xml <mirrors> <mirror> <id>nexus-public-snapsh ...

  2. 记录vue和js操作——尽管很快实现了功能,可总感觉到不爽

    需求产生的原因是:后端有一些数据是从旧平台直接迁移过来的,新平台需要根据迁移过来的数据,自动生产新的数据格式. 操作符有如下几种,分项.支路和数字配合操作符可以自定义组合,例如 [0000000000 ...

  3. docker 单机部署redis集群

    docker 部署redis集群 1.创建redis网卡 docker network create redis --subnet 172.38.0.0/16 查看网卡信息 docker networ ...

  4. 02-CentOS7基础

    基础知识介绍 shell shell俗称壳,它包裹在内核的外面,是用户命令的翻译官. 作用:接收用户的命令,翻译后(处理一下)交给Linux内核处理. 命令 -> shell -> 内核 ...

  5. Shell脚本实现Linux回收站

    前言 到目前为止,非图形化的Linux还没有回收站的命令. 那么,我们如果不小心将某一个文件删掉了,那就只能数据恢复了.如果这也不行,那就等着哭吧. 最新代码在我的github:https://git ...

  6. TrustZone与高通

    改编自:https://blog.csdn.net/guyongqiangx/article/details/78020257 介绍 导读 Q:什么是Trust Zone A:Trust Zone:是 ...

  7. HGAME2023 week1-week2

    确实是高质量比赛,学到了很多知识.认识到了很多的不足. 任重而道远啊... hgame_week1 web Classic Childhood Game F12检查源码,打开Events.js 发现 ...

  8. 【java深入学习第1章】深入探究 MyBatis-Spring 中 SqlSession 的原理与应用

    前言 在使用 MyBatis 进行持久层开发时,通常会与 Spring 框架集成,以便更好地管理事务和依赖注入.在 MyBatis-Spring 集成中,SqlSession 是一个非常重要的概念.本 ...

  9. UE5 射线检测排除隐藏的Actor

    0x00 Unreal Engine 5(UE5)以其卓越的性能和直观的开发工具在游戏开发领域占据了重要地位.本系列将深入探讨UE5中射线检测的关键概念,着重介绍处理隐藏Actor的技巧. 0x01. ...

  10. redis雪崩

    每个key(即数据)如果设置了失效时间的话,如果大量key同时过期的时候,或者说因为某种原因redis中的数据突然大批量丢失,这些key又大量地去请求这些key时,因为redis里面没有这些数据,就会 ...