Druid的segment

Druid把它的索引存储在segment的文件中,segment是以时间进行分区的。在基本的设置中,segment文件是根据一定的时间间隔创建的,通过granularitySpec中的 segmentGranularity参数来配置时间间隔。为了使Druid在繁重的查询压力下正常运行,设置segment文件的大小在300MB~700MB之间是非常重要的。如果我们的segment文件大于这个范围,需要考虑改变时间间隔的粒度或者对数据进行分区并在我们的partitionsSpec里面调整targetPartitionSize参数(这个参数的比较好的起始点是5百万行)。详细信息查看下面的分片部分和 Batch ingestion 的 分区说明部分
segment文件的核心数据结构
我们描述一下segment文件的内部结构,它本质上是列式存储:每一列的数据都在不同的数据结构中布局。通过分别存储每一列,Druid可以通过只扫描查询实际需要的列来减少查询延迟。有三种基本的列类型:时间戳列,维度列,指标列,例如:

时间戳和指标列非常简单:在幕后,每一个都是用LZ4压缩的整数或浮点值数组。一旦一个查询知道哪些行需要被查询,它只需要进行简单的解压,拉取相关的行,然后应用期望的聚合操作。如果查询不需要某一个列,则这个列的数据就会被跳过。
维度列不一样的原因是它们需要支持过滤和分组操作,因此每个维度都需要以下三种数据结构:
1、需要一个字典来映射值(这些值被当做字符串对待)到整形的ID
2、使用字典进行编码的列值的一个列表
3、对于列的不同的值,使用bitmap来表明哪一行包含这个值

为什么需要这三种数据结构呢?通过字典来把字符串映射到整形,从而能紧凑的表达2和3;3中的bitmap非常方面的应用于 and 和 or 操作。数据结构2中的列表的值用来做分组和topN查询的。换句话说,基于过滤器的聚合指标查询不需要涉及到数据结构2中存储的维度值。

我们使用上面例子中的page列来直观的感受一下这些数据结构,这个维度列的三种数据结构展示如下:
1、经过字典编码的列数据:

"Justin Bieber": 0, 
"Ke$ha": 1 
}

2、列数据:
[0,
 0, 
1, 
1]
3、bitmap:列的每个唯一值对应一行
value="Justin Bieber": [1,1,0,0] --第一行和第二行存在,第三行和第三行不存在这个值。
value="Ke$ha": [0,0,1,1]

需要注意的是,bitmap与前两个数据结构不同,前两个数据结构在数据大小上呈线性增长(在最坏的情况下),而bitmap的大小是数据的大小乘以列的基数。不过呢,我们可以采用压缩算法,我们知道对于每一行的列数据,只有一个非零项的bitmap。这意味着高基数列将具有非常稀疏的,高度可压缩的位图。Druid使用特别适合位图的压缩算法( roaring bitmap压缩)来利用这一点。

多值列
如果一个DataSource使用了多值列,那么segment 文件的数据结构看起来会有一点不同。让我们想象一下上面的例子,如果第二行同时拥有'Ke$ha' 和 'Justin Bieber' 值,在这种情况下,这三种数据结构看起来就像下面展示的:
1、经过字典编码的列数据:
{ "Justin Bieber": 0, "Ke$ha": 1 }

2、列数据:
[0,
 [0,1], <--多值列在行里面存储多个值的数组
1, 
1]

3、bitmap 每一个代表唯一值
value="Justin Bieber": [1,1,0,0] 
value="Ke$ha": [0,1,1,1] ——多值列有多个非零项

注意:主要的不一样的地方就是第二行的列数据 和Ke$ha的bitmap。如果行里面存在多余一个值的列,它将会以数组的形式保存在列数据里面,另外,行里面的列数据存在N个值将会有N个非零项在bitmap里面。

SQL兼容的null处理
默认情况下,druid的字符串维度列中''和null是可以互换使用的,数值列和指标列不能出现null,如果出现会强制转换成0.然而druid提供了SQL兼容的null处理模式,它必须通过配置项druid.generic.useDefaultValueForNull在系统级别进行开启。当它设置为false的时候,druid在进行摄入的时候创建的segment会对字符列的''和null进行区分,同样数字列可以使用null而不是0.
在这种模式下,字符串维度列不包含额外的列结构,而只是为null保留额外的字典条目。数值型列需要在segment添加额外的bitmap来表示存在null的行。这会稍微增加segment的大小。SQL兼容的null处理也会在查询的时候带来性能损耗,因为需要检查null的bitmap。当然这个性能损耗只会发生在列实际包含null的值的时候。

