2022 IoTDB Summit:IoTDB PMC 曹高飞《Apache IoTDB 秒级扩容能力与存算分离实践》
12 月 3 日、4 日,2022 Apache IoTDB 物联网生态大会在线上圆满落幕。大会上发布 Apache IoTDB 的分布式 1.0 版本,并分享 Apache IoTDB 实现的数据管理技术与物联网场景实践案例,深入探讨了 Apache IoTDB 与物联网企业如何共建活跃生态,企业如何与开源社区紧密配合,实现共赢。
我们邀请到天谋科技高级开发工程师,Apache IoTDB PMC 曹高飞参加此次大会,并做主题演讲——《Apache IoTDB 秒级扩容能力与存算分离实践》。以下为内容全文。
大家好,我叫曹高飞,现在就职于天谋科技,是 Apache IoTDB 的 PMC。今天我带来的分享主题是 Apache IoTDB 秒级扩容能力与存算分离的一个实践。
我的介绍主要分为三个部分。第一个部分是背景介绍,主要介绍 IoTDB 研发分布式的需求与目标。第二个部分是集群架构,主要从 ConfigNode、DataNode、分区路由三个部分来展开介绍。最后一个部分,介绍 IoTDB 的秒级扩、缩容,以及存储计算分离的一个原理。
01 背景介绍
首先介绍第一个部分,IoTDB 分布式之路的一个研发需求与目标。
在物联网时代,传感器的大规模使用带来了海量的时序数据。时序数据的全称叫做时间序列数据,是指按时间顺序记录的数据列。时序数据其实和我们的生活息息相关,比如说股票的涨跌曲线、医院的心电图、这一台电脑的 CPU 的利用率曲线等等,当然要在其中写入频率最高、存储规模最大的,还要数工业物联网的一个数据。
我这里举一个实际的例子:风力发电的场景。在风机上有很多的传感器,它们都在以实时的频率采集诸如发电量、风速等一些指标。据我们统计,一台风机在一秒钟就可以产生约 200KB 的一个数据,这样子的话,一个风力发电厂可能会有两万台的风机,这样子一秒就可以产生约 4.5GB 的数据。可以看到,工业物联网对时序数据存储的规模的要求是非常大的,所以这就对系统的分布式的能力提出了一个非常高的要求。
总结来说,分布式的目标以及要求可以分为如下的三点。第一点是大容量,也就是需要管理上亿规模的一个设备以及测点,并且能够充分的利用集群资源,进行高效的写入以及查询分析。
第二点是高可用,即使我们这个集群当中部分的节点失效之后,整个系统依然能够提供服务,并且失效节点可以被方便的进行一下替换。
最后一点是易扩展,因为带着一个工业物联网的场景当中,设备是可能不断在增多的,负载也会不断的增加。这个时候就需要对集群进行一个扩容,用于分担负载以及压力,并且整个系统最好还是可以提供支持存储计算分离,这样就可以按需的进行一下扩展。
其中,针对刚刚讲到的扩展性的需求,我们又可以具体的分为如下几点。第一点是,物联网场景中设备的更迭是非常频繁的,经常需要上线新的一批设备,所以我们就需要经常的进行一个扩容。分布式系统的扩容应该尽可能做到简单。
第二点是,扩容的耗时将直接影响系统的升级时长,所以我们需要系统尽可能的支持一个秒级扩容。
第三点是,整个扩容期间,整个系统也是需要能够提供线上服务的,所以我们需要系统尽可能的支持一下平滑的进行扩容。
02 集群架构
刚刚介绍了物联网场景对于分布式系统的一些目标以及需求,这一章会详细介绍一下 IoTDB 当中是如何围绕这几个目标,进行设计的。在 IoTDB 的集群当中有两种节点类型,第一种是 ConfigNode,Config 顾名思义,是配置节点,类似于集群大脑的一个角色。DataNode 是数据节点,存储了时间序列真实的元数据以及数据。下面我会详细展开介绍一下这两种角色。
首先来看一下 ConfigNode。ConfigNode 是集群的配置节点,在 IoTDB 当中可以配置多个 ConfigNode。不同的 ConfigNode 之间通过强一致性的共识协议进行一个通信,然后保证数据的强一致性。
ConfigNode 管理的全局信息有如下几类。第一类是集群的一个配置信息,比如说集群部署的各项参数,以及节点之间采用的共识协议等等。
第二类是节点信息,在 ConfigNode 会保存当前集群注册的全部的节点,并且会有一个心跳机制,来判断节点的存活的状态,存活状态也是保存在 ConfigNode 当中的。
第三类是一个分区的信息,分区包含元数据分区以及数据分区。分区是 IoTDB 的分布式实现高扩展性的一个关键,通过查找分区,我们就可以确定某一条时间序列具体到底由于哪一个计算节点。后面的章节会详细的介绍 IoTDB 是如何实现分区的。
第四点,ConfigNode 还管理了一些权限的信息,通过权限信息我们可以查询用户对哪一些节点、对哪一些时间序列具有操作权限。
除了管理以上的四条信息以外,ConfigNode 还会负责集群任务的调度,以及集群的一些负载均衡的操作。通过负载均衡,我们可以判断通过节点的 CPU 的使用率、磁盘的使用率等等一些指标,使集群达到一个最可能负载均衡的一个状态。
对于刚刚讲到的任务调度、分区创建等一些操作,ConfigNode 还会使用分布式事务进行一个实现。我们通过引入两阶段提交、支持回滚操作等,实现了事务的原子性。通过引入共识层,实现了一致性。通过事务框架,实现了任务之间的串行隔离,保证了事务的隔离性。通过引入共识层,以及 WAL 的持久化,我们保证了事务的一个持久性。
IoTDB 的节点类型有 ConfigNode 和 DataNode,刚刚介绍了 ConfigNode 的一个作用,这页 PPT 主要会介绍一下 DataNode 的作用。DataNode 存储了真实的时间序列的元数据与时间序列的数据,为了更好的管理 DataNode 上存储的数据,我们引入了 Region 的概念。在 DataNode 上有两种 Region,一种叫做 SchemaRegion,一种叫做 DataRegion,分别对应了时间序列的元数据以及数据。并且,为了保证集群的高可用性,同一个 Region 会在多个不同的 DataNode 上进行一个存储,然后多个 Region 构成了一个 RegionGroup。RegionGroup 可以根据不同的一致性要求,设置不同的共识协议。
在右图的这个示例当中,我们可以看到,集群当中一共有 6 个 DataNode,1 个 SchemaRegionGroup,以及 3 个 DataRegionGroup。虚线标出来的是 SchemaRegionGroup,它的标号是 0 号,它位于三个不同的 DataNode 上。同样的道理,DataRegionGroup 有 3 个,有 1 号、 2 号、以及 3 号,它们分别以不同的颜色进行了标识,它们也分别位于三个不同的 DataNode 上。通过引入 Region 的一个概念,我们就可以对 DataNode 上面的数据进行细粒度的管理与控制,并且能够加大一个并行度。
讲到这里,大家可能会问,时间序列的元数据为何需要分区、分 Region 进行一个管理呢?把它直接存储于 ConfigNode 或者存储于某一个 DataNode 不行吗?这里就需要介绍一下物联网时序数据的一个特点。
第一个是,在物联网的时序数据的场景下,我们对元数据的查询是非常的重要的。我们经常会进行一些聚合的操作,比如说查询整个集群当中所有的设备的数量,或者说查询某一个设备下面所有的测点的数量。
第二点是,物联网场景的负载和关系型数据库是不太一样的。在关系型数据库当中,我们很少会查询关系型数据库的表头的信息,但是在物联网场景下,因为它有上亿、上千万的一个传感器以及测点的一个数量,我们经常会对这一些设备以及测点进行查询。
基于以上几个原因,IoTDB 当中的 ConfigNode 就只存储了元数据的分区的路由表。路由表占用的资源是很少的,我们只在内存当中进行一个存储就足够了,不需要把它进行一个持久化。然后,我们把真正的元数据存储到 DataNode 上,这样子的话,当查询以及写入的时候,需要拉取元数据的时候,我们就可以充分利用 DataNode 节点的一个计算资源进行一个并行的处理,并且还可以利用 DataNode 的节点的内存进行一个缓存。
讲完了 IoTDB 进行元数据分区与数据分区的原因,接下来我们看一下在 IoTDB 的集群当中,我们如何来创建分区。
创建分区的第一步,就是通过设备 ID 来进行哈希。比如说右图当中的这个例子,我们举一个真实的例子,比如说我们要对风机 1 号的设备 ID 进行数据写入,首先我们会把风机 1 号的元数据哈希到元数据分区 1 号。这样子的话,当我们开始写入真正的测点的数据的时候,我们就可以先拉取元数据分区 1 号上的时间序列的元数据。接下来定位到元数据的位置之后,我们就可以写入真实的测点数据。当我们写入第一个时间戳的时候,这个测点的数据就会位于数据分区 1-1。在 IoTDB 当中,数据分区的粒度默认是以 7 天为粒度的,如果我们写入的时间戳的跨度大于一周的话,假如说对于风机一号,它的数据会自动位于数据分区 1-2。
随着时间的不断增长,数据也会越来越多,这样子的话数据分区也会对应的越来越多,并且为了保证高可用性,IoTDB 当中的每一个分区都会存储于多个不同的 DataNode 上。在前面的章节我们介绍过,在 DataNode 当中是以 Region 的粒度来管理数据的,对应于这张图,元数据分区的话在 DataNode 上就对应了 SchemaRegion。在这里,我们配置的元数据分区的副本数,以及数据分区的副本数都是 3,所以我们就可以看到元数据分区 1-1 它存在于 3 个 SchemaRegion,这 3 个 SchemaRegion 分别对应于 3 个不同的 DataNode。
类似的道理,数据分区也是存在于三个 DataRegion,这三个 DataRegion 也是分别位于三个不同的 DataNode。通过这样设计的一个机制,我们就保证了一个高可靠性,并且通过创建分区,我们就可以实现分区级别的一个并行的写入以及并行的查询,能够最大程度上的提升集群的性能。
刚刚讲完了元数据分区通过 SchemaRegion、数据分区通过 DataRegion 来保证高可用性,但实际上我们对于 Region 的配置是非常灵活的,我们可以根据需要,选择不同的一致性的协议与副本数。对于 ConfigNode,它的一致性要求是非常高的,所以我们一般是采用强一致性的协议,在复制的时候要采用全量复制进行复制。对于元数据分区的 SchemaRegion,它对一致性的要求也比较高,一般会配置强一致性的一个协议。而对于数据 DataRegion,IoTDB 提供了强一致性与弱一致性两种不同的协议,用户可以在平衡性能与一致性的需求之后,选择最适合自己的一个协议。
03 秒级扩容、存算分离
讲完了 IoTDB 的集群架构,接下来我们讲一下,基于这个集群架构,IoTDB 的分布式如何实现秒级的扩容以及存储计算的一个分离。
通过刚刚的介绍,我们知道 IoTDB 通过元数据分区与数据分区,来实现集群的一个高扩展性,但是随着时间序列的一个不断的注册,元数据的分区肯定会不断的进行一个增长。同样的,元数据分区的数量不断增长的时候,它对应的 SchemaRegion 的数量也会不断的进行增长。并且,当序列不断的注册,并且写入的数据不断增多的时候,数据分区也会不断的增长。类似的道理,数据分区对应的 DataRegion 的数量也会不断的增长。
所以这个时候,就需要我们在集群层面进行一个扩容。ConfigNode 与 DataNode 都是可以进行一个扩容的,ConfigNode 对于一致性的要求比较高,所以我们在增加 ConfigNode 的副本的时候,我们会全量的迁移数据。但是增加 DataNode 的时候,我们就不会迁移已有 DataNode 上的数据。
所以,为什么增加 DataNode 的时候不迁移数据呢?之后的章节我会详细的展开其中的原因。这里我们首先可以与迁移数据的 KV 系统进行一下比较。
第一方面是从负载的角度考虑,这里我们对比了一个 KV 系统是比较典型的 Cassandra。对于物联网系统,一般是最新的数据分区,它写入的负载是最大的,但是对于 KV 系统,比如说 Cassandra,它的设计的初衷是,它所有 key 的一个写入的机率都是比较类似的。
更详细的我们可以以这张图进行一下对比。上图是物联网的一个系统,下图是 KV 的一个系统。物联网场景下的时序系统,它对历史数据的查询频率是比较低的,它大部分的写入以及查询都会集中在最新的时间分区。我的图中也进行了一个标识,越靠近右侧的时间分区越新,它对应的时间戳也是越新的,它上面的查询以及写入的负载都是更大的。
而对于下图,它是一个 KV 系统,它每个 key 的写入以及查询负载都是比较相同的。所以总结来说,从读写负载的角度,IoTDB 的集群在新增节点之后,是不需要主动进行数据迁移的。通过 IoTDB 里提供的自适应性的一些分区创建的算法,当我们新创建一些时间分区的时候,它自动就能够分配到最新的节点上,从而该节点也会不断的、越来越多的进行一个写入以及查询的负载。
增加节点不进行迁移数据的第二个原因,是从节点能力进行考虑的。假如说,我们增加节点,并且迁移原有的数据的话,很大程度上会影响原有的负载,迁移数据的耗时也直接影响系统的可用性。并且,在 IoTDB 的集群设计当中,少量的节点其实就可以支撑我们比较高频的写入。所以从节点能力的角度来考虑,扩容后大量迁移数据是一个没有充分利用物联网时序特性的一个设计。
总结来说,与扩容迁移数据的 KV 系统做一下对比,这里采用了一致性哈希的 Cassandra 为代表。Cassandra 在扩容以及增加节点的时候,它需要迁移大量的数据,整个迁移过程在集群规模比较庞大的时候,可能的时间成本较高。所以,IoTDB 在分区方式上就采用了查找表的一个实现,它充分的利用了物联网时序系统的特点,在不迁移历史数据的情况下,就能够实现秒级扩容、平滑扩容,并且提供了极高的写入以及查询的吞吐。
IoTDB 除了支持扩容节点,同样也支持移除节点。移除节点适用的场景是当某些节点,因为硬件故障等原因导致不可用的时候,我们可以将其移除。ConfigNode 与 DataNode 都支持移除,在移除的过程当中,系统会自动将移除节点上的数据进行一个迁移。并且,在迁移的过程当中,会自动的进行一个负载均衡,也就是迁移 Region 的过程当中,我们会自动为该 Region 选择一个负载最小的节点进行一个迁移。
这张图是一个移除节点的事例。我们要移除的节点是节点 B,节点 B 上一共有两个 Region,分别是 1-1 与 2-2。DataRegion1-1 属于 DataRegionGroup1,DataRegionGroup1 位于三个节点,分别是 B、C、D,所以我们进行迁移的时候,我们只能把这一个 Region 迁移到没有这个 RegionGroup 的 Node-A 节点上。类似的道理,我们只能将数据 Region2-2 迁移到 Node-C 上。过移除之后它的一个效果就如下图所示。节点被移除之后,它上面的 Region 被均匀迁移到其他的 DataNode 上,这就保证了一定的负载均衡。
这一页 PPT 会更详细的介绍一下集群的负载均衡是怎么实现的。在整个集群扩容、缩容、以及迁移数据的过程当中,我们都会定期的运行负载均衡的一个算法。这里我们实现了一个最小费用、最大流的一个算法,通过右图所示的建模。这其中 S 表示网络源点,大 R 表示 RegionGroup 的一个点集,大 D 表示 DataNode 的一个点集,T 表示流量网络的一个终点。
并且我们会综合考虑 CPU、内存以及磁盘的一些负载,针对不同的一个边,我们这里创新性的引入了容量边、决策边、负载边这三种边,我们会为不同的边赋予一个不同的容量以及代价。经过运行这一个费用流算法之后,我们最终就能够保证整个 RegionGroup 的分配,比如说 Leader 的分配,它处于一个最优的状态。
最后再来介绍一下 IoTDB 的集群对于存储计算分离,以及存储均衡的一些设计与思考。在当前的互联网的架构当中,提及比较多的存算分离的架构,一般是在云原生的基础之上,以这一个共享存储架构最为常见。这种存算分离、共享存储的架构可以比较好的发挥弹性的优势,在计算资源不够的时候,我们可以只增加计算节点,在存储空间不够的时候,可以只扩容存储空间。共享存储固然有它的优势,但是存算分离是否一定要依赖共享存储呢?我认为不是的。
可以看一下,这一页 PPT 就是 IoTDB 对于物联网时序系统的一个存算分离的设计。刚刚我们也讲过,在默认情况下,IoTDB 的 DataNode 是同时承担计算与存储两种角色的,但是实际上我们也可以将 DataNode 配置成只进行查询与计算的节点。
并且,IoTDB 在扩容 DataNode 的时候,我们默认是不进行数据迁移的,所以新增的节点就可以通过一定的配置,来作为一个只计算的一个节点,这样的话就实现了计算的一个分离。但同时,我们也可以通过一定的配置,将新增的节点设置为只存储的节点,通过主动迁移数据,或者是 IoTDB 提供的一些数据 Region 分裂的一些算法,实现了存储容量的提升,通过这样的一个手段就实现了存储的一个分离。
总结来说,IoTDB 集群的存储与计算分离的架构,是围绕着弹性来进行设计的。在增加节点的时候,可以通过如下的三种选择实现存储与计算分离的灵活配置:
增加节点的时候,不迁移历史数据,仅做计算,如此就完成了计算能力的一个扩容。
增加节点的时候,不迁移历史数据,但是我们可以将原有节点上的数据 Region 分裂到一个新的节点,这就实现了充分利用物联网时序系统的节点的特性,进行了存储的扩容。
第三条是,增加节点并且迁移历史数据,分配新数据,然后这就实现了最彻底的存储的扩容。
当然, IoTDB 的存算分离等功能也会在云原生的大背景下不断的进行一个迭代,我们也希望与社区的伙伴们进行更多的一个交流,一同推动 IoTDB 集群向更稳定、更可靠、更易扩展的方向进行一个发展。
我的展示到此结束,感谢大家的观看,也欢迎大家在 IoTDB 用户群以及直播的频道进行交流,谢谢大家。
评论