1 关于Druid的段
ApacheDruid将会将索引存储在按时间分区的段文件中。
在基本设置中,系统通常会为每个时间间隔创建一个段文件,其中时间间隔可在granularitySpec
的segmentGranularity
参数中配置。
为了能够让Druid在复杂的查询负载下良好运行,段文件的大小必须在系统建议的300MB-700MB范围以内。如果段文件超过此范围,那么需要考虑重新定义时间间隔的颗粒度,或者对数据进行分区,并在partitionsSpec
中调整 targetPartitionSize
(此参数的建议起点是500万行)。
有关更多信息,请参阅下面的分片部分和批处理摄取文档的分区规范部分。
2 段文件的核心数据结构
段文件的内部结构是列式的(如下图所示),每列的数据会单独地存放在指定的数据结构中。这样每一列存储的方式能够让Druid只扫描到所需要的指定数据,从而减少查询延迟。
段文件的数据结构有三种基本的列类型,分别是时间戳列(Timestamp)、维度列(Dimensions)和指标列(Metrics):
对于timestamp和metric列而言,每个数据列都是用LZ4压缩的整数或浮点值数组。一旦查询请求指定了所需的行,那么系统只需解压缩并提取这些行,然后使用所需的聚合运算符进行计算就可以了。
对于dimension列而言,它支持过滤和聚合操作,所以每一个维度都需要以下的三种数据结构:
1)一个将值(通常被当做字符串)映射到整数id的字典
2)一个使用上述字典进行编码的列值列表
3)对于列中每一个不同的值,标识哪些行包含该值的位图
为什么需要这三种数据结构?
字典简单地将字符串值映射到整数id,以便于在 2)和 3)中进行表示。
3)中的位图(也称倒排索引)则可以帮助系统进行快速过滤操作(尤其是and和or操作)。
最后,GroupBy
和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]
注意,位图与前两个的数据结构并不相同:
前两个数据结构在数据大小上呈线性增长,而位图大小是数据大小 * 列基数的积。
但是,此处可以利用压缩操作。因为我们知道对于“列数据”的每一行中只有一个具有非零项的位图,这意味着高基数列将具有非常稀疏的、高度可压缩的位图。Druid可以使用压缩算法来适配这一数据结构(如Roaring位图压缩)
多值列
如果数据源使用多值列,那么段文件中的数据结构看起来则会有点不同。
假设在上述例子中,第二行同时标记了“Ke$ha”和“Justin Bieber”主题,那么在这种情况下,这三种数据结构看起来就会如下所示:
1: Dictionary that encodes column values
{
"Justin Bieber": 0,
"Ke$ha": 1
}
2: Column data
[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个非零值项。
3 SQL兼容的空值处理
默认情况下,Druid字符串的维度列(Dimensions)可以使用" "
或者null
,而数值列和指标列则需要将null
标记为0
。但是,Druid还提供了一个与SQL相兼容的空值处理方式,需先在系统中启用Druid.generic.useDefaultValueForNull
,当设置为false
时,则该设置将会许Druid在创建的数据段中:字符串列区分""
和null
,数值列区分null
和0
。
在这种模式下,字符串维度列不包含额外的列结构,只是为null
保留了额外的字典条目。
但是,数值列将与一个附加位图一起存储在段中,该位图标识了哪些行是null
值。
除了略微增加段大小之外,由于需要检查null
的位图,SQL兼容的空值处理在查询时也会导致性能损失,此性能开销仅对实际包含null
列的情况下出现。
4 命名规则
段标识符通常由数据源、时间区间的开始时间(ISO 8601格式)、时间区间的结束时间(ISO 8601格式)、以及版本来组成。
如果数据被额外的分片后超过了时间范围,则段标识符还将包含分区号。
比如,一个段标识符可以是:数据源名称_开始时间_结束时间_版本号_分区号
5 段的组成
一个段通常会由以下几个文件组成:
4个字节,以整数表示当前段版本。 例如,对于v9段,版本为0x0、0x0、0x0、0x9
一个包含其他smoosh 文件内容的元数据(文件名以及偏移量)文件
这些文件中有一些是串联的二进制数据
smoosh文件代表 “smooshed” 在一起的多个文件,以减少必须打开用来容纳数据的文件描述符的数量,它们是最大为2GB的文件(以匹配Java中内存映射的ByteBuffer的限制)。smoosh文件为数据中的每个列提供单独的文件,并在index.drd文件提供有关该段的额外元数据。
还有一个称为__time的特殊列,它表示该段的时间列。希望随着代码的发展,这种特殊性将越来越少,但就目前而言,它就像我妈妈一直告诉我的那样特殊。
在代码库中,段具有内部格式版本。当前的句段格式版本为v9。
6 列的格式
列的存储分为两部分:
- Jackson序列化的列描述符
- 列二进制文件的其余部分
列描述符本质上是一个对象,它允许我们使用Jackson的多态反序列化来添加新的有趣的序列化方法,并且对代码的影响最小。它由关于列的一些元数据(它是什么类型的,它是多值的,等等)和一列序列化/反序列化逻辑组成,这些逻辑可以反序列化二进制文件的其余部分。
7 切分数据以创建段
数据分片
对于同一数据源,同一时间间隔内可能会存在多个段。这些段在一段时间内会形成一个“块”。根据用于切分数据的shardSpec类型,只有当一个“块”完成切分时,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个使用线性切片规范进行分段的段,并且系统中只加载了其中的两个段,那么查询将只返回这两个段的结果。
8 Schema更改
1)换段
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
。 如果在后续的某个时间点使用新的schema重新索引数据,则新创建的段会具备更高的版本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)保证了间隔内的的原子更新。
在我们的例子中,在 2015-01-01/2015-01-02
的所有 v2
段加载到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
段的混合。
9 段之间的不同schemas
同一数据源的Druid段可能会有不同的schema。
如果一个字符串列(维度列)存在于一个段中而不是另一个段中,则涉及这两个段的查询仍然有效。对缺少维度的段的查询将表现为该维度只有空值。类似地,如果一个段有一个数值列(指标列),而另一个没有,那么查询缺少指标列的段通常会“做正确的事情”, 在缺失的指标上做聚合操作也就是缺失的。