1。概述 DolphinDB从1。30。6版本开始支持更新分布式表数据。更新操作支持事务,具备事务ACID的特性,且通过MVCC实现快照隔离级别。DolphinDB为多模数据库,目前支持两种存储引擎:OLAP和TSDB(详见DolphinDB数据模型),不同引擎的更新操作的原理和性能不同。 本文将分别介绍两种存储引擎的更新操作相关的原理,并通过实验验证和分析相关性能。2。使用方法 DolphinDB提供3种更新分布式表的方法:标准SQL的update语法(注意,DolphinDB仅支持关键字小写);sqlUpdate方法,动态生成SQLupdate语句的元代码。生成元代码后,通过eval函数执行。具体用法见《DolphinDB教程:元编程》;upsert!方法,若新数据的主键值已存在,更新该主键值的数据;否则添加数据。 下面通过一个简单的例子介绍更新分布式表的3种方法:通过updateSQL和sqlUpdate方法更新所有ID1的行的vol列数据;通过upsert!更新第一个ID1所在行的date和vol列数据。 (1)使用附件脚本建库表; (2)更新数据: 方法1,下列updateSQL语句执行后,ID为1的所有记录的vol值更新为1:updateptsetvol1whereID1复制代码 方法2,下列sqlUpdate语句执行后,ID为1的所有记录的vol值更新为2:sqlUpdate(pt,2asvol,whereID1)。eval()复制代码 方法3,下列upsert!语句执行后,第一个ID1所在记录的date更新为2022。08。07,vol更新为3:newDatatable(1asID,2022。08。07asdate,3asvol)upsert!(pt,newData,keyColNamesID)复制代码3。存储机制简介 在了解更新原理之前,需要先简单了解DolphinDB的存储机制。 DolphinDB内置分布式文件系统DFS,由其统一管理各个节点的存储空间,在对数据集进行分割时,可以全局优化,规避数据分布不均匀等问题。通过DFS,我们可以将同一集群中不同数据节点上的存储文件夹组织起来,形成一个单独的、逻辑的、层次式的共享文件系统。更多分区数据库介绍可以参考《DolphinDB教程:分区数据库》。 DolphinDB提供配置项volume,可以为每个节点指定数据文件的存储路径,用户可以通过pnodeRun(getAllStorages)查询路径。3。1OLAP引擎存储结构 在OLAP中,每个分区内部都会为表的每列数据分别存储一个列文件。例如第2节脚本创建的使用OLAP引擎的表pt,其分区〔0,50)的数据文件保存在CHUNKSdb1050gApt2路径下,其中:db1为库名;050为范围分区〔0,50);gA为表的物理名索引,于1。30。162。00。4版本为支持表级分区时引入;pt2为pt表的版本文件夹,其中2指表在库内的序号,于1。30。1版本支持renameTable特性时引入。 用户可以进入库名目录,通过tree命令查看表的数据文件目录结构,如下所示:tree。050gApt2date。colID。colvol。col50100gApt2date。colID。colvol。coldolphindb。lockdomainpt。tbl6directories,9files复制代码3。2TSDB引擎存储结构 TSDB引擎以LSM树(LogStructuredMergeTree)的结构存储表数据。例如第2节脚本创建的使用TSDB引擎的表pt1,分区〔0,50)的数据文件保存在CHUNKSTSDBdb1050gEpt12路径下,路径组成同OLAP引擎。 在库名目录使用tree命令查看内容,可以看到pt1表的levelfile,也就是LSM树的分层数据文件,如下所示,其中开头下划线前的数字表示层级,目前支持4层:tree。050gEchunk。dictpt1200000062350100gEchunk。dictpt12000000624dolphindb。lockdomain复制代码 如果levelfile未及时生成,用户可以执行flushTSDBCache(),将TSDB引擎缓冲区里已经完成的事务强制写入磁盘。4。数据更新原理 下面分别介绍OLAP和TSDB存储引擎更新分布式表数据的基本原理。4。1OLAP存储引擎更新原理 更新流程 gitee。comdolphindbT 分布式表数据更新过程是一个事务,采用两阶段提交协议和MVCC机制,OLAP引擎数据更新的具体流程如下: (1)向控制节点申请创建事务,控制节点会给涉及的分区加锁,并分配事务tid,返回分区chunkID等信息; (2)判断是否开启OLAPcacheengine:如果开启了cacheengine,先刷写所有已完成的事务的缓存数据到磁盘;数据写入redolog;在数据节点上创建事务和对应临时目录,并更新数据到该目录下。 通过事务tid生成一个临时的路径,格式为physicalDirtidtidValue,并把数据更新到该目录下。如果某些列的数据无变化,则直接创建原列文件的硬链接;对于linux这种支持硬链接但不支持跨volume,或类似beegfs这种不支持硬链接的文件系统,则采用复制的方式来解决。 如果没有开启cacheengine,只做第2。3步; (3)向控制节点申请事务提交的cid。 更新事务commit时,生成一个单调递增的cid,即CommitID作为提交动作的唯一标识,每个表的元数据有一个createCids数组记录该cid; (4)通知所有事务参与者提交事务:写commitredolog并持久化到磁盘;重命名临时目录为physicalDircid; (5)通知所有事务参与者完成事务,写completeredolog。 不管参与者节点是否成功完成事务,协调者都标记完成事务。如果有失败的情况,通过事务决议和recovery机制来恢复。 事务结束后,最终只会保留最后一个版本,因为程序启动后,其他版本都是历史数据,应该回收掉,不应该继续保留。 定期回收机制 在实际操作中,用户常常会进行多次数据更新,导致上述目录序列不断增加。为此,系统提供了定期垃圾回收的机制。默认情况下,系统每隔60秒回收一定时间值之前更新生成的目录。在开始周期性垃圾回收前,系统最多保留5个更新版本。 快照隔离级别 更新数据的整个过程是一个事务,满足ACID特性,且通过多版本并发控制机制(MVCC,MultiVersionConcurrencyControl)进行读写分离,保证了数据在更新时不影响读取。例如,不同用户同时对相同数据表进行更新和查询操作时,系统将根据操作发生的顺序为其分配cid,并将它们和createCids数组里的版本号对比,寻找所有版本号中值最大且小于cid的文件路径,此路径即为该cid对应的操作路径。如需要在线获取最新的cid,可执行getTabletsMeta()。createCids命令。4。2TSDB存储引擎更新原理 建表参数 DolphinDBTSDB引擎在建表时另外提供了两个参数:sortColumns和keepDuplicates。sortColumns:用于指定表的排序列。系统默认sortColumns最后一列为时间列,其余列字段作为排序的索引列,其组合值称作sortKey。自2。00。8版本起,DolphinDBTSDB引擎开始支持更新sortColumes列。keepDuplicates:用于去重,即指定在每个分区内如何处理所有sortColumns的值相同的数据。参数默认值为ALL,表示保留所有数据;取值为LAST表示仅保留最新数据;取值为FIRST表示仅保留第一条数据。 更新策略 不同的keepDuplicates参数值,会有不同的数据更新策略。keepDuplicatesLAST 这种情况下数据更新实质上是向LSM树的内存缓冲区追加数据。这部分数据最终会持久化到level0层级的levelfile里,故不会产生新的版本目录。通过在LSM树中增加cid列来实现MVCC。查询时,TSDB引擎会做过滤,只返回最新的数据。 写入量达到一定值或更新后一段时间内,会自动合并levelfile以节省硬盘空间。若未及时触发levelfile的合并,可以手动执行triggerTSDBCompaction()强制触发指定chunk内level0级别的所有levelfile的合并操作。 更新流程图如下: (1)向控制节点申请创建事务,控制节点会给涉及的分区加锁,并分配事务tid,返回分区chunkID等信息; (2)TSDB必须开启cacheengine:刷写cacheengine里所有已完成的事务的缓存数据到磁盘;数据写入redolog;读取需要更新的数据到内存并更新,即只读取需要更新的levelfile的部分数据; (3)向控制节点申请事务提交的cid。 更新事务commit时,生成一个单调递增的cid,即CommitID作为提交动作的唯一标识,每个表的元数据有一个createCids数组记录该cid; (4)通知所有事务参与者提交事务,写commitredolog并持久化到磁盘; (5)通知所有事务参与者完成事务:写completeredolog;写入更新的数据到cacheengine,cacheengine的后台线程会异步写入数据到磁盘。 2。keepDuplicatesFIRST或keepDuplicatesALL 在这两种情况下,更新操作流程与OLAP基本相同,也是采用两阶段提交协议,每个分区每次更新产生一个独立的目录,且整个更新过程是一个事务,具备ACID特性,通过MVCC实现快照隔离级别。过期版本的清理机制也与OLAP相同。 但是第2。3步读取要更新的数据到内存并更新到临时目录,TSDBkeepDuplicatesFIRST或ALL策略与OLAP不同,会读出所有文件进行更新,即未变化的列也会被更新,然后将更新的levelfile信息写入到元数据中。因此,我们也建议用户首先确定分区数据量较小(不能大于可用内存空间)再进行更新操作,否则容易导致内存溢出。5。数据更新实验 接下来我们通过实验来验证数据更新的基本原理,并对比分析OLAP和TSDB引擎在常见场景的性能特点。5。1实验准备 部署集群Linux系统DolphinDBv2。00。7版本单机集群部署1个控制节点1个数据节点单SSD硬盘,硬盘速度为6Gbps,文件系统为xfs开启cacheengine和redolog 创建库表和写入数据 参考《DolphinDB入门:物联网范例》第2。3、2。4节,创建多值模型数据库表,并写入数据。注意此处脚本为异步写入,可以通过getRecentJobs()方法查看写入是否完成。 本实验将模拟100台机器5天的数据量,每台机器每天产生86400条记录,5天总数据量为43200000条。OLAP和TSDB引擎的基本数据和建库名如下表所示: 参考以下脚本修改《DolphinDB入门:物联网范例》2。3节的createDatabase函数。注意实验中需要修改createPartitionedTable函数的keepDuplicates参数值:defcreateDatabase(dbName,tableName,ps1,ps2,numMetrics){mtagstring(1。。numMetrics)schematable(1:0,iddatetimejoinm,〔INT,DATETIME〕jointake(FLOAT,numMetrics))db1database(,VALUE,ps1)db2database(,RANGE,ps2)dbdatabase(dbName,COMPO,〔db1,db2〕,,TSDB)db。createPartitionedTable(schema,tableName,datetimeid,,iddatetime,keepDuplicatesLAST)}复制代码 实验的基本思路为观察执行update语句前后数据目录和文件的变化与上文阐述的原理是否一致。5。2OLAP引擎更新实验更新前 首先进入到需要更新的数据所在目录,默认位置在storageCHUNKSolapDemo202009011112。其中,最后的2为表的物理名索引。通过tree命令查看machines表的列数据文件:tree。2machines2datetime。colid。coltag10。coltag11。col。。。tag9。col复制代码执行更新 在GUI执行如下脚本,对分区20200901下分区111里的数据进行20次更新:machinesloadTable(dfs:olapDemo,machines)for(iin0。。20)updatemachinessettag1i,tag5iwhereidin1。。5,date(datetime)2020。09。01复制代码 更新过程中使用ll命令可以看到,命名带tid的中间文件夹已生成:lltotal20drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2115drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2116drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2117drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2118drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2119drwxrwxrx2dolphindbdolphindb120Sep705:26machines2tid120复制代码更新后 查看目录文件,如果没触发周期性清理旧版本,可以看到只保留了5个更新版本:lltotal20drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2121drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2122drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2123drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2124drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2125复制代码 在触发周期性清理旧版本前,使用ll命令查看更新后的列数据文件,注意到只有更新的列tag1和tag5的数据文件链接数为1,其他数据文件链接数均为5即硬链接。这是因为只更新了tag1和tag5的列,其他列无变化,因此直接创建了硬链接。如下所示:llmachines2125total169632rwrwr5dolphindbdolphindb3469846Sep705:15datetime。colrwrwr5dolphindbdolphindb14526Sep705:15id。colrwrwr5dolphindbdolphindb3469845Sep705:15tag10。colrwrwr5dolphindbdolphindb3469846Sep705:15tag11。col。。。rwrwr1dolphindbdolphindb1742158Sep705:26tag1。col。。。rwrwr1dolphindbdolphindb1742158Sep705:26tag5。col。。。复制代码 更新后过一段时间,发现只保留了最新的版本。这是因为系统会定时进行回收,最终只保留一个最新的副本。如下所示:lltotal4drwxrwxrx2dolphindbdolphindb4096Sep705:26machines2125复制代码5。3TSDB引擎更新实验 keepDuplicatesLAST更新前 首先设置createPartitionedTable方法的keepDuplicates参数为LAST,然后建库和导入数据。 进入需要更新的数据所在的目录,默认位置在storageCHUNKStsdbDemo20200901111S,最后的S为表的物理名索引。用tree命令查看machines表的levelfile:tree。chunk。dictmachines20000000100000000110000000120000000130000000141000000021directory,7files复制代码执行更新 在GUI执行如下脚本,对分区20200901下分区111里的数据进行20次更新:machinesloadTable(dfs:tsdbDemo,machines)for(iin0。。20)updatemachinessettag1i,tag5iwhereidin1。。5,date(datetime)2020。09。01复制代码 更新时或更新后,用tree命令查看数据文件,可见并没有生成新版本的临时目录,但machines2目录下的levelfile变多,说明keepDuplicatesLAST时,更新等同于追加数据。如下所示:tree。chunk。dictmachines2000000010000000011000000012000000013000000014000000241000000243。。。1000000021000000501000000511directory,21files复制代码更新后 更新后过一段时间,用tree命令查看数据文件,发现levelfile进行了合并。合并levelfile时,系统会自动删除更新时的冗余数据,数量由20个减少为6个。如下所示:tree。chunk。dictmachines20000002720000002740000002761000000021000000501000000511directory,7files复制代码 keepDuplicatesALL更新前 设置createPartitionedTable方法的keepDuplicates参数为ALL,然后建库和导入数据。 通过tree命令查看machines表的levelfile:tree。chunk。dictmachines20000002730000002750000002770000002780000002791000000541directory,7files复制代码执行更新 更新过程中使用tree命令可以看到,有命名包含tid的中间文件夹生成:tree。chunk。dictmachines2000000273000000275000000277000000278000000279100000054machines2tid199000000515000000516000000517000000518000000519复制代码更新后 使用tree命令,可以看到如果没触发周期性清理旧版本,则保留了5个更新版本,如下所示:tree。chunk。dictmachines2215000000595000000596000000597000000598000000599machines2216000000600000000601000000602000000603000000604machines2217000000605000000606000000607000000608000000609machines2218000000610000000611000000612000000613000000614machines2219000000615000000616000000617000000618000000619复制代码 在触发周期性清理旧版本前,使用ll查看更新后的列数据文件,注意到所有levelfile链接数均为1,即不存在硬链接:llmachines2219total284764rwrwr1dolphindbdolphindb57151251Sep705:48000000615rwrwr1dolphindbdolphindb57151818Sep705:48000000616rwrwr1dolphindbdolphindb58317419Sep705:48000000617rwrwr1dolphindbdolphindb59486006Sep705:48000000618rwrwr1dolphindbdolphindb59482644Sep705:48000000619复制代码 更新后过一段时间,发现只保留了最新的版本:tree。chunk。dictmachines22190000006150000006160000006170000006180000006191directory,6files复制代码 keepDuplicatesFIRST createPartitionedTable方法的keepDuplicates参数为FIRST时更新操作的表现与keepDuplicates参数为ALL时相同。6。性能分析 在上文实验的基础上,我们重复执行循环更新脚本,并统计耗时。 更新1条记录的2列脚本如下:machinesloadTable(dfs:olapDemo,machines)timerupdatemachinessettag11,tag55whereid1anddatetime2020。09。01T00:00:00复制代码 更新20次分区20200901下分区111里的数据,总计432000208640000条记录,2列脚本如下:machinesloadTable(dfs:olapDemo,machines)timer{for(iin0。。20)updatemachinessettag1i,tag5iwhereidin1。。5,date(datetime)2020。09。01}复制代码 更新20次分区20200901下分区111里的数据,总计432000208640000条记录,20列脚本如下:machinesloadTable(dfs:olapDemo,machines)timer{for(iin0。。20)updatemachinessettag1i,tag2i,tag3i,tag4i,tag5i,tag6i,tag7i,tag8i,tag9i,tag10i,tag11i,tag12i,tag13i,tag14i,tag15i,tag16i,tag17i,tag18i,tag19i,tag20iwhereidin1。。5,date(datetime)2020。09。01}复制代码 统计更新操作耗时如下表: 注:程序运行性能受硬件性能、系统运行状况等因素影响,该表格运行耗时仅供参考。存储引擎配置为OLAP时 更新性能与需要更新的列数有关,因为更新操作实现为重新生成需要更新的列,而对未更新的列使用了硬链接。故更新20列比更新2列耗时明显长。存储引擎配置为TSDBkeepDuplicatesLAST时 性能相较keepDuplicatesALL或keepDuplicatesFIRST更好,因为更新操作只需要向LSM树的追加数据,而LSM树具有吞吐量大的特点,写性能优异。更新性能比插入性能慢23倍,是因为更新需要先读取数据到内存,而插入不需要。存储引擎配置为TSDBkeepDuplicatesALL或FIRST时 更新性能较差,因为该配置下每次更新都会在一个新的目录产生一个新的版本,且未变化的列也会被更新,磁盘IO较大,故而性能较差。 综上所述,DolphinDB只适合低频更新。使用TSDB引擎时,若对更新的性能有要求,建议配置keepDuplicatesLAST。7。总结 本文对比介绍了OLAP和TSDB两种存储引擎更新分布式表数据的使用方法和基本原理,通过实验来验证基本原理,并分析了常见场景的性能问题。 OLAP存储引擎的更新性能与需要更新的列数和操作系统是否支持硬链接有关。TSDB存储引擎的更新性能与建表参数keepDuplicates有关,建表参数keepDuplicatesLAST比TSDBkeepDuplicatesALL或FIRST时更新性能更好。