Apache IoTDB [1] 从 2018 年 11 月捐赠给 Apache 基金会,成为孵化项目,到 2020 年 9 月毕业,期间经历了 0.8-0.11 4 个大的版本,但都是单机版本,社区对 Apache IoTDB 的分布式版本呼声也越来越高。2021 年 4 月,Apache IoTDB 0.12 版本的发布,带来了一个期盼已久的好消息:Apache IoTDB 0.12 开始支持分布式。今天我们就来看一下 Apache IoTDB 分布式中的数据分布。
分布式架构
IoTDB 采用 Share-Nothing 的分布式架构 (这里指的是数据存本地磁盘的情况下,IoTDB 也支持数据存储到 HDFS 上面,这种情况本文暂且不表),各个节点都是同质的,每个节点主要模块如下图所示:

单机有如下几大模块:分别为 Physical Plan Generator、SQL Parser、Single Read/Write Engine 以及存储文件 TsFile。分布式较单机多了如下几大模块:Data Partition、Distributed Query、Distributed Write 以及各个节点之间同步协议 Raft Synchronization 模块。
IoTDB 集群搭建之后,会根据节点的 ip 和 port 和当前启动时间生成一个 hash 值,所有节点按照此 hash 值排序形成一个环形,hash 值最大的节点的后面的节点就是 hash 值最小的节点,集群会按照配置的副本数 N,从 hash 值最小的节点开始,依次选择 N-1 个节点组成一个 raft 组,形成一个 data raft group。所有的 meta 节点组成一个 meta raft group。

以 4 节点 3 副本为例,其会形成如下 raft 复制组,每个节点上面都会有 N+1 个复制组,N 是副本数,即 N 个 data raft group,1 指的是所有节点形成一个 meta raft group。

分布式数据存储
当初始化好了 raft 复制组之后,面临的问题就是数据归属问题,哪些 raft 复制组处理哪些数据呢?这就需要数据分区这个模块来解决了。
数据分区模块负责元数据、数据分布的计算工作,为了便于表述,本文在此讲述下 IoTDB 的一些基础概念。
IoTDB 基础概念
IoTDB 模型是树状结构。如下所示,有存储组、设备、测点等概念。存储组可以理解为传统数据库中的表,在存储的时候,不同存储组的数据是存储在不同的文件夹中的。下图中有root.sgcc、root.ln两个存储组。叶子节点叫做测点,叶子节点的父节点叫做设备。从父节点 root 到叶子节点的全路径叫做时序。比如下图中有root.sgcc.wf01.status等 4 条时序。关于这些概念的详细描述,请参考 IoTDB 官方网站 [1]。

元数据分布
对于分布式 IoTDB 来说,有两种类型的元数据信息,一种是存储组、另外一种是时序。为什么说两种类型的元数据呢?因为这两种元数据是不同的复制组管理的。存储组是 meta raft group 管理的。而时序信息是由 data raft group 管理的。这样做的好处也是由于存储组元数据比较少,可以在各个节点保存;但是时序信息有可能比较大,有可能高达千万级别,如果每个节点都保存有相同的时序信息,每个节点会浪费太多的内存空间 (这里也是内存和时间的 trade off,当然每个节点都有时序的元数据信息是最好的,因为写入、查询会用到这些时序元数据信息。这里将在后面的文章中进行分析)。所以会把这些时序数据信息分散到各个节点进行管理。
存储组分布
对于存储组这样的元数据,由于是 meta raft group 管理的。所以在每个节点都会保存。
时序分布
对于时序和数据的存储,是分布到多个节点进行分散存储的。分布的核心是一个数据分区算法,即如何判定我的 (元) 数据应该存储在哪台机器上?
IoTDB 系统中预先设置了 10000 个 slot,然后会均匀的把这些 slot 分到集群中各个 IoTDB 实例中,假设节点数是 M,则集群环中的前 M-1 个节点每个节点分配的 slot 数是10000/M,最后一个节点是 slot 数就是10000-10000/M*(M-1),可能较前 M-1 个节点会多一些。
比如,在 6 个节点组成的集群中,第一个节点会分配 0-1665 共 1666 个 slot;第二个节点会分配 1666-3331 共 1666 个 slot;第三个节点会分配 3332-4997 共 1666 个 slot;第四个节点会分配 4998-6663 共 1666 个 slot;第五个节点会分配 6664-8329 共 1666 个 slot;而第六个节点就会分配 8330-9999 共 1670 个 slot

得到了节点和 slot id 的关系,剩下的就是如何把数据映射到 slot 中。
IoTDB 采用了如下 hash 算法:
•
slot_id = hash(storage_group, time_partition) % total_slot_num
storage_group 即存储组的名字,time_partition 会根据元数据操作还是数据操作,执行不同的策略。
在计算时序的分布的时候,time_partition 永远等于 0,即可以理解为同一个存储组的所有时序都是保存在同一个 data raft 组中的。
数据分布
数据分布与时序元数据分布的计算公式都是一致的,区别在于其 time_partition 不是 0,而是计算出来的。
•
•
time_partition = time_stamp % partition_interval
如上所示,time_stamp 是数据插入的时间戳,而 partition_interval 是时间分区,比如一周、一个月等。由于同一个时间分区的数据是保存在一起的,对于查询和过期数据删除都有优势。
如下所示,每个 data raft group 会有许多的 slot,每个 slot 上面会有多个 time partition,每个 partition 会有多个 TsFile,一个 TsFile 会有属于这个时间分区的多条时序的具体数据,关于 TsFile 的详细描述,请参考官网 TsFile 介绍 [2]。
