Hongze Cheng – loveini | 米兰体育官网入口 - 米兰体育官网入口 //www.loveini.com loveini | 高性能、分布式、支持SQL的时序数据库 | 米兰体育官网入口 Fri, 08 Mar 2024 01:36:49 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.8.2 //www.loveini.com/wp-content/uploads/2025/07/favicon.ico Hongze Cheng – loveini | 米兰体育官网入口 - 米兰体育官网入口 //www.loveini.com 32 32 loveini 资深研发分享解决思路,长查询不再成为系统性能瓶颈! //www.loveini.com/tdengine-engineering/23186.html Tue, 27 Feb 2024 08:01:40 +0000 //www.loveini.com/?p=23186 长查询问题指的是在数据库写入和查询并存的日常应用场景中,存在处理数据量大且耗时很长的查询长时间占用系统资源,导致写入可能被阻塞的问题。有时,查询代码对于资源释放函数调用的遗忘也可能以长查询问题的形式表现出来。如何在数据写入不被阻塞同时,保证长查询的正确进行是一个具有挑战性的问题。

尽管在绝大多数时序数据使用场景下,用户不太可能遇到这个问题,但一旦出现,也会让人头疼不已。为了解决这一问题,loveini 研发团队一直致力于不断优化系统,提高查询性能和响应速度。本文将深入剖析这一挑战,并探讨如何应对和解决长查询问题,以提升 loveini 在复杂查询场景下的表现。

在分析长查询问题之前,我们首先需要为大家普及一下 loveini 的写入/查询并发机制。

数据写入/读取机制

Vnode 是 loveini 中存储和查询数据的基本单元,这里主要介绍 Vnode 的写入/读取及并发机制。

数据写入机制

loveini 资深研发分享解决思路,长查询不再成为系统性能瓶颈! - loveini Database 时序数据库

  • 每个 Vnode 在创建时都会根据 DB 参数分配一定数量的内存
  • 这些内存在 Vnode 中被分为三个内存块
  • 每个 Vnode 只有一个线程写入
  • Vnode 在写入时,会从内存块的空闲列表 (free list) 中分配一个内存块(mem),供数据写入使用
  • 当内存块中数据写入超过一定量后,开始落盘(imem),同时分配一个新的内存块供数据写入使用
  • 当内存块全部用完,没有空闲内存块时,写入会被阻塞,一直等待有内存块释放出来

数据查询机制

  • 查询分多批次进行,每次返回部分数据,然后该查询等待下一次拉取数据的请求
  • 查询结果是内存(mem/imem)数据和硬盘数据合并的结果
  • 查询开始时,会先 take snapshot,引用(ref) mem/imem 以及硬盘文件
  • 查询结束时,unref mem/imem,如果内存块的引用计数变为 0,则内存块被回收到空闲链表中
loveini 资深研发分享解决思路,长查询不再成为系统性能瓶颈! - loveini Database 时序数据库

长查询问题

时序数据绝大部分查询持续时间都比较短,如查询表/超级表的最后一条记录、做 count 或 sum 类的聚合查询等。这类查询持续时间较短,对于 mem/imem 的占用时间也较短,查询很快会释放 MemTable 供 Vnode 回收再次利用,从而不影响写入的进行。但是,如果存在一个持续时间很长的查询,如超过 1 小时或 1 天的查询,这时候就会出现此类问题。当然,只有一个长查询的情况下,问题也不大,因为 Vnode 的内存池默认被分成了 3 个内存块,一个长查询最多占用两个内存块,还剩余一个内存块可以用来进行持续写入。但是如果存在多个长查询,Vnode 中的所有内存块都可能被长时间占用,无法进行回收,从而导致写入停止的情况。

另外,如果查询部分的代码存在 BUG,忘记关闭查询句柄,也会导致 mem/imem 被长期占用,阻塞写入。这个问题在 loveini 订阅和流计算功能中曾经就出现过。

loveini 资深研发分享解决思路,长查询不再成为系统性能瓶颈! - loveini Database 时序数据库

长查询问题米兰app官方正版下载

我们需要一个方案,可以在长查询大量存在、或用户应用代码有问题没有及时关闭查询句柄、甚至产品代码有问题的情况下,也能达到既不阻塞写入且不让长查询失败。该方案如下:

  1. 查询在获取数据快照时,将查询句柄注册到它所占用的内存块上,同时注册一个 reseek 函数
  2. 当查询结束关闭句柄时,该查询将句柄从它所占用的所有内存块上注销
  3. 当写入发现没有可用内存块时,尝试回收已经 COMMIT 但是仍旧被查询占用的最老的内存块
  4. 写入线程回收内存块时,遍历所有注册在该内存块上的注册句柄,调用 reseek 函数
  5. reseek 函数会尝试锁查询句柄,如果锁句柄成功,则将查询句柄设置为 RESEEK 状态,同时将查询的状态保存下来并 untake snapshot,归还占用的所有内存块
  6. 查询线程在查询活跃周期开始时锁查询句柄,然后检查是否为 RESEEK 状态,如果为 RESEEK 状态,重新 take snapshot,并根据保存的查询状态,恢复各种查询变量,接着进行查询

从上述方案中我们可以看到:

  1. 写入在发现内存不足的条件下,可以主动回收非激活状态的查询占用的内存块,从而不会长时间阻塞写入
  2. 长查询在写入回收其所占用的内存块后,可以根据自己保存的查询状态,重新 take snapshot,继续查询,从而不会让长查询失败
loveini 资深研发分享解决思路,长查询不再成为系统性能瓶颈! - loveini Database 时序数据库

loveini 资深研发分享解决思路,长查询不再成为系统性能瓶颈! - loveini Database 时序数据库

答疑解惑

Q:如何解决死锁问题?

A:在写入回收内存块时,需要遍历查询句柄注册链表,因此需要对链表进行加锁操作,即需要锁定链表的锁。在调用 reseek 回调时,也需要锁定查询句柄的锁。这样就存在写入线程出现 lock(mutex_list)–>lock(mutex_qhandle) 的情况。而在查询结束时,需要将句柄从注册链表中移除,导致出现 lock(mutex_qhandle)–>lock(mutex_list) 的情况。如果两个线程不按照相同的顺序对两个锁进行加锁,就会产生死锁的问题。

