Druid.IO原理之索引文件Segment详解

Druid按时间分区以后,将索引信息存储在segment文件里面。在基础的配置安装里,每个segment对应一个时间区间(时间区间定义参考granularitySpec中的 segmentGranularity参数:url链接)。为了在高负载的情况下提供良好的响应性能,强烈推荐segment的大小限制在推荐值区间(300mb-700mb),如果你的segment文件大小超过这个区间,可以考虑改变时间区间的粒度,或者对数据进行分片并微调PartitionSpec中的targetPartitionSize设置(建议在数据量超过500万行以后再开启这项设置)。更多分片信息可以参考Batch ingestion中的“Partitioning Specification”部分内容。

Segment文件核心数据结构

下面介绍一个segment文件的内部结构,其本质是一种列式存储:每列的数据被独立存储在segment结构的不同位置。通过独立存储每列数据,Druid在执行查询请求时可以只访问相关列的数据,因此可以大幅提高性能。Druid的列有三种基本类型:Timestamp类型,dimension类型和metric类型,如下图所示:

Druid column types

Timestamp列和Metric列比较简单,他们在实现中被存储为通过lz4压缩的整型或浮点数组。当一个查询需要访问某列数据时,只需要解压缩这些列,然后读取出这些列的数据,然后执行预定义的聚合操作。对于查询过程中不需要的列,Druid会直接跳过对这些列数据的访问。

Dimension列由于要实现filter和group-by操作,它的实现有些不同,dimension列包行下列三种数据结构:

  1. 一个字典:将值从字符串(所有的dimension列的数值都被当做字符串处理)映射到整数id。
  2. 一个值的列表(正排数据):把列中的数值通过字典编码以后,将数字id存储在列表里面。
  3. 一个bitmap倒排索引:对于列里面的每个不同的值,都对应存储为一个bitmap,用来表示哪一行数据包含该值。

为什么我们需要这三种数据结构呢?通过字典将字符串映射成数字id,数字id通常比字符串更小,因此可以被更紧凑的存储。第三点中的bitmap,通常被称为倒排索引,用于支持快速的过滤操作(bitmap对AND和OR操作的速度非常快)。最后,第二点中的值列表将用于支持group by和topN查询,而普通的查询只需要根据filter出来的行去来聚合相应的metirc就可以了,而不需要访问上述2中的值列表。

下面以上面表格中的page列数据为例,来演示一下这三种数据结构,如下所示:

1.编码了列值的字典:
{
    'Justin Bieber': 0,
    'Ke$ha':         1
}

2.列值数据(正排):
[0, 0, 1, 1](第一个和第二个0代表第一,二行数据编码,即上述字典中的'Justin BieBer',第三个和第四个代表第三四行数据,即上述字典中的'Ke$ha')

3. Bitmaps:字典中的每个值对应一个bitmap
value='Justin Bieber': [1, 1, 0, 0]
value='Ke$ha': [0, 0, 1, 1]

注意bitmap跟其他两种数据结构不同,其他两种数据结构都是跟随数据量的增长而线性增长的(最差情况下),而bitmap的大小=数据的总行数*列中值的种类数(也就是字典的size)。这就意味着如果值得的种类很多,那么bitmap中为1的数量将会非常稀疏,这种情况下bitmap有可能被大幅压缩。Druid针对这种情况,采用了特殊的压缩算法,比如roaring bitmap压缩算法。

多值(multi-value)列

如果datasource使用了多值列,那么segment里面的数据结构就稍微有些不同。让我们对上面例子中的数据稍微做一下修改,来演示多之列的情况。假设上述表格中的第二行的page列,同时被标注为Ke$ha和Justin Bieber,这种情况下,上面的三种数据结构如下图所示:

1.编码了列值的字典:
{
    'Justin Bieber': 0,
    'Ke$ha':         1
}

2.列值数据(正排):
[0, 
[0,1], <-------(多个值)
1, 
1](第一个和第二个0代表第一,二行数据编码,即上述字典中的'Justin BieBer',第三个和第四个代表第三四行数据,即上述字典中的'Ke$ha')

3. Bitmaps:字典中的每个值对应一个bitmap
value='Justin Bieber': [1, 1, 0, 0]
value='Ke$ha':         [0, 1, 1, 1]
                           ^
                           |
                           |
            多值列在多个value中有多个不为0的标识

 请注意这个数据跟上面数据在列值数据的第二行和bitmap的第二位与原来数据的区别。如果某一行在特定列上有多个值,那么在列值表里面,该位置对应的是一个数组;在bitmap的对应行位置会有多个非0值。

命名规范

Segment的标识通常由datasource的名字,时间的起始和结束时间(ISO8601格式),以及一个版本号组成。如果数据在时间区间内还被分片了,那么名字也对应包含一个分片号。

一个segment的名字示例如下:datasource_intervalStart_intervalEnd_version_partitionNum

Segment组件

Segment通常由多个文件组成,如下所示:

  • version.bin: 一个4byte的整数值作为版本号。比如对于v9版本的segment,版本号是:0x0,0x0,0x0,0x9
  • meta.smoosh: 一个包含其他smoosh文件元信息(比如文件名和偏移量)的文件。
  • XXXXX.smoosh: 多个这种文件,它们有二进制数据拼接而成。数据文件有多个smoosh拼接而成,这些文件最大的size可以达到2GB(java中内存映射到ByteBuffer的最大大小)。每一列包含多个独立的文件,这些文件被包装在smoosh文件中。另一保存有额外meta数据的index.drd文件也被包装在smoosh中。

__time列时druid中的一种特殊列,用来表示segment的时间列。从代码的角度来看,这一列应该跟其他列没有本质区别,但是目前为止,druid里还是有一些_time列的特殊逻辑。

在代码中,segment有一个内部的版本号,目前的版本号是v9

列的格式

每一列被保存为两部分:

  1. 被序列化为json格式的ColumnDescriptor
  2. 列的其他数据被保存为二进制格式。

ColumnDescriptor是必须的,它被序列化为json格式,因此可以很方便的在不影响老代码的情况下,在里面添加新功能的meta信息。它包含了列所必要的一些元信息。

Segment的分区(Sharding)

对于有些datasource来说,同一时间区间之内可能存在多个segment。这些segment构成了该时间区间的一个block。根据shardspec配置有两种类型,其中之一要求druid只能在block里面的segment都ready的情况下才能提供查询。也就是说,如果有下面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

所有的这三个segment必须都被加载以后才能提供查询。

但是当使用线性分片配置时,druid并不要求所有的segment都ready时才能提供查询。比如,在使用线性分片配置时,当你的实时数据导入模块创建了3个segment,如果只有两个segment被加载,druid将只返回在这两个segment上的查询结果。

本文翻译自:http://druid.io/docs/latest/design/

更多内容请访问:Druid.IO介绍系列之汇总篇

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值