-
干货丨时序数据库DolphinDB内存管理详解
2021-01-13 09:24:29DolphinDB是一款支持多用户多任务并发操作的高性能分布式时序数据库软件(distributed time-series database)。针对大数据的高效的内存管理是其性能优异的原因之一。本教程涉及的内存管理包括以下方面: 变量的内存...DolphinDB是一款支持多用户多任务并发操作的高性能分布式时序数据库软件(distributed time-series database)。针对大数据的高效的内存管理是其性能优异的原因之一。本教程涉及的内存管理包括以下方面:
- 变量的内存管理:为用户提供与回收编程环境所需内存。
- 分布式表的缓存管理:多个session共享分区表数据,以提高内存使用率。
- 流数据缓存:流数据发送节点提供持久化和发送队列缓存,订阅节点提供接收数据队列缓存。
- DFS数据库写入缓存:写入DFS的数据先写到WAL和缓存,通过批量写入提升吞吐量。
1. 内存管理机制DolphinDB向操作系统申请内存块,自行进行管理。当申请的内存块闲置时,系统会定期检查并释放。目前vector和table以及所有字符串的内存分配都已经纳入DolphinDB的内存管理系统。
通过参数maxMemSize设定节点的最大内存使用量:该参数制定节点的最大可使用内存。如果设置太小,会严重限制集群的性能,如果设置太大,例如超过物理内存,可能会触发操作系统强制关闭进程。若机器内存为16GB,并且只部署1个节点,建议将该参数设置为12GB左右。
以512MB为单位向操作系统申请内存块:当用户查询操作或者编程换进所需要内存时,DolphinDB会以512MB为单位向操作系统申请内存。如果操作系统无法提供大块的连续内存,则会尝试256MB,128MB等更小的内存块。
系统充分利用可用内存缓存数据库数据:当节点的内存使用总量小于maxMemSize时,DolphinDB会尽可能多的缓存数据库分区数据,以便提升用户下次访问该数据块的速度。当内存不足时,系统自动会剔除部分缓存。
每隔30秒扫描一次,空闲的内存块还给操作系统:当用户使用释放内存中变量,或者使用函数clearAllCache释放缓存时,如果内存块完全空闲,则会整体还给操作系统,如果仍有小部分内存在使用,比如512MB的内存块中仍有10MB在使用,则不会归还操作系统。
2. 变量的内存管理2.1 创建变量
在DolphinDB节点上,先创建一个用户user1,然后登陆。创建一个vector,含有1亿个INT类型元素,约400MB。
示例1. 创建vector变量
login("admin","123456") //创建用户需要登陆admin createUser("user1","123456") login("user1","123456") v = 1..100000000 sum(mem().blockSize - mem().freeSize) //输出内存占用结果
结果为: 402,865,056,内存占用400MB左右,符合预期。
再创建一个table,1000万行,5列,每列4字节,约200MB。
示例2. 创建table变量
n = 10000000 t = table(n:n,["tag1","tag2","tag3","tag4","tag5"],[INT,INT,INT,INT,INT]) (mem().blockSize - mem().freeSize).sum()
结果为:612,530,448,约600MB,符合预期。
2.2 释放变量可通过undef函数,释放变量的内存。
示例3. 使用undef函数或者赋值为NULL释放变量
undef(`v)
或者
v = NULL
除了手动释放变量,当session关闭时,比如关闭GUI和其他API连接,都会触发对该session的所有内存进行回收。当通过web notebook连接时,10分钟内无操作,系统会关闭session,自动回收内存。
3. 分布式表的缓存管理DolphinDB对分布式表是以分区为单位管理的。分布式表的缓存是全局共享的,不同的session或读事务在大部分情况下,会看到同一份数据copy(版本可能会有所不同),这样极大的节省了内存的使用。
历史数据库都是以分布式表的形式存在数据库中,用户平时查询操作也往往直接与分布式表交互。分布式表的内存管理有如下特点:
- 内存以分区列为单位进行管理。
- 数据只加载到所在的节点,不会在节点间转移。
- 多个用户访问相同分区时,使用同一份缓存。
- 内存使用不超过maxMemSize情况下,尽量多缓存数据。
- 缓存数据达到maxMemSize时,系统自动回收。
以下多个示例是基于以下集群:部署于2个节点,采用单副本模式。按天分30个区,每个分区1000万行,11列(1列DATE类型,1列INT类型,9列LONG类型),所以每个分区的每列(LONG类型)数据量为1000万行 * 8字节/列 = 80M,每个分区共1000万行 * 80字节/行 = 800M,整个表共3亿行,大小为24GB。
函数clearAllCache()可清空已经缓存的数据,下面的每次测试前,先用该函数清空节点上的所有缓存。
3.1 内存以分区列为单位进行管理DolphinDB采用列式存储,当用户对分布式表的数据进行查询时,加载数据的原则是,只把用户所要求的分区和列加载到内存中。
示例4. 计算分区2019.01.01最大的tag1的值。该分区储存在node1上,可以在controller上通过函数getClusterChunksStatus()查看分区分布情况,而且由上面可知,每列约80MB。在node1上执行如下代码,并查看内存占用。
select max(tag1) from loadTable(dbName,tableName) where day = 2019.01.01 sum(mem().blockSize - mem().freeSize)
输出结果为84,267,136。我们只查询1个分区的一列数据,所以把该列数据全部加载到内存,其他的列不加载。
示例5. 在node1 上查询 2019.01.01的前100条数据,并观察内存占用。
select top 100 * from loadTable(dbName,tableName) where day = 2019.01.01 sum(mem().blockSize - mem().freeSize)
输出结果为839,255,392。虽然我们只取100条数据,但是DolphinDB加载数据的最小单位是分区列,所以需要加载每个列的全部数据,也就是整个分区的全部数据,约800MB。
注意: 合理分区以避免"out of memory":DolphinDB是以分区为单位管理内存,因此内存的使用量跟分区关系密切。假如用户分区不均匀,导致某个分区数据量超大,甚至机器的全部内存都不足以容纳整个分区,那么当涉及到该分区的查询计算时,系统会抛出"out of memory"的异常。一般原则,如果用户设置maxMemSize=8,则每个分区常用的查询列之和为100-200MB为宜。如果表有10列常用查询字段,每列8字段,则每个分区约100-200万行。
3.2 数据只加载到所在的节点在数据量大的情况下,节点间转移数据是非常耗时的操作。DolphinDB的数据是分布式存储的,当执行计算任务时,把任务发送到数据所在的节点,而不是把数据转移到计算所在的节点,这样大大降低数据在节点间的转移,提升计算效率。
示例6. 在node1上计算两个分区中tag1的最大值。其中分区2019.01.02数组存储在node1上,分区2019.01.03数据存储在node2上。
select max(tag1) from loadTable(dbName,tableName) where day in [2019.01.02,2019.01.03] sum(mem().blockSize - mem().freeSize)
输出结果为84,284,096。在node2上用查看内存占用结果为84,250,624。每个节点存储的数据都为80M左右,也就是node1上存储了分区2019.01.02的数据,node2上存储了2019.01.03的数据。
示例7. 在node1上查询分区2019.01.02和2019.01.03的所有数据,我们预期node1加载2019.01.02数据,node2加载2019.01.03的数据,都是800M左右,执行如下代码并观察内存。
select top 100 * from loadTable(dbName,tableName) where day in [2019.01.02,2019.01.03] sum(mem().blockSize - mem().freeSize)
node1上输出结果为839,279,968。node2上输出结果为839,246,496。结果符合预期。
注意: 谨慎使用没有过滤条件的"select *",因为这会将所有数据载入内存。在列数很多的时候尤其要注意该点,建议仅加载需要的列。若使用没有过滤条件的"select top 10 *",会将第一个分区的所有数据载入内存。
3.3 多个用户访问相同分区时,使用同一份缓存DolphinDB支持海量数据的并发查询。为了高效利用内存,对相同分区的数据,内存中只保留同一份副本。
示例8. 打开两个GUI,分别连接node1和node2,查询分区2019.01.01的数据,该分区的数据存储在node1上。
select * from loadTable(dbName,tableName) where date = 2019.01.01 sum(mem().blockSize - mem().freeSize)
上面的代码不管执行几次,node1上内存显示一直是839,101,024,而node2上无内存占用。因为分区数据只存储在node1上,所以node1会加载所有数据,而node2不占用任何内存。
3.4 节点内存占用情况与缓存数据的关系3.4.1 节点内存使用不超过maxMemSize情况下,尽量多缓存数据
通常情况下,最近访问的数据往往更容易再次被访问,因此DolphinDB在内存允许的情况下(内存占用不超过用户设置的maxMemSize),尽量多缓存数据,来提升后续数据的访问效率。
示例9. 数据节点设置的maxMemSize=8。连续加载9个分区,每个分区约800M,总内存占用约7.2GB,观察内存的变化趋势。
days = chunksOfEightDays(); for(d in days){ select * from loadTable(dbName,tableName) where = day sum(mem().blockSize - mem().freeSize) }
内存随着加载分区数的增加变化规律如下图所示:
当遍历每个分区数据时,在内存使用量不超过maxMemSize的情况下,分区数据会全部缓存到内存中,以在用户下次访问时,直接从内存中提供数据,而不需要再次从磁盘加载。
3.4.2 节点内存使用达到maxMemSize时,系统自动回收
如果DolphinDB server使用的内存,没有超过用户设置的maxMemSize,则不会回收内存。当总的内存使用达到maxMemSize时,DolphinDB 会采用LRU的内存回收策略, 来腾出足够的内存给用户。示例10. 上面用例只加载了8天的数据,此时我们继续共遍历15天数据,查看缓存达到maxMemSize时,内存的占用情况。如下图所示:
如上图所示,当缓存的数据超过maxMemSize时,系统自动回收内存,总的内存使用量仍然小于用户设置的最大内存量8GB。
示例11. 当缓存数据接近用户设置的maxMemSize时,继续申请Session变量的内存空间,查看系统内存占用。此时先查看系统的内存使用:
sum(mem().blockSize - mem().freeSize)
输出结果为7,550,138,448。内存占用超过7GB,而用户设置的最大内存使用量为8GB,此时我们继续申请4GB空间。
v = 1..1000000000 sum(mem().blockSize - mem().freeSize)
输出结果为8,196,073,856。约为8GB,也就是如果用户定义变量,也会触发缓存数据的内存回收,以保证有足够的内存提供给用户使用。
4. 流数据消息缓存队列当数据进入流数据系统时,首先写入流表,然后写入持久化队列和发送队列(假设用户设置为异步持久化),持久化队列异步写入磁盘,将发送队列发送到订阅端。
当订阅端收到数据后,先放入接受队列,然后用户定义的handler从接收队列中取数据并处理。如果handler处理缓慢,会导致接收队列有数据堆积,占用内存。如下图所示:
流数据内存相关的配置选项:
- maxPersistenceQueueDepth: 流表持久化队列的最大消息数。对于异步持久化的发布流表,先将数据放到持久化队列中,再异步持久化到磁盘上。该选项默认设置为1000万。在磁盘写入成为瓶颈时,队列会堆积数据。
- maxPubQueueDepthPerSite: 最大消息发布队列深度。针对某个订阅节点,发布节点建立一个消息发布队列,该队列中的消息发送到订阅端。默认值为1000万,当网络出现拥塞时,该发送队列会堆积数据。
- maxSubQueueDepth: 订阅节点上最大的每个订阅线程最大的可接收消息的队列深度。订阅的消息,会先放入订阅消息队列。默认设置为1000万,当handler处理速度较慢,不能及时处理订阅到的消息时,该队列会有数据堆积。
- 流表的capacity:在函数enableTablePersistence()中第四个参数指定,该值表示流表中保存在内存中的最大行数,达到该值时,从内存中删除一半数据。当流数据节点中,流表比较多时,要整体合理设置该值,防止内存不足。
运行过程,可以通过函数getStreamingStat()来查看流表的大小以及各个队列的深度。
5. 为写入DFS数据库提供缓存DolphinDB为了提高读写的吞吐量和降低读写的延迟,采用先写入WAL和缓存的通用做法,等累积到一定数量时,批量写入。这样减少和磁盘文件的交互次数,提升写入性能,可提升写入速度30%以上。因此,也需要一定的内存空间来临时缓存这些数据,如下图所示:
当事务t1,t2,t3都完成时,将三个事务的数据一次性写入到DFS的数据库磁盘上。Cache Engine空间一般推荐为maxMemSize的1/8~1/4,可根据最大内存和写入数据量适当调整。CacheEngine的大小可以通过配置参数chunkCacheEngineMemSize来配置。
- chunkCacheEngineMemSize:指定cache engine的容量。cache engine开启后,写入数据时,系统会先把数据写入缓存,当缓存中的数据量达到chunkCacheEngineMemSize的30%时,才会写入磁盘。
6. 高效使用内存在企业的生产环境中,DolphinDB往往作为流数据中心以及历史数据仓库,为业务人员提供数据查询和计算。当用户较多时,不当的使用容易造成Server端内存耗尽,抛出"out of memory" 异常。可遵循以下建议,尽量避免内存的不合理使用。
- 合理均匀分区:DolphinDB是以分区为单位加载数据,因此,分区大小对内存影响巨大。合理均匀的分区,不管对内存使用还是对性能而言,都有积极的作用。因此,在创建数据库的时候,根据数据规模,合理规划分区大小。每个分区的常用字段数据量约100MB左右为宜。
- 及时释放数据量较大的变量:若用户创建数据量较大的变量,例如v = 1..10000000,或者将含有大量数据的查询结果赋值给一个变量t = select * from t where date = 2010.01.01,v和t将会在用户的session占用大量的内存。如果不及时释放,当其他用户申请内存时,就有可能因为内存不足而抛出异常。
- 只查询需要的列:避免使用select *,如果用select *会把该分区所有列加载到内存。实际中,往往只需要几列。因此为避免内存浪费,尽量明确写出所有查询的列,而不是用*代替。
- 数据查询尽可能使用分区过滤条件:DolphinDB按照分区进行数据检索,如果不加分区过滤条件,则会全部扫描所有数据,数据量大时,内存很快被耗尽。有多个过滤条件的话,要优先写分区的过滤条件。
- 尽快释放不再需要的变量或者session:根据以上分析可知,用户的私有变量在创建的session里面保存。session关闭的时候,会回收这些内存。因此,尽早使用undef函数或者关闭session来释放内存。
- 合理配置流数据的缓存区:一般情况下流数据的容量(capacity)会直接影响发布节点的内存占用。比如,capacity设置1000万条,那么流数据表在超过1000万条时,会回收约一半的内存占用,也就是内存中会保留500万条左右。因此,应根据发布节点的最大内存,合理设计流表的capacity。尤其是在多张发布表的情况,更需要谨慎设计。
7. 内存监控及常见问题
7.1 内存监控
7.1.1 controller上监控集群中节点内存占用在controller上提供函数getClusterPerf()函数,显示集群中各个节点的内存占用情况。包括:
MemAlloc:节点上分配的总内存,近似于向操作系统申请的内存总和。
MemUsed:节点已经使用的内存。该内存包括变量、分布式表缓存以及各种缓存队列等。
MemLimit:节点可使用的最大内存限制,即用户配置的maxMemSize。
7.1.2 mem()函数监控某个节点内存占用
mem()函数可以显示整个节点的内存分配和占用情况。该函数输出4列,其中列blockSize表示分配的内存块大小,freeSize表示剩余的内存块大小,通过sum(mem().blockSize - mem().freeSize) 得到节点所使用的总的内存大小。
7.1.3 监控节点上不同session的内存占用可通过函数getSessionMemoryStat()查看节点上每个session占用的内存量,该内存只包含session内定义的变量。当节点上内存占用太高时,可以通过该函数排查哪个用户使用了大量的内存。
7.1.4 查看某个对象占用的内存大小通过函数memSize来查看某个对象占用内存的具体大小,单位为字节。比如:
v=1..1000000 memSize(v)
输出:4000000。
7.2 常见问题
7.2.1 监控显示节点内存占用太高通过上面的分析可知,DolphinDB会在内存允许的情况下,会尽可能多的缓存数据。因此,如果只显示节点内存占用太高,接近maxMemSize,而没有其他内存相关的错误,那么这种情况是正常的。 如果出现"out of memory"等类似的错误,首先可以通过函数getSessionMemoryStat()查看各个session占用的内存大小,其次通过函数clearAllCache()来手动释放节点的缓存数据。
7.2.2 MemAlloc显示值跟操作系统实际显示值有差异
DolphinDB是C++程序,本身需要一些基础的数据结构和内存开销,MemAlloc显示内存不包括这些内存,如果两者显示相差不大,几百MB以内,都属于正常现象。
7.2.3 查询时,报告"out of memory"该异常往往是由于query所需的内存大于系统可提供的内存导致的。可能由以下原因导致:
- 查询没有加分区过滤条件或者条件太宽,导致单个query涉及的数据量太大。
- 分区不均匀。可能某个分区过大,该分区的数据超过节点配置的最大内存。
- 某个session持有大的变量,导致节点可用的内存很小。
7.2.4 查询时,DolphinDB进程退出,没有coredump产生
这种情况往往是由于给节点分配的内存超过系统物理内存的限制,操作系统把DolphinDB强制退出。Linux上可以通过操作系统的日志查看原因。
7.2.5 执行clearAllCache()函数后,MemUsed没有明显降低可以通过getSessionMemoryStst()查看各个session占用的内存大小。可能是由于某个session持有占用大量内存的变量不释放,导致该部分内存一直不能回收。
-
干货丨时序数据库DolphinDB内存表详解
2021-01-20 09:50:49内存表是DolphinDB数据库的重要组成部分。内存表不仅可以直接用于存储数据,实现高速数据读写,而且可以缓存计算引擎的中间结果,加速计算过程。本教程主要介绍DolphinDB内存表的分类、使用场景以及各种内存表在数据...内存表是DolphinDB数据库的重要组成部分。内存表不仅可以直接用于存储数据,实现高速数据读写,而且可以缓存计算引擎的中间结果,加速计算过程。本教程主要介绍DolphinDB内存表的分类、使用场景以及各种内存表在数据操作以及表结构(schema)操作上的异同。
1. 内存表类别根据不同的使用场景以及功能特点,DolphinDB内存表可以分为以下四种:
- 常规内存表
- 键值内存表
- 流数据表
- MVCC内存表
1.1 常规内存表常规内存表是DolphinDB中最基础的表结构,支持增删改查等操作。SQL查询返回的结果通常存储在常规内存表中,等待进一步处理。
- 创建
使用table函数可创建常规内存表。table函数有两种用法:第一种用法是根据指定的schema(字段类型和字段名称)以及表容量(capacity)和初始行数(size)来生成;第二种用法是通过已有数据(矩阵,表,数组和元组)来生成一个表。
使用第一种方法的好处是可以预先为表分配内存。当表中的记录数超过容量时,系统会自动扩充表的容量。扩充时系统首先会分配更大的内存空间(增加20%到100%不等),然后复制旧表到新的表,最后释放原来的内存。对于规模较大的表,扩容的成本会比较高。因此,如果我们可以事先预计表的行数,建议创建内存表时预先分配一个合理的容量。如果表的初始行数为0,系统会生成空表。如果初始行数不为0,系统会生成一个指定行数的表,表中各列的值都为默认值。例如:
//创建一个空的常规内存表 t=table(100:0,`sym`id`val,[SYMBOL,INT,INT]) //创建一个10行的常规内存表 t=table(100:10,`sym`id`val,[SYMBOL,INT,INT]) select * from t sym id val --- -- --- 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
table函数也允许通过已有的数据来创建一个常规内存表。下例是通过多个数组来创建。
sym=`A`B`C`D`E id=5 4 3 2 1 val=52 64 25 48 71 t=table(sym,id,val)
- 应用
常规内存表是DolphinDB中应用最频繁的数据结构之一,仅次于数组。SQL语句的查询结果,分布式查询的中间结果都存储在常规内存表中。当系统内存不足时,该表并不会自动将数据溢出到磁盘,而是Out Of Memory异常。因此我们进行各种查询和计算时,要注意中间结果和最终结果的size。当某些中间结果不再需要时,请及时释放。关于常规内存表增删改查的各种用法,可以参考另一份教程内存分区表加载和操作。
1.2 键值内存表键值内存表是DolphinDB中支持主键的内存表。通过指定表中的一个或多个字段作为主键,可以唯一确定表中的记录。键值内存表支持增删改查等操作,但是主键值不允许更新。键值内存表通过哈希表来记录每一个键值对应的行号,因此对于基于键值的查找和更新具有非常高的效率。
- 创建
使用keyedTable函数可创建键值内存表。该函数与table函数非常类似,唯一不同之处是增加了一个参数指明键值列的名称。
//创建空的键值内存表,主键由sym和id字段组成 t=keyedTable(`sym`id,1:0,`sym`id`val,[SYMBOL,INT,INT]) //使用向量创建键值内存表,主键由sym和id字段组成 sym=`A`B`C`D`E id=5 4 3 2 1 val=52 64 25 48 71 t=keyedTable(`sym`id,sym,id,val)
注意:指定容量和初始大小创建键值内存表时,初始大小必须为0。
我们也可以通过keyedTable函数将常规内存表转换为键值内存表。例如:
sym=`A`B`C`D`E id=5 4 3 2 1 val=52 64 25 48 71 tmp=table(sym, id, val) t=keyedTable(`sym`id, tmp)
- 数据插入和更新的特点
往键值内存表中添加新纪录时,系统会自动检查新记录的主键值。如果新记录中的主键值不存在于表中,那么往表中添加新的记录;如果新记录的主键值与已有记录的主键值重复时,会更新表中该主键值对应的记录。请看下面的例子。
首先,往空的键值内存表中插入新记录,新记录中的主键值为AAPL, IBM和GOOG。
t=keyedTable(`sym,1:0,`sym`datetime`price`qty,[SYMBOL,DATETIME,DOUBLE,DOUBLE]); insert into t values(`APPL`IBM`GOOG,2018.06.08T12:30:00 2018.06.08T12:30:00 2018.06.08T12:30:00,50.3 45.6 58.0,5200 4800 7800); t; sym datetime price qty ---- ------------------- ----- ---- APPL 2018.06.08T12:30:00 50.3 5200 IBM 2018.06.08T12:30:00 45.6 4800 GOOG 2018.06.08T12:30:00 58 7800
再次往表中插入一批主键值为AAPL, IBM和GOOG的新记录。
insert into t values(`APPL`IBM`GOOG,2018.06.08T12:30:01 2018.06.08T12:30:01 2018.06.08T12:30:01,65.8 45.2 78.6,5800 8700 4600); t; sym datetime price qty ---- ------------------- ----- ---- APPL 2018.06.08T12:30:01 65.8 5800 IBM 2018.06.08T12:30:01 45.2 8700 GOOG 2018.06.08T12:30:01 78.6 4600
可以看到,表中记录条数没有增加,但是主键对应的记录已经更新。
继续往表中插入一批新记录,新记录本身包含了重复的主键值MSFT。
可以看到,表中有且仅有一条主键值为MSFT的记录。
- 应用场景
(1)键值表对单行的更新和查询有非常高的效率,是数据缓存的理想选择。与redis相比,DolphinDB中的键值内存表兼容SQL的所有操作,可以完成根据键值更新和查询以外的更为复杂的计算。
(2)作为时间序列聚合引擎的输出表,实时更新输出表的结果。具体请参考教程使用DolphinDB计算K线。
1.3 流数据表流数据表顾名思义是为流数据设计的内存表,是流数据发布和订阅的媒介。流数据表具有天然的流表对偶性(Stream Table Duality),发布一条消息等价于往流数据表中插入一条记录,订阅消息等价于将流数据表中新到达的数据推向客户端应用。对流数据的查询和计算都可以通过SQL语句来完成。
- 创建
使用streamTable函数可创建流数据表。streamTable的用法和table函数完全相同。
//创建空的流数据表 t=streamTable(1:0,`sym`id`val,[SYMBOL,INT,INT]) //使用向量创建流数据表 sym=`A`B`C`D`E id=5 4 3 2 1 val=52 64 25 48 71 t=streamTable(sym,id,val)
我们也可以使用streamTable函数将常规内存表转换为流数据表。例如:
sym=`A`B`C`D`E id=5 4 3 2 1 val=52 64 25 48 71 tmp=table(sym, id, val) t=streamTable(tmp)
流数据表也支持创建单个键值列,可以通过函数keyedStreamTable来创建。但与keyed table的设计目的不同,keyedstreamtable的目的是为了在高可用场景(多个发布端同时写入)下,避免重复消息。通常key就是消息的ID。
- 数据操作特点
由于流数据具有一旦生成就不会发生变化的特点,因此流数据表不支持更新和删除记录,只支持查询和添加记录。流数据通常具有连续性,而内存是有限的。为解决这个矛盾,流数据表引入了持久化机制,在内存中保留最新的一部分数据,更旧的数据持久化在磁盘上。当用户订阅旧的数据时,直接从磁盘上读取。启用持久化,使用函数enableTableShareAndPersistence,具体参考流数据教程。
- 应用场景
共享的流数据表在流计算中发布数据。订阅端通过subscribeTable函数来订阅和消费流数据。
1.4 MVCC内存表
MVCC内存表存储了多个版本的数据,当多个用户同时对MVCC内存表进行读写操作时,互不阻塞。MVCC内存表的数据隔离采用了快照隔离模型,用户读取到的是在他读之前就已经存在的数据,即使这些数据在读取的过程中被修改或删除了,也对之前正在读的用户没有影响。这种多版本的方式能够支持用户对内存表的并发访问。需要说明的是,当前的MVCC内存表实现比较简单,更新和删除数据时锁定整个表,并使用copy-on-write技术复制一份数据,因此对数据删除和更新操作的效率不高。在后续的版本中,我们将实现行级的MVCC内存表。
- 创建
使用mvccTable函数创建MVCC内存表。例如:
//创建空的流数据表 t=mvccTable(1:0,`sym`id`val,[SYMBOL,INT,INT]) //使用向量创建流数据表 sym=`A`B`C`D`E id=5 4 3 2 1 val=52 64 25 48 71 t=mvccTable(sym,id,val)
我们可以将MVCC内存表的数据持久化到磁盘,只需创建时指定持久化的目录和表名即可。例如,
t=mvccTable(1:0,`sym`id`val,[SYMBOL,INT,INT],"/home/user1/DolphinDB/mvcc","test")
系统重启后,我们可以通过loadMvccTable函数将磁盘中的数据加载到内存中。
loadMvccTable("/home/user1/DolphinDB/mvcc","test")
我们也可以使用mvccTable函数将常规内存表转换为MVCC内存表。
sym=`A`B`C`D`E id=5 4 3 2 1 val=52 64 25 48 71 tmp=table(sym, id, val) t=mvccTable(tmp)
- 应用场景
当前的MVCC内存表适用于读多写少,并有持久化需要的场景。譬如动态的配置系统,需要持久化配置项,配置项的改动不频繁,已新增和查询操作为主,非常适合MVCC表。
2. 共享内存表
DolphinDB中的内存表默认只在创建内存表的会话中使用,不支持多用户多会话的并发操作,当然对别的会话也不可见。如果希望创建的内存表能被别的用户使用,保证多用户并发操作的安全,必须共享内存表。4种类型的内存表均可共享。在DolphinDB中,我们使用share命令将内存表共享。
t=table(1..10 as id,rand(100,10) as val) share t as st //或者share(t,`st)
上面的代码将表t共享为表st。
使用undef函数可以删除共享表。
undef(`st,SHARED)
2.1 保证对所有会话可见
内存表仅在当前会话可见,在其他会话中不可见。共享之后,其他会话可以通过访问共享变量来访问内存表。例如,我们在当前会话中把表t共享为表st。
t=table(1..10 as id,rand(100,10) as val) share t as st
我们可以在其他会话中访问变量st。例如,往共享表st插入一条数据。
insert into st values(11,200) select * from st id val -- --- 1 1 2 53 3 13 4 40 5 61 6 92 7 36 8 33 9 46 10 26 11 200
切换到原来的会话,我们可以发现,表t中也增加了一条记录。
select * from t id val -- --- 1 1 2 53 3 13 4 40 5 61 6 92 7 36 8 33 9 46 10 26 11 200
2.2 保证线程安全
在多线程的情况下,内存表中的数据很容易被破坏。共享则提供了一种保护机制,能够保证数据安全,但同时也会影响系统的性能。
常规内存表、流数据表和MVCC内存表都支持多版本模型,允许多读一写。具体说,读写互不阻塞,写的时候可以读,读的时候可以写。读数据时不上锁,允许多个线程同时读取数据,读数据时采用快照隔离(snapshot isolation)。写数据时必须加锁,同时只允许一个线程修改内存表。写操作包括添加,删除或更新。添加记录一律在内存表的末尾追加,无论内存使用还是CPU使用均非常高效。常规内存表和MVCC内存表支持更新和删除,且采用了copy-on-write技术,也就是先复制一份数据(构成一个新的版本),然后在新版本上进行删除和修改。由此可见删除和更新操作无论内存和CPU消耗都比较高。当删除和更新操作很频繁,读操作又比较耗时(不能快速释放旧的版本),容易导致OOM异常。
键值内存表写入时需维护内部索引,读取时也需要根据索引获取数据。因此键值内存表共享采用了不同的方法,无论读写都必须加锁。写线程和读线程,多个写线程之间,多个读线程之间都是互斥的。对键值内存表尽量避免耗时的查询或计算,否则会使其它线程长时间处于等待状态。
3. 分区内存表
当内存表数据量较大时,我们可以对内存表进行分区。分区后一个大表有多个子表(tablet)构成,大表不使用全局锁,锁由每个子表独立管理,这样可以大大增加读写并发能力。DolphinDB支持对内存表进行值分区、范围分区、哈希分区和列表分区,不支持组合分区。在DolphinDB中,我们使用函数createPartitionedTable创建内存分区表。
- 创建分区常规内存表
t=table(1:0,`id`val,[INT,INT]) db=database("",RANGE,0 101 201 301) pt=db.createPartitionedTable(t,`pt,`id)
- 创建分区键值内存表
kt=keyedTable(1:0,`id`val,[INT,INT]) db=database("",RANGE,0 101 201 301) pkt=db.createPartitionedTable(t,`pkt,`id)
- 创建分区流数据表
创建分区流数据表时,需要传入多个流数据表作为模板,每个流数据表对应一个分区。写入数据时,直接往这些流表中写入;而查询数据时,需要查询分区表。
st1=streamTable(1:0,`id`val,[INT,INT]) st2=streamTable(1:0,`id`val,[INT,INT]) st3=streamTable(1:0,`id`val,[INT,INT]) db=database("",RANGE,1 101 201 301) pst=db.createPartitionedTable([st1,st2,st3],`pst,`id) st1.append!(table(1..100 as id,rand(100,100) as val)) st2.append!(table(101..200 as id,rand(100,100) as val)) st3.append!(table(201..300 as id,rand(100,100) as val)) select * from pst
- 创建分区MVCC内存表
与创建分区流数据表一样,创建分区MVCC内存表,需要传入多个MVCC内存表作为模板。每个表对应一个分区。写入数据时,直接往这些表中写入;而查询数据时,需要查询分区表。
mt1=mvccTable(1:0,`id`val,[INT,INT]) mt2=mvccTable(1:0,`id`val,[INT,INT]) mt3=mvccTable(1:0,`id`val,[INT,INT]) db=database("",RANGE,1 101 201 301) pmt=db.createPartitionedTable([mt1,mt2,mt3],`pst,`id) mt1.append!(table(1..100 as id,rand(100,100) as val)) mt2.append!(table(101..200 as id,rand(100,100) as val)) mt3.append!(table(201..300 as id,rand(100,100) as val)) select * from pmt
由于分区内存表不使用全局锁,创建以后不能再动态增删子表。
3.1 增加查询的并发性分区表增加查询的并发性有三层含义:(1)键值表在查询时也需要加锁,分区表由子表独立管理锁,相当于把锁的粒度变细了,因此可以增加读的并发性;(2)批量计算时分区表可以并行处理每个子表;(3)如果SQL查询的过滤指定了分区字段,那么可以缩小分区范围,避免全表扫描。
以键值内存表为例,我们对比在分区和不分区的情况下,并发查询的性能。首先,创建模拟数据集,一共包含500万行数据。
n=5000000 id=shuffle(1..n) qty=rand(1000,n) price=rand(1000.0,n) kt=keyedTable(`id,id,qty,price) share kt as skt id_range=cutPoints(1..n,20) db=database("",RANGE,id_range) pkt=db.createPartitionedTable(kt,`pkt,`id).append!(kt) share pkt as spkt
我们在另外一台服务器上模拟10个客户端同时查询键值内存表。每个客户端查询10万次,每次查询一条数据,统计每个客户端查询10万次的总耗时。
def queryKeyedTable(tableName,id){ for(i in id){ select * from objByName(tableName) where id=i } } conn=xdb("192.168.1.135",18102,"admin","123456") n=5000000 jobid1=array(STRING,0) for(i in 1..10){ rid=rand(1..n,100000) s=conn(submitJob,"evalQueryUnPartitionTimer"+string(i),"",evalTimer,queryKeyedTable{`skt,rid}) jobid1.append!(s) } time1=array(DOUBLE,0) for(j in jobid1){ time1.append!(conn(getJobReturn,j,true)) } jobid2=array(STRING,0) for(i in 1..10){ rid=rand(1..n,100000) s=conn(submitJob,"evalQueryPartitionTimer"+string(i),"",evalTimer,queryKeyedTable{`spkt,rid}) jobid2.append!(s) } time2=array(DOUBLE,0) for(j in jobid2){ time2.append!(conn(getJobReturn,j,true)) }
time1是10个客户端查询未分区键值内存表的耗时,time2是10个客户端查询分区键值内存表的耗时,单位是毫秒。
time1 [6719.266848,7160.349678,7271.465094,7346.452625,7371.821485,7363.87979,7357.024299,7332.747157,7298.920972,7255.876976] time2 [2382.154581,2456.586709,2560.380315,2577.602019,2599.724927,2611.944367,2590.131679,2587.706832,2564.305815,2498.027042]
可以看到,每个客户端查询分区键值内存表的耗时要低于查询未分区内存表的耗时。
查询未分区的内存表,可以保证快照隔离。但查询一个分区内存表,不再保证快照隔离。如前面所说分区内存表的读写不使用全局锁,一个线程在查询时,可能另一个线程正在写入而且涉及多个子表,从而可能读到一部分写入的数据。
3.2 增加写入的并发性以分区的常规内存表为例,我们可以同时往不同的分区写入数据。
t=table(1:0,`id`val,[INT,INT]) db=database("",RANGE,1 101 201 301) pt=db.createPartitionedTable(t,`pt,`id) def writeData(mutable t,id,batchSize,n){ for(i in 1..n){ idv=take(id,batchSize) valv=rand(100,batchSize) tmp=table(idv,valv) t.append!(tmp) } } job1=submitJob("write1","",writeData,pt,1..100,1000,1000) job2=submitJob("write2","",writeData,pt,101..200,1000,1000) job3=submitJob("write3","",writeData,pt,201..300,1000,1000)
上面的代码中,同时有3个线程对pt的3个不同的分区进行写入。需要注意的是,我们要避免同时对相同分区进行写入。例如,下面的代码可能会导致系统崩溃。
job1=submitJob("write1","",writeData,pt,1..300,1000,1000) job2=submitJob("write2","",writeData,pt,1..300,1000,1000)
上面的代码定义了两个写入线程,并且写入的分区相同,这样会破坏内存。为了保证每个分区数据的安全性和一致性,我们可将分区内存表共享。这样即可定义多个线程同时对相同分区分入。
share pt as spt job1=submitJob("write1","",writeData,spt,1..300,1000,1000) job2=submitJob("write2","",writeData,spt,1..300,1000,1000)
4. 数据操作比较
4.1 增删改查下表总结了4种类型内存表在共享/分区的情况下支持的增删改查操作。
说明:
- 常规内存表、键值内存表、MVCC内存表都支持增删改查操作,流数据表仅支持增加数据和查询,不支持删除和更新操作。
- 对于键值内存表,如果查询的过滤条件中包含主键,查询的性能会得到明显提升。
- 对于分区内存表,如果查询的过滤条件中包含分区列,系统能够缩小要扫描的分区范围,从而提升查询的性能。
4.2 并发性在没有写入的情况下,所有内存表都允许多个线程同时查询。在有写入的情况下,4种内存表的并发性有所差异。下表总结了4种内存表在共享/分区的情况下支持的并发读写情况。
说明:
- 共享表允许并发读写。
- 对于没有共享的分区表,不允许多线程对相同分区同时写入的。
4.3 持久化- 常规内存表和键值内存表不支持数据持久化。一旦节点重启,内存中的数据将全部丢失。
- 只有空的流数据表才支持数据持久化。要对流数据表进行持久化,首先要配置流数据持久化的目录persistenceDir,再使用enableTableShareAndPersistence使用将流数据表共享,并持久化到磁盘上。例如,将流数据表t共享并持久化到磁盘上。
t=streamTable(1:0,`id`val,[INT,INT]) enableTableShareAndPersistence(t,`st)
流数据表启用了持久化后,内存中仍然会保留流数据表中部分最新的记录。默认情况下,内存会保留最新的10万条记录。我们也可以根据需要调整这个值。
流数据表持久化可以设定采用异步/同步、压缩/不压缩的方式。通常情况下,异步模式能够实现更高的吞吐量。
系统重启后,再次执行enableTableShareAndPersistence函数,会将磁盘中的所有数据加载到内存。
- MVCC内存表支持持久化。在创建MVCC内存表时,我们可以指定持久化的路径。例如,创建持久化的MVCC内存表。
t=mvccTable(1:0,`id`val,[INT,INT],"/home/user/DolphinDB/mvccTable") t.append!(table(1..10 as id,rand(100,10) as val))
系统重启后,我们可以使用loadMvccTable函数将磁盘中的数据加载到内存中。例如:
t=loadMvccTable("/home/user/DolphinDB/mvccTable","t")
5. 表结构操作比较
内存表的结构操作包括新增列、删除列、修改列(内容和数据类型)以及调整列的顺序。下表总结了4种类型内存表在共享/分区的情况下支持的结构操作。
说明:
- 分区表以及MVCC内存表不能通过addColumn函数新增列。
- 分区表可以通过update语句来新增列,但是流数据表不允许修改,因此流数据表不能通过update语句来新增列。
6. 小结DolphinDB支持4种类型内存表,还引入了共享和分区的概念,基本能够满足内存计算和流计算的各种需求。
-
干货丨时序数据库DolphinDB 内存分区表的加载和操作
2021-01-26 09:13:27DolphinDB的内存数据表可以是非分区的,也可以是分区的。除了组合分区以外的所有分区方式都适用于内存数据表。使用分区内存表进行运算能充分发挥多核CPU并行计算的优势。 1. 创建内存数据表 1.1 创建非分区内存表 ...DolphinDB的内存数据表可以是非分区的,也可以是分区的。除了组合分区以外的所有分区方式都适用于内存数据表。使用分区内存表进行运算能充分发挥多核CPU并行计算的优势。
1. 创建内存数据表
1.1 创建非分区内存表
使用table函数可以创建非分区内存表。table函数的用法非常灵活:
- 第一种用法:table(X, [X1], [X2], .....)
这里的X, X1, X2可以是向量、矩阵或元组,其中每个向量、矩阵和元组中每个元素的长度必须相同。
例1. 如果X是向量,那么每个向量对应表中一列。
id=`XOM`GS`AAPL x=102.1 33.4 73.6 table(id, x) id x ---- ----- XOM 102.1 GS 33.4 AAPL 73.6 table(`XOM`GS`AAPL as id, 102.1 33.4 73.6 as x) id x ---- ----- XOM 102.1 GS 33.4 AAPL 73.6
例2. 如果X是矩阵,table函数将矩阵转换为表。
m1=1..6$3:2 table(m1) C0 C1 -- -- 1 4 2 5 3 6 m2=7..12$3:2 table(m1,m2) C0 C1 C2 C3 -- -- -- -- 1 4 7 10 2 5 8 11 3 6 9 12
例3. 如果X是元组,那么元组中的每个元素对应表中的一列。
x=(["a","b","c"],[4,5,6]) table(x) C0 C1 -- -- a 4 b 5 c 6
例4. table函数的输入是向量、矩阵和元组。
x=1..6 y=11..22$6:2 z=(101..106, 201..206) x C1 C2 C3 C4 - -- -- --- --- 1 11 17 101 201 2 12 18 102 202 3 13 19 103 203 4 14 20 104 204 5 15 21 105 205 6 16 22 106 206
- 第二种用法:table(capacity:size, colNames, colTypes)
我们可以通过指定表的容量和初始大小、列名以及每列的数据类型来创建内存表。如果表中实际的记录行数超出的capacity,它会自动扩展。如果要创建一个空的表,可以把size设置为0,如果size>0,创建表时会使用默认值填充。例如,
table(200:0, `name`id`value, [STRING,INT,DOUBLE]) name id value ---- -- ----- table(200:10, `name`id`value, [STRING,INT,DOUBLE]) name id value ---- -- ----- 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1.2 创建分区内存表通过createPartitionedTable函数可以创建分区内存表。在创建分区内存表之前,需要创建内存的分区数据库,并且分区数据库的路径为空字符串。
n=10000 t=table(rand(1..10,n) as id,rand(10.0,n) as val) db=database("",VALUE,1..10) pt=db.createPartitionedTable(t,`pt,`id).append!(t) typestr(pt) SEGMENTED IN-MEMORY TABLE
对于内存分区表,不能直接访问,需要使用SQL语句访问。
select * from pt
2. 加载数据到内存表
我们通过以下脚本,生成一个模拟数据集,用于后续例子。
n=30000000 workDir = "C:/DolphinDB/Data" if(!exists(workDir)) mkdir(workDir) trades=table(rand(`IBM`MSFT`GM`C`FB`GOOG`V`F`XOM`AMZN`TSLA`PG`S,n) as sym, 2000.01.01+rand(365,n) as date, 10.0+rand(2.0,n) as price1, 100.0+rand(20.0,n) as price2, 1000.0+rand(200.0,n) as price3, 10000.0+rand(2000.0,n) as price4, 10000.0+rand(3000.0,n) as price5, 10000.0+rand(4000.0,n) as price6, rand(10,n) as qty1, rand(100,n) as qty2, rand(1000,n) as qty3, rand(10000,n) as qty4, rand(10000,n) as qty5, rand(10000,n) as qty6) trades.saveText(workDir + "/trades.txt");
2.1 加载数据到未分区内存表使用loadText函数将文本文件数据导入到未分区的内存表。
trades=loadText(workDir + "/trades.txt")
2.2 加载数据到内存分区表
2.2.1 使用ploadText函数将文本文件导入为顺序分区的内存表这是将数据导入到内存分区表最简单的方法,但缺乏其它导入方法的某些优点及灵活性。例如,需要导入的文本文件必须小于可用内存;无法使用函数
sortBy!
进行分区内有意义的排序,等等。trades = ploadText(workDir + "/trades.txt");
2.2.2 使用loadTextEx函数将文本文件导入为指定分区方式的表
这种方法适合下列情况:
- 经常需要在各个分区内部进行排序
- 经常需要根据分区字段进行group by与context by的计算
使用这种方法时,database函数的directory参数以及loadTextEx函数的tableName参数需使用空字符串("")。
db = database("", VALUE, `IBM`MSFT`GM`C`FB`GOOG`V`F`XOM`AMZN`TSLA`PG`S) trades = db.loadTextEx("", `sym, workDir + "/trades.txt"); trades.sortBy!(`qty1); trades.sortBy!(`date`qty1, false true); trades.sortBy!(<qty1 * price1>, false);
请注意,对内存分区表使用函数 sortBy! 时,是在每个分区内部进行排序,并不是对全表进行排序。
我们分别对未分区内存表、顺序分区内存表以及本节中的VALUE分区内存表进行相同的分组聚合运算。SQL语句如下:
timer(10) select std(qty1) from trades group by sym;
这里的 "timer(10)" 指的是此语句被连续执行10次的总耗时。
结果如下表所致。可以看到,当分组列和分区列相同时,分组计算性能最优。
2.2.3 使用loadTable函数导入磁盘分区表的全部或部分分区这种方法适合下列情况:
- 文本文件比服务器可用内存更大,并且每次只需要用到其中的一部分数据。
- 需要重复使用数据。加载一个数据库表比导入一个文本文件要快得多。
使用loadTextEx在磁盘上建立一个分区表:(亦可使用createPartitionedTable和append!函数)
db = database(workDir+"/tradeDB", RANGE, ["A","G","M","S","ZZZZ"]) db.loadTextEx(`trades, `sym, workDir + "/trades.txt");
若只需要加载两个分区(["A","G")与["M","S"))到内存时:
db = database(workDir+"/tradeDB") trades=loadTable(db, `trades, ["A", "M"], 1);
请注意,这里需要将函数loadTable的可选参数memoryMode设为1,否则将只会加载表的元数据。
2.2.4 使用loadTableBySQL函数导入磁盘分区表指定的行/列
这是最灵活的产生内存分区表的方法,可以使用SQL语句选择磁盘分区表中指定的行/列以载入内存分区表。需与函数loadTable结合使用。
db = database(workDir+"/tradeDB") trades=loadTable(db, `trades); sample=loadTableBySQL(<select * from trades where date between 2000.03.01 : 2000.05.01>); sample=loadTableBySQL(<select sym, date, price1, qty1 from trades where date between 2000.03.01 : 2000.05.01>); dates = 2000.01.16 2000.02.14 2000.08.01 st = sql(<select sym, date, price1, qty1>, trades, expr(<date>, in, dates)) sample = loadTableBySQL(st); colNames =`sym`date`qty2`price2 st= sql(sqlCol(colNames), trades) sample = loadTableBySQL(st);
3. 内存表的数据处理以2.2.3中的分区内存表trades为例,介绍内存表的用法。
trades = ploadText(workDir + "/trades.txt");
3.1 插入数据
可以通过以下方法往内存表中插入数据:
- SQL insert 语句
//往指定列插入数据,其他列为空 insert into trades(sym,date) values(`S,2000.12.31) //往所有列插入数据 insert into trades values(`S`IBM,[2000.12.31,2000.12.30],[10.0,20.0],[10.0,20.0],[10.0,20.0],[10.0,20.0],[10.0,20.0],[10.0,20.0],[10,20],[10,20],[10,20],[10,20],[10,20],[10,20])
2. append!函数
如果使用 append! 函数往表中插入数据,新数据必须是以表的形式表示。例如:
tmp=table(`S`IBM as col1,[2000.12.31,2000.12.30] as col2,[10.0,20.0] as col3,[10.0,20.0] as col4,[10.0,20.0] as col5,[10.0,20.0] as col6,[10.0,20.0] as col7,[10.0,20.0] as col8,[10,20] as col9,[10,20] as col10,[10,20] as col11,[10,20] as col12,[10,20] as col13,[10,20] as col14) trades.append!(tmp)
3. tableInsert 函数
tableInsert 函数会返回插入的行数。
对于分区表,如果使用 tableInsert 函数往表中插入数据,新数据必须是以表的形式表示。
tmp=table(`S`IBM as col1,[2000.12.31,2000.12.30] as col2,[10.0,20.0] as col3,[10.0,20.0] as col4,[10.0,20.0] as col5,[10.0,20.0] as col6,[10.0,20.0] as col7,[10.0,20.0] as col8,[10,20] as col9,[10,20] as col10,[10,20] as col11,[10,20] as col12,[10,20] as col13,[10,20] as col14) trades.tableInsert(tmp) 2
对于未分区表,如果使用tableInsert 函数往表中插入数据,新数据可以用元组的形式表示。
a=(`S`IBM,[2000.12.31,2000.12.30],[10.0,20.0],[10.0,20.0],[10.0,20.0],[10.0,20.0],[10.0,20.0],[10.0,20.0],[10,20],[10,20],[10,20],[10,20],[10,20],[10,20]) trades.tableInsert(a)
3.2 增加列可以通过以下三种方法为内存表增加列:
- SQL update 语句
update trades set logPrice1=log(price1), newQty1=double(qty1);
2. update! 函数
trades.update!(`logPrice1`newQty1, <[log(price1), double(qty1)]>);
3. 赋值语句
trades[`logPrice1`newQty1] = <[log(price1), double(qty1)]>;
3.3 更新已存在的列
可以通过以下三种方法为内存表更新列:
- SQL update 语句
update trades set qty1=qty1+10; update trades set qty1=qty1+10 where sym=`IBM;
2. update! 函数
trades.update!(`qty1, <qty1+10>); trades.update!(`qty1, <qty1+10>, <sym=`IBM>);
3. 赋值语句
trades[`qty1] = <qty1+10>; trades[`qty1, <sym=`IBM>] = <qty1+10>;
3.4 删除行
可以通过以下三种方法为内存表删除行:
- SQL delete 语句
delete from trades where qty3<20;
2. erase 语句
trades.erase!(< qty3<30 >);
3.5 删除列
通过 drop! 函数删除列:
trades.drop!("qty1");
3.6 重命名列通过 rename! 函数重命名列:
trades.rename!("qty2", "qty2New");
3.7 查看表结构
通过schema函数查看表的结构:
schema(trades) partitionSchema->[XOM,V,TSLA,S,PG,MSFT,IBM,GOOG,GM,FB,...] partitionSites-> partitionColumnIndex->0 chunkPath-> colDefs-> name typeString typeInt comment ------ ---------- ------- ------- sym SYMBOL 17 date DATE 6 price1 DOUBLE 16 price2 DOUBLE 16 price3 DOUBLE 16 price4 DOUBLE 16 price5 DOUBLE 16 price6 DOUBLE 16 qty1 INT 4 qty2 INT 4 qty3 INT 4 qty4 INT 4 qty5 INT 4 qty6 INT 4 partitionType->1 partitionColumnName->sym
3.8 删除内存表可以通过以下两种方法删除内存表:
- undef函数
undef(`trades)
2. 把变量赋值为NULL
trades=NULL
undef 函数会将命名空间删除,而把变量赋值为NULL仍然保留命名空间。
3.9 修改列的数据类型通过replaceColumn! 函数可以修改列的数据类型。目前只有未分区的内存表才支持修改列的数据类型,分区内存表不支持该功能。以未分区的内存表trades为例,将price1的数据类型修改为FLOAT(目前是DOUBLE):
NewPrice1=float(exec price1 from trades) replaceColumn!(trades,`price1,NewPrice1) schema(trades) partitionColumnIndex->-1 chunkPath-> colDefs-> name typeString typeInt comment ------ ---------- ------- ------- sym SYMBOL 17 date DATE 6 price1 `FLOAT` 15 price2 DOUBLE 16 price3 DOUBLE 16 price4 DOUBLE 16 price5 DOUBLE 16 price6 DOUBLE 16 qty1 INT 4 qty2 INT 4 qty3 INT 4 qty4 INT 4 qty5 INT 4 qty6 INT 4
3.10 修改列的顺序通过reorderColumns! 函数可以修改列的顺序。目前只有未分区的内存表才支持修改列的数据类型,分区内存表不支持该功能。以未分区的内存表trades为例,将列的顺序调整为sym,date,price6,price5,...,price1,qty6,qty5,...,qty1。
reorderColumns!(trades,`sym`date`price6`price5`price4`price3`price2`price1`qty6`qty5`qty4`qty3`qty2`qty1)
-
kairosdb时序数据库
2021-03-31 10:07:451.什么是时序数据库: 时序数据库随时间存储值,可以一分钟打一个点,且有前端页面可以直接查看存储数据的趋势图,kairosdb查询数据和插入数据都要发送post请求,与关系型数据库相比,kairosdb会将多个列名折叠成一...1.什么是时序数据库:
时序数据库随时间存储值,可以一分钟打一个点,且有前端页面可以直接查看存储数据的趋势图,kairosdb查询数据和插入数据都要发送post请求,与关系型数据库相比,kairosdb会将多个列名折叠成一行来表示一个时间段。通过多个tag来定位一条数据,时序数据库能及时存储和检索记录,通常,各种数据库通过索引减少磁盘I/O来实现快速检索,索引只有在足够小以适合内存时才有效,因为如果索引不足,则必须将其分页到需要I/O的磁盘上,由于索引的大小与表中的行数成正比,所以时序数据库比关系型数据库性能高的多,因为行数少
2.kairosdb通过rest接口插入数据;
提交数据的网址是http:// localhost:8080 / api / v1 / datapoints [{ “ name” : “ archive.file.tracked” , “ timestamp” : 1349109376 , “ type” : “ long” , “ value” : 123 , “ tags” :{ “ host” :“ test” } }, { “ name” : “ archive.file.search” , “ timestamp” : 999 , “ type” : “ double” , “ value” : 32.1 , “ tags” :{ “ host” :“ test” } }]
3.查询数据:
用于获取数据的URL是http:// localhost:8080 / api / v1 / datapoints / query 绝对日期取出数据: { “ start_absolute” :1 , “ metrics” : [ { “ name” : “ archive.file.tracked” } ] } 取得相对日期的数据: { “ start_relative” :{ “ value” :20 ,“ unit” :“ weeks” }, “ metrics” : [ { “ name” : “ archive.file.tracked” } ] }
4.导出数据:导出的格式是以json对象的形式每行一个度量。这可能很冗长,但是可以让您在导出后对数据做一些有趣的事情。
要从KairosDB导出数据,请运行以下命令: > bin / kairosdb.sh导出-f export.txt
-
InfluxDB时序数据库
2020-10-16 16:37:13InfluxDB(时序数据库)是一个由InfluxData开发的开源时序型数据。由Go写成,着力于高性能地查询与存储时序型数据。存储和分析时间序列数据的开源数据库。 常用的使用场景:监控数据统计。每毫秒记录电脑内存的使用... -
时序数据库InfluxDB详解
2019-11-22 11:28:18InfluxDB(时序数据库),常用的一种使用场景:监控数据统计。每毫秒记录一下电脑内存的使用情况,然后就可以根据统计的数据,利用图形化界面(InfluxDB V1一般配合Grafana)制作内存使用情况的折线图;可以理解为按... -
Monarch: 谷歌的全球级内存时序数据库
2020-11-08 10:00:00点击上方“朱小厮的博客”,选择“设为星标”后台回复"书",获取本文为Google监控团队发表在VLDB的论文,翻译为蚂蚁监控团队计算组。摘要Monarch 是谷歌的一个全... -
初识influxDB——时序数据库
2020-07-03 16:57:06InfluxDB(时序数据库),常用的一种使用场景:监控数据统计。每毫秒记录一下电脑内存的使用情况,然后就可以根据统计的数据,利用图形化界面(InfluxDB V1一般配合Grafana)制作内存使用情况的折线图; 可以理解为... -
Prometheus时序数据库-磁盘中的存储结构
2021-03-05 00:25:39Prometheus时序数据库-磁盘中的存储结构前言之前的文章里,笔者详细描述了监控数据在Prometheus内存中的结构。而其在磁盘中的存储结构,也是非常有意思的,关于这部分内容,将在本... -
Facebook开源的时序数据库存储引擎Beringei.zip
2019-07-19 06:25:17Beringei 是一个高性能的、内存型的时序数据库存储引擎。时间序列通常用作统计,仪表和计数器的表示,用于监视系统的性能和运行状况。Beringei 有以下特性:支持非常快的内存存储,由磁盘支持持久化非常高效的流压缩... -
深度解读Facebook刚开源的beringei时序数据库
2017-02-09 11:41:51beringei是用来解决其内部监控数据存储和查询需求的数据库,其特点是读写速度快,属于内存数据库。 beringei是如何做到的呢,其压缩算法上有哪些独到之处? 阿里云数据库高级专家叶翔借着源代码和论文,对beringei... -
Aliware打造史上最强时序数据库_HiTSDB每秒写入时序数据达1000万!
2017-08-09 14:29:42摘要: 近日,Aliware对外正式发布HiTSDB高性能时序数据库。HiTSDB引入了高效压缩算法,能够将每个数据点的平均内存开销压缩到2字节...HiTSDB 是一种高性能、低成本、稳定可靠的在线时序数据库服务;提供高效读写 -
深度解读Facebook刚开源的beringei时序数据库——数据压缩delta of delta+充分利用内存以提高性能...
2019-09-27 11:35:16摘要:Facebook最近开源了beringei时序数据库,其是用来解决其内部监控数据存储和查询需求的数据库,特点是读写速度快。beringei在压缩算法上有哪些独到之处?本文中阿里云数据库高级专家叶翔将为大家深度解读。 ... -
监控预警丨时序数据库DolphinDB异常检测引擎教程
2020-12-09 17:42:30DolphinDB作为一个高性能的分布式时序数据库 (time series database),内置了一个流数据框架,既能实时处理分析这些物联网数据,也能对历史数据进行计算分析,帮助用户利用、发挥这些数据的价值。DolphinDB内置的流... -
时序数据库技术体系 – Druid 多维查询之Bitmap索引
2020-01-17 23:18:11文章目录时序数据库技术体系 – Druid 多维查询之Bitmap索引背景Bitmap索引到底是什么Bitmap索引如何在内存中构建Bitmap索引构建时机维度列构建维度字典构建Bitmap索引Bitmap索引如何进行压缩处理Bitmap索引为什么... -
Aliware打造史上最强时序数据库,HiTSDB每秒写入时序数据达1000万!
2017-08-08 15:14:19HiTSDB 是一种高性能、低成本、稳定可靠的在线时序数据库服务;提供高效读写,高压缩比存储、时序数据插值及聚合计算。是物联网(IoT)设备监控系统 ,企业能源管理系统(EMS),生产安全监控系统,电力检测系统等... -
postgresql批量插入数据脚本_时序数据库DolphinDB和TimescaleDB 性能对比测试报告
2020-12-21 01:25:57一、概述DolphinDBDolphinDB 是以 C++ 编写的一款分析型的高性能分布式时序数据库,使用高吞吐低延迟的列式内存引擎,集成了功能强大的编程语言和高容量高速度的流数据分析系统,可在数据库中进行复杂的编程和运算,... -
揭秘高性能分布式时序数据库—DolphinDB Database
2020-11-27 09:30:36DolphinDB是由浙江智臾科技有限公司自主研发,于2018年初发布的高性能的磁盘与内存混合型和列式分布式数据库产品。DolphinDB集成了功能强大的编程语言和高容量高速度的流数据分析系统,为海量数据(特别是时间序列... -
开源时序型数据库 InfluxDB
2020-12-17 22:11:06InfluxDB是开源时序型数据库,由Go写成,被广泛应用于监控系统,如cpu利用率,io,内存等指标;穿戴设备,如心率,体温;IoT实时数据等场景。InfluxDB 官方网站:... -
零距离接触阿里云时序时空数据库TSDB
2018-12-19 17:34:06阿里云TSDB是阿里巴巴集团数据库事业部研发的一款高性能分布式时序时空数据库,在即将过去的2018年,我们对TSDB进行了多次的系统架构改进,引入了倒排索引、无限时间线支持、时序数据高压缩比算法、内存缓存、数据预...