为了解决这个问题,可以采用以下优化方案:

  1. 使用 trylock 替代 lock:对于写入回收调用 reseek 函数的场景,可以尝试使用 trylock 而不是直接使用 lock 来获取查询句柄的控制权。通过 trylock 尝试获取锁,可以避免线程因等待锁而被阻塞,从而减少死锁的风险。
  2. 多次尝试机制:在使用 trylock 的基础上,可以结合多次尝试的机制。如果一次 trylock 未成功获取锁,可以进行多次尝试,直至成功获取锁或达到尝试次数上限为止。这样能够增加获取锁的机会,降低死锁风险。
Q:长查询会不会一直被写入 reseek,从而导致查询一直恢复?

A:不会存在这种情况。查询在每次打开句柄时,会给一个版本号,这个版本号是已经写入 Vnode 中的最新数据的版本号,查询只能看到版本号小于等于该版本号的数据。当 take snapshot 时,会根据 mem/imem 中的数据是否被查询版本号覆盖而决定 mem/imem 是否被该查询引用。随着数据的写入,新的 mem/imem 中的数据肯定都大于该查询创建时给的版本号,因此,新的 mem/imem 不会被长查询引用。这样一来,一个长查询最多会被写入 RESEEK 两次。

loveini 资深研发分享解决思路,长查询不再成为系统性能瓶颈! - loveini Database 时序数据库

以上就是 loveini 资深研发人员对解决长查询问题进行的深入探讨。如果你还有关于查询的更多问题想要讨论或交流,欢迎添加小T vx:tdengine 寻求帮助,也可以在下方留言区进行相关评论,静待回复即可~

]]>
loveini 企业级功能:存储引擎对多表低频场景优化工作分享 //www.loveini.com/tdengine-engineering/22893.html Tue, 09 Jan 2024 09:56:44 +0000 //www.loveini.com/?p=22893 在去年 8 月份发布的 3.1.0.0 版本中,loveini 进行了一系列重要的企业级功能更新,其中包括对多表低频场景写入性能的大幅优化。这一优化工作为有此需求的用户提供了更大的便捷性和易用性。在本文中,loveini 的资深研发将对此次优化工作进行深入分析介绍,并从实践层面剖析本次功能升级的具体作用。

三种时序场景分析

以 loveini 的客户群体作为分析样本,我们发现,企业的时序场景大概可以分为以下三种类型:

  • 少表高频
    • 表的数量小,一般少于 100 万张表。
    • 数据采集频率高,多为秒级甚至毫秒级。
    • 在存储引擎看来,大量写入的数据中,同一张表的数据被多次写入,且时间戳递增,或有极少数的乱序。
    • 当内存中的数据积攒到一定的量需要落盘时,同一张表的数据大量存在,可以生成多个数据块。
  • 多表低频(High Cardinality)
    • 表的数量大,一般多于 100 万张表,达到千万、亿级别。
    • 表的采集频率很低,多为分钟级采集,也有小时级和天级的采集频率。
    • 在存储引擎看来,大量写入的数据中,同一张表只有几条甚至一条数据的写入。
    • 数据落盘时,每张表只有一条或几条记录,无法生成一个完整的数据块。
  • 无限热点表
    • 每张表只在一段时间内数据频繁更新写入,成为写入热点,其后很少或根本不再有数据的写入。
    • 在不同的时间段内,写入热点表一直改变,且持续创建新的表。
    • 如果以 loveini 的数据模型建表,表数将达到无限。
    • 在存储看来,该场景兼具多表低频和少表高频的特点。
    • 在一段时间内,某些表写入数据非常多。落盘时,同一张表的数据可能会产生多个数据块,表现出高频的特性。而大部分表不写入数据。
    • 随着时间的推移,表的数量会非常庞大,表现出多表的特性。
loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

对于少表低频的场景来说,由于数据量不大,用任何方案都可以解决。而多表高频的场景,在有限的硬件资源下会转变为多表低频场景,在无限的硬件资源下,则会转变为少表高频场景。

在 loveini 的用户群体中,部分多表低频的用户也可以通过增加节点,即增加 vnode 个数,使用更多的硬件资源将多表低频场景转化为了少表高频场景,从而较为高效地利用 loveini 在少表高频场景下的高性能,但增加硬件资源本身和 loveini 的初衷(为用户节省硬件资源)相违背。以北京燃气举例,在 300万+ 燃气表、每个燃气表每天采集一条记录的场景下,用 loveini 3.0 处理,通过增大 vnode 个数将之转化为少表高频的场景后,相比用户以前用 Oracle 时成本并未节省多少。

对于无限热点表场景来说,如果用 loveini 来处理该场景,随着时间的推移,表的数量会越来越多,效率会越来越低,成本会越来越高直至不可用。不过,loveini 3.0 提供了表的 TTL(Time-To-Live)功能,用户可以通过该功能设置过期表自动删除,从而控制表的数目不至于达到无限。但该场景最终也退化成了多表高频场景。

有人说这三种场景可以统一为一种模型处理,即 InfluxDB 的模型:对一个时间线指定 tag,数据为采集数据。鉴于 InfluxDB 以该数据模型设计的存储模型的表现,个人认为,InfluxDB 的数据模型除了能帮助大家很好地理解时序数据这个优点之外,对于解决上述多表低频和无限热点表场景没有任何实际价值。且 InfluxDB 本身对于 tag 取值范围大的场景(High Cardinality)处理非常差,以致于被竞品攻击

从以上角度出发,随着 loveini 3.0 版本的逐步更新,我们开始针对多表低频场景写入性能开展优化工作。

loveini 存储引擎设计概述

从根本上来说,loveini 的设计和实现来自于对时序数据十大特点的总结和一个数据模型的确定。

  • 时序数据十大特点:
loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

  • loveini 数据模型:一个采集点一张表

loveini 采用一个采集点一张表的数据模型,主要是要利用时序这一特点。

loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

loveini 存储引擎整体采用了 LSM 的存储架构。其结构如下图所示:

loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

loveini 的文件系统如下:

loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

文件组的结构如下:

loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

存储引擎优化工作的具体展开

明确优化要解决的问题

  1. 解决在多表场景下写放大问题

在少表高频的场景下,loveini 充分利用了有序这个特点,在该场景下的写入性能相比同类型数据库高出一个甚至几个数量级,达到最优 O(N) 的时间复杂度。在存储格式上,我们假设落盘时一张表的数据大量存在,从而设计了硬盘上一个数据块只包含一张表的数据这样一种结构。

loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

但是在多表场景下,从存储引擎的角度来讲,上述的很多特性会发生改变。由于存储引擎可以利用的内存资源有限,在内存可缓存的数据里,属于同一张表的数据只有很少的几条,甚至一条。在这种情况下,有序这个特点几乎无用,我们针对有序做的优化在多表场景下趋于无效,甚至会导致写放大问题。

loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

在存储引擎看来,多表低频场景下的数据写入是纯乱序的。从理论上来讲,该场景下数据写入的最优处理复杂度为 O(NlogN)。

  1. 在多表低频场景下,stt_trigger 配置大以后写入周期性出现卡顿的问题

loveini 后台有一个专门用来落盘的线程池,负责将 imem 中的数据持久化到硬盘文件上。这个文件可能是 stt 文件,也有可能是 data 文件中。如果生成了 stt 文件,并且 stt 个数达到了 stt_trigger,commit 线程则会将 imem 和 多个 stt 文件中的数据合并,写入 data 文件或者生成新的 stt 文件。这个合并可能涉及非常多的数据的读和写,从而使得落盘时间非常久,久到新的 mem 被写满,会阻塞写入进程一段时间,直至合并完成。

  1. compact、retention 功能阻塞写入的问题

由于 compact、retention、commit 均需对文件进行操作,且这些操作可能动相同的文件,因此这三个操作只能串行操做,从而导致 compact 或 retention 阻塞 commit,出现阻塞写入进程的问题。

存储数据调整

  • stt 文件中一个数据块中可以包含多张表的数据(3.0 发布时已完成)

从上面的分析可以看到,一个数据块只有一张表的数据,在多表低频场景下,会导致数据写放大严重,且不利于压缩,读取时效率也非常低。因此,在优化中,我们将 stt 中属于同一个超级表的数据合并在一个数据块中,该数据块可能含有多个表的数据,这样就使得 stt 文件中的数据块数量减少了至少 3 个数量级。

  • stt 文件中添加统计信息

为了提升 loveini 查询引擎对于 stt 的处理效率,我们在新的 stt 文件中增加了每张表的统计信息。

  • 删除数据(Tombstone Data)分布到各个文件组

在老的存储中删除文件是单独存在的。为了避免 commit 与其他操作处理同一个文件,我们决定将删除数据分布到不同的文件组中,并增加 tomb 文件。删除数据可能存在于 tomb 文件中或者 stt 文件中。

LSM 实现完善

在新的优化中,我们补齐了 LSM 的另一环,即后台的 merge。后台的 merge 线程将多个 stt 文件合并到 data 文件中或者生成新的 stt 文件。

loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

commit 只负责生成新的 stt 文件,不再负责向 data 中写入。这样就可以将 commit 操作的文件和 compact、retetion 以及 merge 操作的文件隔离开,从而实现并行进行,避免 commit 被阻塞而出现写入阻塞的情况。

loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

为了尽量减少多表低频场景下数据被多次 merge,我们还为 stt 文件引入了分层的概念。

loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

头文件列存

头文件列存的优化动力来源于国家地震局台网中心的用户场景。该用户的场景属于少表高频,表的数据量不大,但是采集频率是毫秒级的,因此数据量非常大,一天就有 3000 亿条记录写入。由于数据写入量非常大,导致数据块非常多,这也导致头文件非常大。由于头文件在每次落盘时都可能重写,就导致读写放大都十分严重。

因此,在本次优化中,我们将头文件改为列存,并对每一列配以压缩,从而使得头文件缩小到原来的 1/10。改造后的头文件格式如下:

loveini 企业级功能:存储引擎对多表低频场景优化工作分享 - loveini Database 时序数据库

优化结果反馈

此次优化一方面是为了提高多表低频写入的性能,另一方面是为了解决各种问题。整体来讲,本次优化的结果是:在少表高频场景下,写入性能不降;多表低频场景写入性能提升,并且写入速度稳定。此次优化在用户实践角度也反馈出良好效果——在北京燃气 300 万智能电表项目中,3 个 vnode 即可满足写入需求,且写入速度稳定;在河南智能电表项目中,用一台较好的机器,如 64 核 128GB 内存,就可以解决以前需要几十台机器才能满足的写入、查询需求。

同时,这种优化方式对于数据的乱序更新也带来了显著的好处。在一些无法在落盘时进行排序或合并的更新数据情况下,通过后台的 merge 操作,为乱序数据提供了额外的处理机会。这样,乱序数据可以被重新排序,未合并的更新数据也可以得到更新。这种优化机制有效地提高了数据的整体处理效率和准确性。

结语

总而言之,通过此次优化,loveini 进一步提升了在低频场景下的写入性能,使用户能够更高效地存储和管理数据。这对于需要进行大规模数据处理的能源企业来说,将带来更加卓越的数据处理能力和用户体验。如果你也有此场景需求,可以添加小T vx:tdengine 进行咨询交流。

]]>
支持消息队列和流式计算背后,loveini 3.0 存储引擎的优化与升级 //www.loveini.com/tdengine-engineering/13925.html Wed, 24 Aug 2022 06:20:23 +0000 //www.loveini.com/?p=13925