命名约定
segment的标识由segment的DataSource、间隔的开始时间(iso 8601格式)、间隔的结束时间(ISO 8601格式)和一个版本号组成。如果数据需要根据时间范围分片的话,segment的标识还会包含一个分片数字。
segment的标识类似这样:datasource_intervalStart_intervalEnd_version_partitionNum

segment的组件
在幕后,一个segment有好几个文件组成,清单如下:
1、version.bin
使用4字节的整数来表示当前segment的版本,比如v9 segment,版本就是0x0, 0x0, 0x0, 0x9
2、meta.smoosh
元数据文件,存储了其他smoosh文件的内容(文件名和偏移量)
3、xxxxx.smoosh
一系列连续的二进制文件
这些smoosh文件表示多个文件顺序的放在一起以最小化文件描述符(用来打开存储的数据)的数量。这些文件最大是2GB(匹配java的bytebuffer的内存限制)。对于每个列,smoosh文件都是使用单独的文件进行数据的存储,以及一个index.drd文件,其中包含有关该段的额外元数据。
还有一个称为“__time间”的特殊列,它引用段的时间列。随着代码的发展,这可能会变得越来越不特别,但现在它就像我妈妈一直告诉我的那样特别。
在代码库中,segment具有内部格式版本。当前段格式版本是v9。

列的格式
每个列都会存储两部分内容:
1、json的列描述
2、剩下的就是列的二进制数据
列描述本质上是一个对象,它允许我们使用json多样化的反序列化来最少化的代码影响去添加新的和有趣的序列化方法。它包含一些列的元数据(它的类型,是不是多值……)和可以反序列化剩余的二进制数据的序列化和反序列化的逻辑

创建segment的时候分片
分片
同一个DataSource在相同的时间间隔可能会存在多个segment。这些segment在时间间隔里形成了一个块。依据shardSpec的类型来进行数据的切分,只有当一个块完成时,Druid才能进行查询。这就是说,假如一个块包含三个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的数据,所有这三个segment都必须加载完成。

例外的是线性分片声明,线性分片声明不需要强制块必须完成,并且在分片没有加载到系统中时也能完成查询。例如:如果我们的实时摄入创建了三个线性分片声明的segment,并且只有两个segment加载到系统里面,查询的时候会返回这两个segment的数据。

模式修改
替换segment
druid使用DataSource,时间间隔,版本和分区号唯一标识segment,分区号只有在一个segment 标识中可见是当某些时间粒度的时候存在多个segment。例如:如果我们有一个按小时的segment,但是我们的数据太多导致一个segment没法存储,我们就可以在相同的时间创建多个segment。这些segment拥有相同的DataSource,时间间隔和版本,但是会线性的增加分区好。
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
在上面的示例中,DataSource= foo,时间间隔=2015-01-01/2015-01-02,version=v1,分区号=0.如果在后续的某一个时间点,我们使用新的模式从新索引了数据,则新创建的segment将会使用一个高的版本
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要么基于索引任务)保证基于时间间隔的原子性更新。在我们的例子中,除非有所有的2015-01-01/2015-01-02的v2 segment全部加载到druid的集群中,否则查询就使用v1的segment。一旦所有的v2segment加载完成并可查询,所有的查询都会忽略v1segment并切换到v2 segment。稍后v1segment就会从集群中卸载。

需要注意的是只有在时间间隔内的所有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
v2segment将在构建后立即加载到集群中,并在segment重叠的时间段内替换v1段。在完全加载v2段之前,集群可能混合了v1和v2段。
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会存在不同的模式
同一数据源的Druid段可能有不同的模式。如果一个字符串列(维度)存在于一个segment中而不是另一个segment中,则涉及这两个segment的查询仍然有效。对缺少维度的segment的查询将表现为该维度只有空值。类似地,如果一个段有一个数值列(metric),而另一个没有,那么查询缺少metric的段通常会看似在“做正确的事情”。在缺少的度量上的聚合的行为类似于该度量丢失。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章