暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

使用 Apache IoTDB 如何提高存储效率

原创 Apache IoTDB 2023-08-02
464

    各位观众朋友们大家好,我是来自天谋科技的侯昊男,现在是 Apache IoTDB 的一名 PMC。今天我给大家带来的分享是 Apache IoTDB 如何快速高效地存储时序数据。我今天的分享主要分这三个部分,第一个部分我想给大家介绍一下 IoTDB 1.0 之后的一个新的架构。第二部分我想给大家介绍一下,就是我们自研的一个时间序列数据的一个文件存储格式 TsFile。第三部分,我想大家给大家介绍一下我们这个 IoTDB 的一个存储引擎的一个架构,叫 IoTLSM。

    首先第一部分我就先给大家介绍一下 IoTDB 1.0 的一个架构。我们作为一个 1.0 的一个新架构,我们想达到的有一些对于新架构的一些目标。首先是这个核心思想是我们希望我们的这个 IoTDB 的潜力无上限,就是说在资源充足的场景下能够提供这样一个完美的服务。第二个思想就是适配低成本,就是在这种资源有限的场景下能够提供一个最佳的一个选择。为此我们期望的一个系统的目标就是这样六点。第一个就是支持多模式,就是我们期望一种架构可以同时支持单机模式和分布式模式。然后我们也希望能够做到这样一个大容量,可以管理上亿设备和测点,数据量没有上限的一个限制。第三个是期望能够做到一个很高的一个可用性,能够容忍部分节点失效,然后做到一个系统的高可用。第四个目标是希望能够有一个很高的一个扩展性,就是随时可以进行集群的一个扩容,能够平滑的过渡到一个规模更大的一个集群上。第五点是我们期望就是能够继续我们的这样一个高性能的目标,写入吞吐能够达到数千万点每秒。第六个目标是我们也希望能够对我们的系统做有一些可观测性的一些建设,就对我们系统的集群的一个核心性能进行一个监控。

    接下来我给大家介绍一下,就是 IoTDB 这个集群的有这样两个角色。第一个角色是叫 ConfigNode,它是作为管理集群节点和各个分区表信息的这样的一个角色,然后负责集群的整个的任务调度和负载均衡。DataNode 就是作为真实的一个数据的存储和查询的这样一个引擎,就是负责管理各个时间序列的数据和元数据,然后作为查询功能来讲就是我们的那个 MPP 计算引擎也是运行在 DataNode 之上的,整体的一个大概的结构就是如下面这张图所示。上面的应用层指的就是一些我们的客户端,客户端去直接访问我们的数据服务层。数据服务层就是由各个 DataNode 去组成的,每个 DataNode 可能会负责不同的数据分区和不同的元数据分区。在数据服务层之下还有一个就是 ConfigNode 所在的这个集群管理层,负责管理整个集群的一些系统数据。

    然后这张图是我们 Apache IoTDB 1.0 之后整体的一个架构图,也是跟刚才讲的内容类似,就是我们也把整个 DataNode 和 ConfigNode 负责的一些功能表示了一下。ConfigNode 是负责整个集群的管理,它有这么四个模块组成:节点管理、分区路由、负载均衡和操作调度。DataNode 也有不同的一些模块。首先是这个网络协议层,我们也支持不同的网络协议,包括 Session,MQTT,RestAPI,然后 JDBC 这样的一些通讯协议;然后协议之下就是一个查询的一个处理层,负责做这种 SQL 的解析;然后在查询处理层之下,就是我们的这个共识层。我们也支持多种不同的共识协议,包括 Simple,包括我们自研的这个 IoTConsensus 协议,还有一些强一致性的,包括 Ratis 这样的一些共识协议。在共识协议之下就是负责管理我们整个数据和元数据的这样一个角色,就分为 SchemaRegion 和 DataRegion。SchemaRegion 就是负责整个元数据的一些管理,包括元数据本身的缓存和一些元数据的日志的管理。然后 DataRegion 就是管理各个,就是我们真实的一个数据的这样一个存储的结构,包括管理顺乱序的 TsFile,然后包括内存数据的内存表管理,还有一些写前日志,还有合并之类的模块。整体的 IoTDB 的架构,内核就是 ConfigNode 和 DataNode,但在内核之外我们也做了一些生态的对接,包括跟 Spark,Flink,然后跟我们的RocketMQ 之类这些大数据生态也有对接的一些接口。

    IoTDB 1.0 的部署模式方面,我们有这样的四种模式,就是可以根据我们对用户的一些不同的需求做这样的一个模式的一个选择。像有一些用户对于扩展性和这种高可用性没有特别高的要求,但是对于性能有非常高的要求的话,我们会推荐用这种轻量级的这种单机模式去满足他的这样的一个部署需求。如果有些用户有非常高的一些扩展性需求和高可用需求,我们可能会推荐一些高可用、高性能的分布式模式,或者是一些强一致性的分布式模式去满足他们具体的一个业务的需求。

    对于这个集群的一个分配策略和系统扩展,我们是做这样一个查找表的结构。 IoTDB 可以做到一个全面接管、精确控制这样的一个数据分配的这样一个需求,在集群扩展扩容的时候就可以不需要去迁移数据,不像其他的一些数据库的话,在做一些集群扩容的时候可能会做需要一些迁移数据的这样的一个工作。就是我们能够全面的掌握这个分配策略,能够做到一秒扩容。

    IoTDB 的这个共识协议框架,我们是把整个这样一个共识协议做了一个抽象的一个协议层,我们也提供了一个可配置的这样一个共识协议的框架。我们目前是支持了三种不同的一个共识协议:第一种是 Raft 协议,能够去满足强一致性的这样一个保证。第二个是 Simple 协议,这个就是作为一个单副本的一个高性能的这样一个共识协议。第三个是我们自己研发的这个 IoTConsensus 协议,是专门为 IoT 场景进行设计的,只需要两个副本就可以提供高可用的这样一个能力,同时也能满足对于性能高要求的一些需求,能够实现最低成本,并且做到高可用。

    关于读写流程,我们是做了一些缓存,用来节省节点间的这样一个远程通信,实现一个最短路径的读写。右边这张图就是可以表示我们现在的读写大致的一个流程。首先客户端来一个请求,发到一个 DataNode 上,ConfigNode 那边去做这样一个分区的查询,之后我们会再去别的 DataNode,或者在本机做这样一个元数据的查询,之后再去往不同的节点,不同的副本进行一个数据的同步,最后返回给客户端。有了这个缓存的话,我首先这个第二步和第三步的分区查询和元数据查询就可以缓存到这个中间的 DataNode 本地,这样就可以节省掉 2 和 3 的这样一个远程通信的路径。我们也就是通过这样的一个架构,可以做到负载均衡,也可以做到一个高线性扩展比的这样一个效果。 然后我们关于 IoTDB 的一个单机模式,就是可以做到很轻量级的一个效果。首先就是使用这个 SimpleConsensus协议,能够使这个共识协议共识层的开销做到一个最低,达到最好的一个性能。其次就是在 DataNode 之中做一些执行器的一些优化,把需要的一些网络通信变成了本地的一些方法调用。第三个就是做了一些缓存的优化,可以释放多余的一些分区和元数据这类的缓存。

    下一部分我想给大家介绍一下 TsFile,就是我们 IoTDB 的底层的一个文件数据结构。TsFile 是我们从 IoTDB 项目发起的时候就开始自研的一个模块,它大概的一个结构如下面这张图所示,与我们大家应该都知道的 InfluxDB 相比,我们的磁盘占用空间可以降低 85%。

    首先我想给大家介绍一下这个 TsFile 的一个数据部分的这样一个存储结构。首先就是一个大的结构,叫 ChunkGroup,是代表的一个设备一段时间的这样一个数据的存储的一个块。在每个 ChunkGroup 之内有不同的 Chunk,代表一个物理量一段时间的这样一个数据。Chunk 在 TsFile 中也分为这三种,一种是叫 TSChunk,就是它某一个时间和值都存在一个 Chunk 里;然后还有分 TimeChunk,和 ValueChunk,是把 time 和 value 进行分开存储的这样一个 Chunk的结构。在每个 Chunk 之内还有一个 Page 的一个结构,是指一个物理量一段时间的一些数据,不同的多个 Page 可以组合成一个 Chunk,然后 Page 相对的来讲,根据 Chunk 类型的不同也会有不同类型的一个 Page,包括 TSPage、TimePage 和 ValuePage。

    在 TsFile 的数据结构之外,还会有一些索引结构。首先是 TsFile 里面序列内的一个索引结构,这个索引是分为三级,有 Page 级、 Chunk 级和文件级,它的作用就是用来快速的过滤一些数据块,能够减少查询时候的 IO 和物化。同时可以在这样的一个索引结构里面,也会保存一些统计信息,能够在做一些聚合查询的时候,能够直接根据所有里面的统计信息来返回结果,做到查询效率的一个最大化。

    其次就是序列间的这样一个索引结构,我们在 TsFile 的尾部还有这样的一个元数据的索引数,也是分为三层的。首先是设备层,然后是测点层,然后测点层再指向一个具体的序列内的索引。它这样的一个树形结构的管理的作用就是,能够在一个文件里面,如果有海量的一个序列,能够快速高效的去定位到某一个具体的需要查询的序列。

    在 TsFile 中,我们目前是支持了多种的数据类型,也支持了不同的一些编码压缩方式。尤其是像中间的这些数值类型的,INT32、INT64、FLOAT、DOUBLE,也支持了一些比较新的算法,比如说 TS_2DIFF、SPRINTZ,一些可能最近几年才发表出来的一些新的编码算法。在编码外我们还有一层二次的一个压缩,我们现在默认是使用 LZ4 的算法进行一个压缩的,当然我们也支持别的一些压缩算法,包括 GZIP、ZSTD 这些。

    除此之外,IoTDB 的 TsFile 还支持死区处理的这样一个算法 SDT。它是相当于是直接去丢掉一些可能不太重要的、重复的一些点,比如说原本的一个序列可能有 11 个点,然后经过这样一个 STD 的算法的话,可能就只需要保留 5 个点就可以,做到极致的一个压缩。所以现在就是总结一下,目前的 IoTDB 是可以支持这样的一个三级的压缩:首先是经过一些死区处理,就是直接丢掉一些点,当然这个是可配置的,可以不去做这一步。然后第二步是做一个编码,包括也有不同的一些无损或者有损的一些编码方式。最后进行最终的一个压缩,这个压缩都是无损的一些压缩方法。

    IoTDB 还支持这样一个加载外部 TsFile 的一个功能。比如说我们在资源比较受限的一些场景下,可能无法运行一个完整的 IoTDB,但它可以去调这个 TsFile 的一些直接写入 TSFile 的 API,直接把数据写成 TsFile。在这种场景下,IoTDB 把数据通过 TsFile 接口直接写成了这样的一个文件,通过 SQL 命令去加载到我们这个 IoTDB 集群当中,就是通过这个 load 的具体一个文件或者一个文件的文件夹的路径,做到这个功能。它可以实现自动去创建这个 TsFile 里面的一些元数据,也可以去自动去做这样的一个校验。还有这个功能也可以用于 IoTDB 数据库的升级,比如从 0 点几的这个版本升级到我们 1.0 之后,你可以做这样一个数据迁移的功能。

    最后一个部分我想给大家介绍一下 IoTDB 的存储引擎架构,叫 IoTLSM。这张图是我们这个 IoTDB 存储引擎的一个示意图,每当一个新的一个写入过来的时候,首先它会去写到这个写前日志 WAL 里面。然后我们会有一个顺乱序数据的一个判断机制,把这个写入来的数据分到顺序空间或乱序空间这两个部分,来进行处理。当然首先是写到内存的一个缓冲区里面等,然后等到内存数据刷到硬盘之后,也是区分这样一个顺序空间和乱序空间。在后台我们会有一个合并的这样一个机制,去慢慢的把这个数据合并到顺序空间去。

    我们这样的一个存储引擎架构可以做到三点好处。首先是这个顺序数据的一个最优化处理,如果一个写入里面来的数据被直接判断为是顺序数据的话,就直接写到数据空间内了。第二点好处是它的乱序写入性能会很稳定,就是它没有在写入的时候去做乱序数据的合并操作,它是在后台去做的,所以它的乱序写入性能会比较稳定。第三点就是它可以去降低合并放大的一个机制。

    下面就是给一个例子去简单解释分离存储的这样一个流程。假如说一个顺序空间内有这样的一些数据,就是它有一个序列叫 root.sg.d.s1,它的 time 是 1 和 3,value 也是 1 和 3。首先就是如果它的内存处这样一个缓存区达到了一定大小,需要持久化,持久化到磁盘之后它就会把那个文件日志给清除掉,然后会在内存里面留一个设备级别的一个索引文件,就是记录一下这个设备的开始时间和终止时间。在下面这个图上所表示的,它留了一个 root.sg.d,然后它的开始时间是 1,它的终止时间是 3。之后假如说我们来了这样两条写入请求,第一条它的 time 是 2,这个 time 就会因为是比这个之前那个索引文件里面的这个 end-time 要小,所以会判断为这是一个乱序数据,它就会直接写到乱序的一个数据空间里面。然后第二个写入它的时间戳是 4,它比这个之前的那个 end-time 要大,所以会判断它是一个顺序的文件,所以它就直接进到这个顺序空间了。

    在这个磁盘里面的每一个空间内,如果磁盘内的文件数达到了一定的阈值的话,我们会做这样一个空间内的合并。就比如说现在顺序空间内的磁盘里面有这样三个文件,它的开始时间和终止时间都没有重叠,那么它这时候会去做这样一个空间内合并,把这三个小文件合并成一个大的文件。这也是一个传统 LSM 的机制,它的作用就是用来减少查询的 IO。内存有限的时候,如果刷出来的数据块比较小的话,这个 IO 次数会变多,如果把这些数据块能够压缩到一个数据块的话,它对查询会更加友好。第二个优点是它可以提升索引速度,就是把文件合并成大文件的话,也就是同时会提升查询的速度。第三个优点就是优化文件系统,通过这样一个空间内的合并,可以减少我们磁盘上的文件的一个数量,降低这个文件系统的一个负载。

    除了空间内合并之外,我们还有一个跨空间的合并,就是把乱序空间内的磁盘文件,合并到顺序空间内来。就比如说有这样的两个文件,它的时间是相互重叠的,通过这样一个跨空间合并的话,就可以把第二个乱序文件合并到一个顺序空间内。这样做的好处就是提升了乱序数据查询的一个效率。关于这个跨空间合并,我们 IoTDB 也发表了一些论文,可以了解一下。

    关于内存数据的一个管理,我们是使用了一个内存的对象池来管理这样一个内存存储单元,叫 Array Pool。每次写入的时候向 Array Pool 去申请所需要的类型的一个具体的数组,然后等到内存缓冲区里面的数据刷盘之后,就可以把这个使用后的数组还给 Array Pool。这个好处就是用来减少内存对象的初始化,减少垃圾回收的这样一个过程。同时我们也在 Array Pool 里面支持这个数据类型的一些动态的调整。比如说过一段时间之后,写入的类型可能发生了变化,比如说之前它可能大量的去写一些 float 类型的数据,但是后面它 float 的数据类型可能没有再写入了,它可能更多的去写一些比如说 long 类型的一些数字、一些序列。然后 Array Pool 就会根据它写入的这样一个过程去调整这个 Array Pool 里面的各个类型数组的一个比例。

    我今天的分享大概就是这些内容,也是欢迎大家来关注一下我们这个 Apache IoTDB 的代码仓库,也欢迎大家给我们点个星星。非常感谢大家,谢谢。

最后修改时间:2024-04-09 09:34:05
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论