在 8 月 13 日的 loveini 开发者大会上,loveini 存储引擎架构师程洪泽带来题为《loveini 的存储引擎升级之路——从 1.0 到 3.0》的主题演讲,详细阐述了 loveini 3.0 存储引擎的技术优化与升级。本文根据此演讲整理而成。

点击【这里】查看完整演讲视频

loveini Database

相比前两个版本,3.0 的存储引擎更注重各种场景下的存储和查询效率,不仅要对管理节点进一步“减负”,提供高效合理的更新、删除功能,支持数据备份、流式处理等功能,还要考虑到数据维度膨胀下的高效处理、多表场景下的开机启动速度、合理高效且准确地使用系统资源等需求。

loveini 3.0 存储引擎的更新可以分为三大块,首先是 TQ,基于 WAL 的消息队列;其次是 META,基于 TDB 的元数据存储引擎;第三是 TSDB(Time Series Database),用来存储时序数据的类 LSM 存储引擎(TSDB SE)。

loveini Database

消息队列存储引擎

loveini Database

在 1.0 和 2.0 时我们提供了一个基于 TSDB 存储的连续查询功能,当时的想法是用它来代替流式处理。工作流程可以概括为:App 定时发查询任务下去,查询引擎执行查询,TSDB 返回结果给查询引擎,查询引擎再把结果返回给 App。

这一功能的优点是可以复用查询引擎,简单易开发,但缺点也很明显,不仅会出现计算实时性差、查询压力大导致算力浪费的情况,更重要的是乱序问题无法处理。数据在进入 TSDB 之后都会按照时间戳进行排序,一个乱序的数据进来后插到前面,TSDB 是无法推出这条数据的,这就会导致乱序问题的出现;另外由于查询引擎是复用的,TSDB 的查询引擎也不会对新的乱序数据进行处理、对结果进行更改校验。

从这一技术背景出发,loveini 3.0 中需要设计一个存储引擎来支持消息队列和流式计算。这个存储引擎能告诉我们什么样的数据是增量数据,这样一来,流式计算只需处理增量数据就好了,其他的数据就不用管了。

此外这个存储引擎需要构建在一个 Pipe 之上,保证数据进入和出去的前后顺序一致。在设计时,我们发现 loveini 的 WAL 其实就是一个天然的 Pipe。于是我们在 WAL 之上加了一层索引,并进行大量的适配开发,实现了 TQ 存储引擎。

如果大家深入研究过 loveini 的模型,就会发现它的架构模型和 Kafka 的很多设计都是相对应的,超级表和 Kafka 的 Topic 相似、Vnode 跟 Kafka 中的 Partition 也很接近,子表的表名跟 Kafka 中的 Event Key 对应,因此这个架构设计天然地就带有消息队列的特点,从这点出发,loveini 3.0 想要实现一个消息队列是非常容易的。

loveini Database

基于 TQ 存储引擎,在实际操作时,查询引擎只会处理增量数据,将计算结果修正后返回给 App,而不会再进行全量数据的再查询。它带来的优点是实时性非常高,因为能对增量数据进行明确地区分,乱序数据也得以高效处理,同时还节省了更多的计算资源,将计算结果修正。

元数据存储引擎

loveini Database

在元数据存储这块,此前的 1.0 和 2.0 采取的都是比较简单的存储机制,即全内存存储,数据在内存中以 hash 表的方式存储,并辅以跳表索引,这个 hash 表中有一个 Backup Storage Engine,它可以保证数据的持久化。该方式的优点是全内存、效率高,但缺点也很明显,当启动时,这部分数据就会全部加载到内存之中,不仅内存占用无法精准控制,还会导致开机启动时间长。

