导读:首先你将经过这篇文章了解到 Apache Druid 底层的数据存储方式。其次将知道为何 Apache Druid 兼具数据仓库,全文检索和时间序列的特色。最后将学习到一种优雅的底层数据文件结构。javascript
今日格言:优秀的软件,从模仿开始的原创。java
了解过 Apache Druid 或以前看过本系列前期文章的同窗应该都知道 Druid 兼具数据仓库,全文检索和时间序列的能力。那么为何其能够具备这些能力,Druid 在实现这些能力时作了怎样的设计和努力?算法
Druid 的底层数据存储方式就是其能够实现这些能力的关键。本篇文章将为你详细讲解 Druid 底层文件 Segment 的组织方式。数组
带着问题阅读:数据结构
Druid 将数据存储在 segment 文件中,segment 文件按时间分区。在基本配置中,将为每个时间间隔建立一个 segment 文件,其中时间间隔能够经过granularitySpec
的segmentGranularity
参数配置。为了使 Druid 在繁重的查询负载下正常运行,segment 的文件大小应该在建议的 300mb-700mb 范围内。若是你的 segment 文件大于这个范围,那么能够考虑修改时间间隔粒度或是对数据分区,并调整partitionSpec
的targetPartitonSize
参数(这个参数的默认值是 500 万行)。ide
下面将描述 segment 文件的内部数据结构,该结构本质上是列式的,每一列数据都放置在单独的数据结构中。经过分别存储每一个列,Druid 能够经过仅扫描实际须要的那些列来减小查询延迟。oop
Druid 共有三种基本列类型:时间戳列,维度列和指标列,以下图所示:学习
timestamp
和metric
列很简单:在底层,它们都是由 LZ4 压缩的 interger 或 float 的数组。一旦查询知道须要选择的行,它就简单的解压缩这些行,取出相关的行,而后应用所需的聚合操做。与全部列同样,若是查询不须要某一列,则该列的数据会被跳过。ui
维度列
就有所不一样,由于它们支持过滤和分组操做,因此每一个维度都须要下列三种数据结构:编码
为何须要这三种数据结构?字典
仅将字符串映射成整数 id,以即可以紧凑的表示 2 和 3 中的值。3 中的
bitmap
也称为反向索引,容许快速过滤操做(特别是,位图便于快速进行 AND 和 OR 操做)。最后,group by和TopN须要 2 中的值列表
,换句话说,仅基于过滤器汇总的查询无需查询存储在其中的维度值列表
。
为了具体了解这些数据结构,考虑上面示例中的“page”列,下图说明了表示该维度的三个数据结构。
1: 编码列值的字典 { "Justin Bieber": 0, "Ke$ha": 1 } 2: 列数据 [0,0,1,1] 3: Bitmaps - 每一个列惟一值对应一个 value="Justin Bieber": [1,1,0,0] value="Ke$ha": [0,0,1,1]
注意bitmap
和前两种数据结构不一样:前两种在数据大小上呈线性增加(在最坏的状况下),而 bitmap 部分的大小则是数据大小和列基数的乘积。压缩将在这里为咱们提供帮助,由于咱们知道,对于“列数据”中的每一行,只有一个位图具备非零的条目。这意味着高基数列将具备极为稀疏的可压缩高度位图。Druid 使用特别适合位图的压缩算法来压缩 bitmap,如roaring bitmap compressing
(有兴趣的同窗能够深刻去了解一下)。
若是数据源使用多值列,则 segment 文件中的数据结构看起来会有所不一样。假设在上面的示例中,第二行同时标记了“ Ke \$ ha” 和 “ Justin Bieber”主题。在这种状况下,这三个数据结构如今看起来以下:
1: 编码列值的字段 { "Justin Bieber": 0, "Ke$ha": 1 } 2: 列数据 [0, [0,1], <--Row value of multi-value column can have array of values 1, 1] 3: Bitmaps - one for each unique value value="Justin Bieber": [1,1,0,0] value="Ke$ha": [0,1,1,1] ^ | | Multi-value column has multiple non-zero entries
注意列数据和Ke$ha
位图中第二行的更改,若是一行的一个列有多个值,则其在“列数据“中的输入是一组值。此外,在”列数据“中具备 n 个值的行在位图中将具备 n 个非零值条目。
segment 标识一般由数据源
,间隔开始时间
(ISO 8601 format),间隔结束时间
(ISO 8601 format)和版本号
构成。若是数据由于超出时间范围被分片,则 segment 标识符还将包含分区号
。以下:segment identifier=datasource_intervalStart_intervalEnd_version_partitionNum
在底层,一个 segment 由下面几个文件组成:
version.bin
4 个字节,以整数表示当前 segment 的版本。例如,对于 v9 segment,版本为 0x0, 0x0, 0x0, 0x9。
meta.smoosh
存储关于其余 smooth 文件的元数据(文件名和偏移量)。
XXXXX.smooth
这些文件中存储着一系列二进制数据。
这些smoosh
文件表明一块儿被“ smooshed”的多个文件,分红多个文件能够减小必须打开的文件描述符的数量。它们的大小最大 2GB(以匹配 Java 中内存映射的 ByteBuffer 的限制)。这些smoosh
文件包含数据中每一个列的单独文件,以及index.drd
带有有关该 segment 的额外元数据的文件。
还有一个特殊的列,称为__time
,是该 segment 的时间列。
在代码库中,segment 具备内部格式版本。当前的 segment 格式版本为v9
。
每列存储为两部分:
ColumnDescriptor 本质上是一个对象。它由一些有关该列的元数据组成(它是什么类型,它是不是多值的,等等),而后是能够反序列化其他二进制数的序列化/反序列化 list。
对于同一数据源,在相同的时间间隔内可能存在多个 segment。这些 segment 造成一个block
间隔。根据shardSpec
来配置分片数据,仅当block
完成时,Druid 查询才可能完成。也就是说,若是一个块由 3 个 segment 组成,例如:
sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_0 sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_1 sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_2
在对时间间隔的查询2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z
完成以前,必须装入全部 3 个 segment。
该规则的例外是使用线性分片规范。线性分片规范不会强制“完整性”,即便分片未加载到系统中,查询也能够完成。例如,若是你的实时摄取建立了 3 个使用线性分片规范进行分片的 segment,而且系统中仅加载了两个 segment,则查询将仅返回这 2 个 segment 的结果。
Druid 使用 datasource,interval,version 和 partition number 惟一地标识 segment。若是在一段时间内建立了多个 segment,则分区号仅在 segment ID 中可见。例如,若是你有一个一小时时间范围的 segment,可是一个小时内的数据量超过单个 segment 所能容纳的时间,则能够在同一小时内建立多个 segment。这些 segment 将共享相同的 datasource,interval 和 version,但 partition number 线性增长。
foo_2015-01-01/2015-01-02_v1_0 foo_2015-01-01/2015-01-02_v1_1 foo_2015-01-01/2015-01-02_v1_2
在上面的示例 segment 中,dataSource = foo,interval = 2015-01-01 / 2015-01-02,version = v1,partitionNum =0。若是在之后的某个时间点,你使用新的模式从新索引数据,新建立的 segment 将具备更高的版本 ID。
foo_2015-01-01/2015-01-02_v2_0 foo_2015-01-01/2015-01-02_v2_1 foo_2015-01-01/2015-01-02_v2_2
Druid 批量索引(基于 Hadoop 或基于 IndexTask 的索引)可确保每一个间隔的原子更新。在咱们的示例中,在将全部v2
segment2015-01-01/2015-01-02
都加载到 Druid 集群中以前,查询仅使用v1
segment。一旦v2
加载了全部 segment 并能够查询,全部查询将忽略v1
segment 并切换到这些v2
segment。以后不久,v1
segment 将被集群卸载。
请注意,跨越多个 segment 间隔的更新仅是每一个间隔内具备原子性。在整个更新过程当中,它们不是原子的。例如,当你具备如下 segment:
foo_2015-01-01/2015-01-02_v1_0 foo_2015-01-02/2015-01-03_v1_1 foo_2015-01-03/2015-01-04_v1_2
在v2
构建完并替换掉v1
segment 这段时间期内,v2
segment 将被加载进集群之中。所以在彻底加载v2
segment 以前,群集中可能同时存在v1
和v2
segment。
foo_2015-01-01/2015-01-02_v1_0 foo_2015-01-02/2015-01-03_v2_1 foo_2015-01-03/2015-01-04_v1_2
在这种状况下,查询可能会同时出现v1
和和v2
segment。
同一数据源的 segment 可能具备不一样的 schema。若是一个 segment 中存在一个字符串列(维),但另外一个 segment 中不存在,则涉及这两个 segment 的查询仍然有效。缺乏维的 segment 查询将表现得好像维只有空值。一样,若是一个 segment 包含一个数字列(指标),而另外一部分则没有,则对缺乏该指标的 segment 的查询一般会“作正确的事”。缺乏该指标的聚合的行为就好像该指标缺失。
roaring bitmap compressing
压缩算法。*请持续关注,后期将为你拓展更多知识。对 Druid 感兴趣的同窗也能够回顾我以前的系列文章。
关注公众号 MageByte,设置星标点「在看」是咱们创造好文的动力。后台回复 “加群” 进入技术交流群获更多技术成长。