Apache Druid将其索引(index)存储在段(Segments)文件中,该段文件按时间进行分区(partitioned)。在默认设置中,Druid为每个时间间隔(time interval)创建一个段文件,其中时间间隔可通过配置 granularitySpec.
segmentGranularity实现
。为了使Druid在繁重的查询负载下正常运行,将段文件的大小配置为建议的300MB-700MB范围内很重要。如果你的段文件超过该范围,然后再考虑要么改变时间间隔(time interval)的粒度或分区你的数据(partitioning your data),并调整了您的partitionsSpec.targetPartitionSize
(建议将参数调整为500万行)。有关更多信息,请参见下面的分片部分和“ 批量提取”文档的“分区规范”部分。
下面,我们描述了段文件的内部结构,该结构基本上是柱状的:每列的数据都布置在单独的数据结构中。通过分别存储每列,Druid可以通过仅扫描查询实际需要的那些列来减少查询延迟。共有三种基本列类型:时间戳列,维度列和指标列,如下图所示:
timestamp和metric列很简单:在幕后每个都是由LZ4算法压缩的整数或浮点值的数组。一旦查询知道需要选择的行,它就简单地解压缩这些行,取出相关的行,然后应用所需的聚合运算符。与所有列一样,如果查询不需要一列,则该列的数据将被跳过。
维度列有所不同,因为它们支持筛选和分组操作,因此每个维度都需要以下三个数据结构:
为什么要使用这三个数据结构?字典仅将字符串值映射为整数id,以便可以紧凑地表示(2)和(3)中的值。(3)中的位图-也称为倒排索引,允许快速过滤操作(特别是,位图便于快速应用AND和OR运算符)。最后,group by和TopN 查询需要(2)中的值列表。换句话说,仅基于过滤器汇总指标的查询不需要使用到存储在(2)中的维度值列表(注:因为不需要根据维度进行分组)。
为了具体了解这些数据结构,请考虑上面示例数据中的“ page”列。下图说明了表示该维度的三个数据结构。
1: Dictionary that encodes column values
{
"Justin Bieber": 0,
"Ke$ha": 1
}
2: Column data
[0,
0,
1,
1]
3: Bitmaps - one for each unique value of the column
value="Justin Bieber": [1,1,0,0]
value="Ke$ha": [0,0,1,1]
请注意,位图与前两个数据结构不同:前两个在数据大小上呈线性增长(在最坏的情况下),而位图部分的大小则是数据大小*列基数的乘积。压缩对于位图十分有用,因为我们知道对于“列数据”中的每一行,只有一个位图的条目为非零。这意味着高基数列将具有极为稀疏、高度可压缩位图。德鲁伊使用特别适合位图的压缩算法(例如咆哮的位图压缩)来利用这一点。
如果数据源使用多值列,则段文件中的数据结构看起来会有所不同。让我们想象一下,在上面的示例中,第二行同时标记了“ Ke $ ha” 和 “ Justin Bieber”主题。在这种情况下,这三个数据结构现在看起来如下:
1: Dictionary that encodes column values
{
"Justin Bieber": 0,
"Ke$ha": 1
}
2: Column data
[0,
[0,1], <--多值列的行值拥有数组形式的值
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个非零值条目。
默认情况下,Druid字符串维列的null值 将会被替换为'',而数值和度量列完全不可能出现null
,因为null会强制转换为0
。Druid还提供了与SQL兼容的null处理模式,但是需要在系统级别启用该模式,将druid.generic.useDefaultValueForNull
设置为false
,Druid在摄取时间创建的段,其字符串列可以区分''
和null
,并且数字列可以存储null而不是0。
字符串维度列在此模式下不包含任何其他列结构,而是仅保留该null
值的其他字典条目。但是,数值维度列将与附加位图一起存储在段中,该位图的设置位显示有null值的列。除了需要稍微增加段大小外,由于需要检查空位图,因此与SQL兼容的空处理也可能在查询时产生性能成本。该性能开销仅发生在实际上包含空值的列上。
段标识符通常使用段数据源、间隔开始时间(ISO 8601格式)、间隔结束时间(ISO 8601格式)和版本来构造。如果另外将数据分片超出时间范围,则段标识符还将包含分区号。
一个示例段标识符可以是:datasource_intervalStart_intervalEnd_version_partitionNum
在幕后,一个段由几个文件组成,下面列出。
version.bin
4个字节,以整数表示当前段版本。例如,对于v9段,版本为0x0、0x0、0x0、0x9
meta.smoosh
具有有关其他smoosh
文件内容的元数据(文件名和偏移量)的文件
XXXXX.smoosh
这些文件中有一些是串联的二进制数据
这些smoosh
文件代表在一起“ smooshed”的多个文件,以减少必须打开以容纳数据的文件描述符的数量。它们是最大2GB的文件(以匹配Java中内存映射的ByteBuffer的限制)。这些smoosh
文件包含数据中每个列的单独文件,以及index.drd
带有有关该段的额外元数据的文件。
还有一个称为__time的特殊列,它表示段的时间列。希望随着代码的发展,这种特殊性越来越小,但就目前而言,它就像我妈妈一直告诉我的那样特殊。
在代码库中,段具有内部格式版本。当前的句段格式版本为v9
。
每列存储为两部分:
ColumnDescriptor本质上是一个对象,它使我们能够使用Jackson的多态反序列化来添加新的有趣的序列化方法,而对代码的影响最小。它由一些有关该列的元数据组成(它是什么类型,它是多值的,等等),然后是可以反序列化二进制其余部分的序列化/反序列化逻辑列表。
对于同一数据源,在相同的时间间隔内可能存在多个段。这些段形成一个block
间隔。根据shardSpec
用于分片数据的类型,仅当a block
完成时,Druid查询才可能完成。也就是说,如果一个块由3个段组成,例如:
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个段。
该规则的例外是使用线性分片规范。线性分片规范不会强制“完整性”,即使分片未加载到系统中,查询也可以完成。例如,如果您的实时摄取创建了3个使用线性分片规范进行分片的段,并且系统中仅加载了两个段,则查询将仅返回这2个段的结果。
Druid使用数据源、时间间隔、版本和分区号唯一地标识段。如果在一段时间内创建了多个段,则分区号仅在段ID中可见。例如,如果您有按小时分的段,但一个小时内的数据量超过单个段所能容纳的数据,则可以在同一小时内创建多个段。这些段将共享相同的数据源,间隔和版本,但分区号线性增加。
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
,interval = 2015-01-01/2015-01-02
,version = v1
,和partitionNum = 0
。如果在以后的某个时间点,您使用新的架构重新索引数据,则新创建的段将具有更高的版本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
段2015-01-01/2015-01-02
都加载到Druid集群中之前,查询仅使用v1
段。一旦v2
加载了所有段并可以查询,所有查询将忽略v1
段并切换到这些v2
段。过段时间后,v1段将会从集群之中卸载
。
请注意,跨越多个段间隔的更新仅是每个间隔内的原子。在整个更新过程中,它们不是原子的。例如,您具有如下段:
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
在分段重叠的时间段内替换分段。在完全加载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
指标。
同一数据源的Druid段可能具有不同的架构。如果一个段中存在一个字符串列(维度),但另一个段中不存在,则涉及这两个段的查询仍然有效。缺少维的段查询将表现得好像维只有空值。同样,如果一个段中有一个数字列(指标),而另一部分则没有,则对缺少指标的段查询通常会“做正确的事”。此缺失指标上的聚合的行为就像该指标缺失一样。