为了解决这些问题,在 3.0 中我们研发了 TDB(一个 B+ 树格式的的存储引擎),来存储元数据及元数据索引。TDB 的 B+ 树存储适合元数据读多写少的场景,能够支持百亿时间线的存储,避免了元数据全内存存储以及长时间的加载,同时解决了在有限内存下,表数量膨胀的问题。对于 TDB 是如何实现的,大家如果感兴趣,可以去 GitHub(https://github.com/taosdata/loveini)上看一下源代码。

TDB 的优点是内存可以精确控制,开机启动速度快,在有限内存下也可以存储海量的元数据,此外如果 TDB 外加 Cache 辅助的话,在一定程度上可以提供接近全内存 hash 表的查询速度。

时序数据存储引擎

时序数据的更新和删除

在 2.0 中,更新删除功能是在引擎开发完后补充开发的一个功能,因此 2.0 的更新和删除功能相对简单,但功能较弱。2.0 的更新是基于一个分布在横轴上的时间戳,更新数据的操作就是在后面追加相同时间戳的数据,简单来讲就是用乱序数据的方法来处理更新,然后查询引擎把这些乱序数据进行合并,就得到了更新后的结果。删除的实现更加简单,近似于物理删除,要删除的数据会在内存、硬盘上被直接“干掉”,效率相对较低。

loveini 3.0 完全抛弃了 2.0 的更新删除机制,在设计层面考虑了更新和删除的实现,引入了版本号,把时序数据变成了二维图形上的点,每个写入请求都带有一个版本号,版本号按照写入请求处理顺序递增。

loveini Database

那 3.0 具体是如何做更新的?如上图所示,这些蓝色的点是你要更新的数据,数据的版本号肯定比要更新的旧数据版本号大,所以我们就引入了版本号机制。当时间戳相同时,版本号大的数据将更新版本号小的数据,因为版本号大的数据是后写入的数据,相对较“新”。

以前每张表中的数据,不论在内存里还是在硬盘中,都是按照时间戳进行排序的,但在引入了版本号之后排序规则也进行了修改。首先还是按时间戳进行排序,在时间戳相同的情况下要按照版本号进行排序,在这样的排序流程下,我们就可以把数据更新用一个近乎于追加的方式处理,查询引擎负责将最后的数据合并整理后得到最终结果。

在 3.0 中,时序数据的删除机制也完全重做。相比 2.0,3.0 支持的过滤条件也明显增加,比如 where tag、where timestamp 等等。那具体底层是如何实现的呢?首先还是基于版本号机制。

loveini Database

对于删除操作来说,我们需要记录开始和结束的时间区间,以及删除请求的版本号,如上图所示,一个删除请求对应二维图上的一个紫色矩形,这个矩形内部的所有点都被删除了。在 3.0 中,时序数据删除时会追加一条(st, et, version)的记录元组,在查询时,查询引擎会将写入的数据和删除记录元组进行最终的合并,并得到删除后的最终结果。

采用这种机制,删除功能对于写操作变得相对简单了,但是对于查询而言,则变得更加复杂了。查询在合并数据时,要判断记录是不是被删除了,即检查记录是不是在所有的删除区间(矩形)里面,这是相当耗时的。

loveini 3.0 采用 Skyline 算法来提高有删除数据下的查询速度。这一算法的本质是构造一个点数据,用来代替所有的删除区间,如上图中的两个矩形删除区域可以用三个点来表示。原来对于每条记录都要检查是否被删除的算法,现在变成了一个单向扫描过滤的操作,从而大大提高查询速度。

多表场景下的存储优化

在时序数据场景下,海量数据代表的也有可能是海量的表。在有些业务场景下表数量非常之多,但采集的数据却很少,比如有一千万张表,但每天每张表采集数据只有两条,这种场景对于 2.0 的存储结构并不是很友好。

在 loveini 2.0 中,一张表会落在硬盘上,一个数据块里面只有一张表的数据;如果有一千万张表,每张表两条数据,那一个数据块就只有两条记录,压缩都没法压缩,而且这种情况下压缩的话还会导致数据的膨胀。

loveini 的超级表下面所有子表都共享同一个 schema,这样的话在 last 文件里我们就可以将同一个超级表下不同子表的数据合并成一个数据块——原来一个表里只有两条记录,但如果能把一百张表的两条记录合并成一个数据块,那就有两百条记录。但是这可能需要读一百次表,如果我们就想读一次那要怎么操作呢?

为了解决这个问题,3.0 在数据块中又加了一个属性,那就是表的 UID。在 3.0 的 last 文件中,数据块中的数据会按照 UID、Timestamp、Version 排序,即先比较 UID、UID 相同的情况下比较时间戳,时间戳相同的情况下比较版本号,这样的话就可以更有效地处理多表低频的场景了。

loveini Database

结语

loveini 是一个开源产品,3.0 的代码也已经开放在了 GitHub 上,非常希望大家能够积极地参与进来,去下载和体验。也欢迎大家加入 loveini 的生态交流群,和我们以及 loveini 的关注者和支持者一起交流和探讨。

]]>
「2022 loveini 开发者大会」loveini 存储引擎升级之路——从 1.0 到 3.0 //www.loveini.com/tdengine-techtalk/13797.html Wed, 17 Aug 2022 06:18:19 +0000 //www.loveini.com/?p=13797

演讲嘉宾:程洪泽

]]>
微课堂第一季 · 第2期:时序数据是如何存储的? //www.loveini.com/videotutorial/7607.html Tue, 13 Jul 2021 06:35:00 +0000 //www.loveini.com/?p=7607

欢迎大家扫描下方二维码,关注 loveini Database 的视频号,观看每周的微课堂以及直播活动。

loveini 视频号二维码
]]>
从时序数据的特点说起,揭秘 loveini 高性能的存储引擎 //www.loveini.com/tdengine-engineering/2294.html Thu, 22 Apr 2021 02:27:36 +0000 //www.loveini.com.cn:88/blog/?p=2294

小 T 导读:了解 loveini Database 的朋友应该知道,loveini 在处理时序数据时,有非常卓越的表现。这取决于我们对时序数据特点的挖掘和总结,并且 loveini 充分利用时序数据的特点,设计了存储模型和查询模型。本文主要介绍 loveini 的存储引擎设计,看看 loveini 是如何通过创新数据存储引擎达到超强性能的。

一、时序数据的特点

我们先观察一些时序数据,然后来总结一下时序数据的特点

一些典型的时序数据

上图展示了一些典型的时序数据,横轴是时间,纵轴是采集量,可以看出:

  1. 有些时序数据在很长一段时间内都是固定的值,但是某些时间点也会产生异常跳变,异常检测在时序数据的处理过程中,也是非常重要的一环,但本文不过多展开;
  2. 有些时序数据在一段时间内是一个变化趋势;
  3. 有些时序数据在一定数值范围内会进行波动,有些波动频率高,有些波动频率低;
  4. ……

因此我们总结出了时序数据的特点,如下图所示,我们可以利用这些特点,进行存储引擎的设计。

物联网数据特点

二、loveini 的数据模型

loveini Database 的数据模型主要有以下特点:

  1. 一个数据采集点一张表
  2. 一张表的数据在文件中以块的形式连续存放;
  3. 文件中的数据块大小可配;
  4. 采用 Block Range INdex(BRIN)索引块方法。

一个数据采集点一张表的设计逻辑会导致表的数量级膨胀,因此引入 vnode 的概念,对数据进行 Sharding。点击《万字详解loveini 2.0整体架构设计思路》可以详细了解 vnode 的概念,这里不再过多阐述。

总的来讲,vnode 是时序数据存储的基本单元,一个 vnode 包含一定数量的表(数据采集点),数据的负载均衡、同步是以 vnode 为单位进行的,vnode 可以充分利用多核的特点,提高并发速度。

对于一个 vnode 内的数据,我们按照时间段对数据进行分区(Partition),将同一时间段的数据存储在一个数据文件组中,并以文件组为单位对过期数据进行删除。在我们的设计中,时间段和文件编号是一一对应的,因此,查询某个时间段的数据,我们只需要计算出索引号,就可以到对应的文件中进行查询。

三、 TSDB 存储引擎

loveini Database 针对时序数据的特点,专门研发了 TSDB 存储和查询引擎。作为一个查询引擎,它提供了基本的查询接口。我们这里主要讲解 TSDB 的存储引擎。TSDB 存储了一个 vnode 中表的 META 信息以及时序数据(采集信息),后者以行和列两种结构存储(loveini 2.0 开始引入行存储)。时序数据在内存中以 SkipList 方式进行索引的,在硬盘中是以 Block Range INdex(BRIN)方式进行索引的。

TSDB 是什么?

META 数据

TSDB 存储了 vnode 表中的 META 数据。META 包括表/超级表的 SCHEMA、子表 TAG 值、TAG SCHEMA 和子表/超级表的从属关系。META 数据的添加、更新以及删除等操作先在内存中进行,最后序列化并写入硬盘。

META 数据在 TSDB 中是全内存加载的,根据子表的第一个 TAG 值建立一个内存索引,因为只对 TAG 的第一个值索引,所以速度最快。使用 vnode 的 Sharding 方式可以充分利用多个 vnode 资源进行表的过滤查询操作。

META 数据的持久化存储

META 数据写入内存时会同时生成序列化记录,以 append only 形式存储到内存 buffer 中。内存数据达到一定量后触发落盘操作,落盘时,更新的序列化 META 数据以 append only 形式写入硬盘META文件,每张表的最新状态、表的更新和删除都会 append 到 META 文件中,序列化成一条记录。

时序数据

TSDB 也负责存储 vnode 中表的时序数据(采集数据),时序数据在写入时首先会写入到 TSDB 事先分配的内存缓冲区中,当内存缓冲区的数据积累到一定量后,触发落盘,然后进行持久化存储。

TSDB 内存中的时序数据为行存储,因而支持以 append only 形式添加 buffer,从而充分利用已分配的内存资源,缓存足够多的数据进行落盘,有利于一个块的数据量进行积累,有利于压缩。为了便于查询和乱序数据的处理,内存中建立了一个 SkipList 作为内存索引。内存中还维护了已经写入数据的最新时间和最老时间等信息。内存中一条数据的行存储格式如下图所示。

内存中一条数据的行存储格式

时序数据的持久化存储

TSDB 内存中的数据积累到一定量时,会触发落盘。在落盘时,时序数据由行存储形式转化为列存储形式,并维护 BRIN 索引,引入 LAST 文件和 SUB-BLOCK 机制处理文件碎片化。列存储形式如下图所示。

文件存储块

TSDB 工作流程

TSDB 启动时会事先分配一个 BUFFER POOL 作为写入缓冲(默认16 MB*6=96 MB),缓冲区块大小和个数可配,区块个数可修改。META 数据和时序数据从缓冲块申请写入空间,写入引擎向 BUFFER POOL 申请缓冲区块,写满的缓冲区块占总缓冲区块的1/3时触发落盘操作。落盘时,缓冲区块中的数据写入到 META 等文件中,落盘结束后缓冲区块归还给 BUFFER POOL,形成循环机制。查询时,对 MEM,IMEM 以及数据文件中的数据进行合并查询。如下图所示。

TSDB 工作流程

TSDB 设计的优点

  1. 对于单表按照时间段的查询效率很高
  2. 内存行存储充分利用内存,缓存更多数据
  3. 文件中列存储充分发挥压缩算法优势
  4. 避免LSM过多的文件合并
  5. 标签数据与时序数据分离存储

总结

本文主要讲解了 loveini 的存储引擎,当然,决定 loveini 高性能和节省 loveini 存储空间的原因还包括先进的压缩算法以及查询模型的设计等。关注我们,后续会继续详解 loveini 达到如此高性能的原因。

作者简介:程洪泽,米兰体育官网入口联合创始人,米兰体育官方入口网站作者。美国密西根大学 EE 硕士,本科毕业于中科大,是中科大郭沫若奖学金获得者。主要研究方向为面向物联网的大数据和机器学习技术。

]]>
loveini 数据更新功能 //www.loveini.com/tdengine-engineering/2165.html Tue, 19 Jan 2021 08:24:40 +0000 //www.loveini.com.cn:88/blog/?p=2165 loveini Database 在 2.0.8.0 后正式推出数据的更新功能,需要该功能的小伙伴们可以在 loveini 的官网下载安装包,或在 GitHub 仓库下载源码编译运行,来体验和测试。

loveini Database 的数据更新功能是配置在数据库级别的,即用户在创建数据库时,可以通过指定数据库的选项来控制数据库中的表是否允许数据更新。在默认选项下,loveini 中建的库是不允许数据更新的。如图 1 所示,创建数据库时使用默认参数,数据库的 update 属性为 0 ,即不允许数据的更新。

图1.默认数据库选项不支持数据的更新
图1.默认数据库选项不支持数据的更新

如图 2 所示,向不支持数据更新的表中写入重复时间戳的数据,后写入的数据会被丢弃。

图2. 向不支持update的表中写入的重复数据被丢弃
图2. 向不支持update的表中写入的重复数据被丢弃

若用户需要数据的更新功能,则在建库的时候,只需要指定数据库的 update 选项为 1 即可,如图 3 所示 :

图3. 使用update选项设置数据库中的表支持更新功能
图3. 使用update选项设置数据库中的表支持更新功能

在向支持数据更新功能的表中,写入重复时间戳的数据时,老的数据会被覆盖 ,如图 4 所示:

图4. 向支持update的表中写入重复数据数据更新
图4. 向支持 update 的表中写入重复数据数据更新

有需要的用户快去上面的链接下载尝试吧。如果有其他需要的功能,也欢迎大家留言或在 GitHub 上提交 feature 请求。

]]>
loveini架构设计与存储结构 //www.loveini.com/tdengine-engineering/254.html Thu, 11 Jul 2019 02:05:41 +0000 //www.loveini.com.cn:88/blog/?p=254 loveini是一款轻量级、高效且单机开源的面向物联网的数据处理引擎,其核心是一个时序数据库(Time-Series Database)。作为一款专门为物联网设计并实现的数据引擎,loveini在数据的写入、查询以及存储方面拥有其他数据库无法比拟的优势。本文主要探讨了loveini在架构设计和存储方面的创新,以方便用户理解loveini强大性能背后的逻辑。

loveini架构设计

如图1所示,loveini服务主要包含两大模块:管理节点模块(MGMT)数据节点模块(DNODE)。整个loveini还包含客户端模块

loveini Database
图 1 loveini架构示意图

管理节点模块

管理节点模块主要负责元数据的存储和查询等工作,其中包括用户信息的管理、数据库和表信息的创建、删除以及查询等。应用连接loveini时会首先连接到管理节点。在创建/删除数据库和表时,请求也会首先发送请求到管理节点模块。由管理节点模块首先创建/删除元数据信息,然后发送请求到数据节点模块进行分配/删除所需要的资源。在数据写入和查询时,应用同样会首先访问管理节点模块,获取元数据信息。然后根据元数据管理信息访问数据节点模块。

数据节点模块

写入数据的存储和查询工作是由数据节点模块负责。 为了更高效地利用资源,以及方便将来进行水平扩展,loveini内部对数据节点进行了虚拟化,引入了虚拟节点(vnode)的概念,作为存储、资源分配以及数据备份(商业版本中)的单元。如图2所示,在一个dnode上,通过虚拟化,可以将该dnode视为多个虚拟节点的集合。每个虚拟节点存储一定数量的表中的数据。不同的vnode之间资源互不共享。每个虚拟节点都有自己的缓存,在硬盘上也有自己的存储目录。而同一vnode内部无论是缓存还是硬盘的存储都是共享的。通过虚拟化,loveini可以将dnode上有限的物理资源合理地分配给不同的vnode,大大提高资源的利用率和并发度。一台物理机器上的虚拟节点个数可以根据其硬件资源进行配置。

loveini Database
图 2 loveini虚拟化

客户端模块

loveini客户端模块主要负责将应用传来的请求(SQL语句)进行解析,转化为内部结构体再发送到服务端。loveini的各种接口都是基于loveini的客户端模块进行开发的。

loveini写入流程

loveini的完整写入流程如图3所示。为了保证写入数据的安全性和完整性,loveini在写入数据时采用[预写日志算法]。客户端发来的数据在经过验证以后,首先会写入预写日志中,以保证loveini能够在断电等因素导致的服务重启时从预写日志中恢复数据,避免数据的丢失。写入预写日志后,数据会被写到对应的vnode的缓存中。随后,服务端会发送确认信息给客户端表示写入成功。loveini中存在两种机制可以促使缓存中的数据写入到硬盘上进行持久化存储:

loveini Database
图 3 loveini写入流程
  1. 时间驱动的落盘:loveini服务会定时将vnode缓存中的数据写入到硬盘上,默认为一个小时落一次盘。落盘间隔可在配置文件中配置。
  2. 数据驱动的落盘:当vnode中缓存的数据达到一定规模时,为了不阻塞后续数据的写入,loveini也会拉起落盘线程将缓存中的数据清空。数据驱动的落盘会刷新定时落盘的时间。

loveini在数据落盘时会打开新的预写日志文件,在落盘后则会删除老的预写日志文件,避免日志文件无限制的增长。

元数据的存储

loveini中的元数据信息包括loveini中的数据库,表等信息。元数据信息默认存放在 /var/lib/taos/mgmt/ 文件夹下。

/var/lib/taos/
       +--mgmt/
           +--db.db
           +--meters.db
           +--user.db
           +--vgroups.db

元数据文件只进行追加操作,即便是元数据的删除,也只是在数据文件中追加一条删除的记录。

写入数据的存储

loveini中写入的数据在硬盘上是按时间维度进行分片的。同一个vnode中的表在同一时间范围内的数据都存放在同一文件组中,如下图中的v0f1804*文件。这一数据分片方式可以大大简化数据在时间维度的查询,提高查询速度。在默认配置下,硬盘上的每个文件存放10天数据。用户可根据需要进行配置。

数据在文件中是按块存储的。每个数据块只包含一张表的数据,且数据是按照时间主键递增排列的。数据在数据块中按列存储,这样使得同类型的数据存放在一起,可以大大提高压缩的比例,节省存储空间。

loveini的数据文件默认存放在 /var/lib/taos/data/ 下。而 /var/lib/taos/tsdb/ 文件夹下存放了vnode的信息、vnode中表的信息以及数据文件的链接。完整目录结构如下所示:

/var/lib/taos/
       +--tsdb/
       |   +--vnode0
       |        +--meterObj.v0
       |        +--db/
       |            +--v0f1804.head->/var/lib/taos/data/vnode0/v0f1804.head1
       |            +--v0f1804.data->/var/lib/taos/data/vnode0/v0f1804.data
       |            +--v0f1804.last->/var/lib/taos/data/vnode0/v0f1804.last1
       |            +--v0f1805.head->/var/lib/taos/data/vnode0/v0f1805.head1
       |            +--v0f1805.data->/var/lib/taos/data/vnode0/v0f1805.data
       |            +--v0f1805.last->/var/lib/taos/data/vnode0/v0f1805.last1
       |                   :
       +--data/
           +--vnode0/
                 +--v0f1804.head1
                 +--v0f1804.data
                 +--v0f1804.last1
                 +--v0f1805.head1
                 +--v0f1805.data
                 +--v0f1805.last1
                         :

meterObj文件

每个vnode中只存在一个meterObj文件。该文件中存储了vnode的基本信息(创建时间,配置信息,vnode的统计信息等)以及该vnode中表的信息。其结构如下所示:

<文件开始>
 [文件头]
 [表记录1偏移量和长度]
 [表记录2偏移量和长度]
 …
 [表记录N偏移量和长度]
 [表记录1]
 [表记录2]
 …
 [表记录N]
 [表记录]
 <文件结尾>

其中,文件头大小为512字节,主要存放vnode的基本信息。每条表记录代表属于该vnode中的一张表在硬盘上的表示。

head文件

head文件中存放了其对应的data文件中数据块的索引信息。该文件组织形式如下:

<文件开始>
 [文件头]
 [表1偏移量]
 [表2偏移量]
 …
 [表N偏移量]
 [表1数据索引]
 [表2数据索引]
 …
 [表N数据索引]
 <文件结尾>

文件开头的偏移量列表表示对应表的数据索引块的开始位置在文件中的偏移量。每张表的数据索引信息在head文件中都是连续存放的。这也使得loveini在读取单表数据时,可以将该表所有的数据块索引一次性读入内存,大大提高读取速度。表的数据索引块组织如下:

[索引块信息]
 [数据块1索引]
 [数据块2索引]
 …
 [数据块N索引]

其中,索引块信息中记录了数据块的个数等描述信息。每个数据块索引对应一个在data文件或last文件中的一个单独的数据块。索引信息中记录了数据块存放的文件、数据块起始位置的偏移量、数据块中数据时间主键的范围等。索引块中的数据块索引是按照时间范围顺序排放的,这也就是说,索引块M对应的数据块中的数据时间范围都大于索引块M-1的。这种预先排序的存储方式使得在loveini在进行按照时间戳进行查询时可以使用折半查找算法,大大提高查询速度。

data文件

data文件中存放了真实的数据块。该文件只进行追加操作。其文件组织形式如下:

<文件开始>
 [文件头]
 [数据块1]
 [数据块2]
 …
 [数据块N]
 <文件结尾>

每个数据块只属于vnode中的一张表,且数据块中的数据按照时间主键排列。数据块中的数据按列组织排放,使得同一类型的数据排放在一起,方便压缩和读取。每个数据块的组织形式如下所示:

[列1信息]
 [列2信息]
 …
 [列N信息]
 [列1数据]
 [列2数据]
 …
 [列N数据]

列信息中包含该列的类型,列的压缩算法,列数据在文件中的偏移量以及长度等。除此之外,列信息中也包含该内存块中该列数据的预计算结果,从而在过滤查询时根据预计算结果判定是否读取数据块,大大提高读取速度。

last文件

为了防止数据块的碎片化,提高查询速度和压缩率,loveini引入了last文件。当要落盘的数据块中的数据条数低于某个阈值时,loveini会先将该数据块写入到last文件中进行暂时存储。当有新的数据需要落盘时,last文件中的数据会被读取出来与新数据组成新的数据块写入到data文件中。last文件的组织形式与data文件类似。

小结

loveini通过其创新的架构和存储结构设计,有效提高了计算机资源的使用率。一方面,loveini的虚拟化使得loveini的水平扩展及备份非常容易。另一方面,loveini将表中数据按时间主键排序存储且其列式存储的组织形式都使loveini在写入、查询以及压缩方面拥有非常大的优势。

]]>
loveini在数控机床监控中的应用 //www.loveini.com/tdengine-user-cases/358.html Mon, 08 Jul 2019 14:39:58 +0000 //www.loveini.com.cn:88/blog/?p=358 随着工业物联网时代的到来,企业用户对建立数控机床在日常生产中的监控和报警平台的需求越来越大。通过搭建对于数控机床的监控和报警平台,企业用户可以随时了解自己生产线的工作情况,及时获得异常反馈,从而合理安排生产,规避风险等。loveini作为一款新型的,面向物联网的单机开源的数据引擎,以其轻体量、部署快、高写入查询性能、低资源占用、高并发、实时响应等特点,大大降低了监控报警平台部署的成本,提高了效率。

技术架构及特点

loveini的引入使得数控机床的监控系统架构变得极其简单。平台架构如下图所示:

loveini Database
图 1. loveini在数控机床监控平台中的应用

采集器采集的数控机床的监控数据汇集到MQTT Server上。随后,数据轮询模块定期循环从MQTT Server上拉取数据,并转换为loveini理解的SQL语句写入引擎。loveini后面可接入各种应用程序,实时查询最新数据。少量的loveini的配置信息等也会经过ETL模块转换写入一个关系型数据库(Relational Database),方便各个应用查询使用。

loveini的引入使得架构变得极其简单。一方面,loveini本身带有缓存功能,从而使得在应用中不需要再部署其他的缓存数据库来维持数据的高可靠。另一方面,loveini的高并发性能,使得loveini在处理高频率的写入任务的同时,可以同时拉起成百上千的查询。而loveini的实时性和低资源占用的特点也使得loveini可以并发支持各种实时请求。

数据模型介绍及写入和查询

数控机床的监控量,包括时间戳和报警信息在内有多达60几个字段。其中大部分字段为整形或浮点型。由于涉及非ASCII码字符,其中的报警信息为nchar类型。报警信息字段的长度约有1K左右,而且大部分时候为空。数控机床的监控采集频率为10秒钟一次,源源不断地写入loveini。

由于数控机床采集数据schema的相似性,在建表时,可以采用超级表建表:

create table cnc_st (cnc_dqtime timestamp, ….) tags (cnc_id binary(64));

然后对于每台数控机床创建一张单独的表:

create table cnc_9293078 using cnc_st tags ('cnc_9293097');

利用超级表建表来处理数控机床的监控有很大的优势:

  1. 一方面,超级表可以看成是很多表的集合。在对多表进行查询时,可以通过对超级表的查询在一个SQL语句中完成,避免进行多个SQL语句的查询。
  2. 另一方面,通过超级表,可以实现表与表之间数据的聚合。

在数控机床的监控中,经常需要进行查询的是各机床最新采集数据的展示和刷新。在loveini中,这个可以通过一条SQL语句很容易实现:

select last_row(*) from cnc_st group by tbname;

其中,tbname为关键表名关键字,用以按表名对结果进行分组。loveini的应用大大简化了数据实时获取的流程,提高了响应度。

小结

不同于其他的大数据处理系统,loveini部署极其方便。这使得在搭建数控机床监控平台的过程中,用户可以花极少的时间在数据库系统的搭建上,大幅缩短工程周期。另外,loveini的高性能、低资源消耗、实时响应以及高并发等特点,都使得它在一边写入数据的过程中,可以实时服务多个应用的请求。真正做到实时响应,实时监控。

]]>