分片 (Segments)
Apache Druid 将其数据和索引存储在按时间分区的分片文件(segment files)中。Druid 为每个包含数据的分片时间间隔创建一个分片。如果一个时间间隔为空(即不包含任何行),则该时间间隔不存在相应的分片。如果您通过不同的摄取任务摄取同一时间段的数据,Druid 可能会为同一时间间隔创建多个分片。压缩 (Compaction) 是 Druid 的一个进程,旨在尝试将这些分片合并为每个时间间隔一个分片,以实现最佳性能。
时间间隔可在 granularitySpec 的 segmentGranularity 参数中进行配置。
为了使 Druid 在繁重的查询负载下良好运行,分片文件大小保持在 300-700 MB 的建议范围内非常重要。如果您的分片文件超过此范围,请考虑更改分片时间间隔的粒度,或者对数据进行分区和/或调整 partitionsSpec 中的 targetRowsPerSegment。该参数的一个良好起点是 500 万行。更多指导请参阅下方的“分片 (Sharding)”部分以及 批处理摄取 文档中的“分区规范 (Partitioning specification)”部分。
分片文件结构
分片文件是列式(columnar)的:每一列的数据被布局在独立的数据结构中。通过分别存储每一列,Druid 仅扫描查询实际需要的列,从而降低查询延迟。主要有三种基本的列类型:时间戳、维度和指标。

时间戳列和指标列是使用 LZ4 压缩的整数或浮点值数组。一旦查询确定了要选择哪些行,它就会解压这些数据,提取相关行,并应用所需的聚合算子。如果查询不需要某列,Druid 会跳过该列的数据。
维度列有所不同,因为它们支持过滤和分组(group-by)操作,因此每个维度都需要以下三种数据结构:
- 字典 (Dictionary):将值(始终视为字符串)映射到整数 ID,从而允许紧凑地表示列表和位图值。
- 列表 (List):使用字典编码的列值。这是 GroupBy 和 TopN 查询所必需的。这些算子允许仅基于过滤条件聚合指标的查询在无需访问值列表的情况下运行。
- 位图 (Bitmap):列中每个唯一值对应一个位图,用于指示哪些行包含该值。位图允许进行快速过滤操作,因为它们便于快速应用 AND 和 OR 算子。也称为倒排索引。
为了更好地理解这些数据结构,请参考上方示例数据中的“Page”列,其表现形式为以下数据结构:
1: Dictionary
{
"Justin Bieber": 0,
"Ke$ha": 1
}
2: List of column data
[0,
0,
1,
1]
3: Bitmaps
value="Justin Bieber": [1,1,0,0]
value="Ke$ha": [0,0,1,1]
请注意,位图与字典和列表数据结构不同:字典和列表随数据大小线性增长,而位图部分的大小是数据大小与列基数(cardinality)的乘积。也就是说,每个不同的列值都有一个位图。具有相同值的列共享同一个位图。
对于列数据列表中的每一行,只有一个位图具有非零条目。这意味着高基数列的位图极其稀疏,因此具有很高的可压缩性。Druid 利用专门适用于位图的压缩算法(例如 Roaring 位图压缩)来优化这一点。
处理空值
如果字符串列在任何行中存在空值,Druid 始终将其存储为 ID 0(值字典中的第一个位置),并在用于过滤空值的位图值索引中包含关联条目。数值列也存储空值位图索引以指示空值行,该索引用于空值检查聚合以及过滤匹配空值。
具有不同模式(Schema)的分片
同一数据源的 Druid 分片可能具有不同的模式。如果一个字符串列(维度)存在于一个分片中而不在另一个分片中,涉及这两个分片的查询仍然有效。在默认模式下,对于没有该维度的分片,查询表现为该维度仅包含空字符串值。在 SQL 兼容模式下,查询表现为该维度仅包含 NULL 值。同样,如果一个分片有数值列(指标)而另一个没有,对没有该指标的分片的查询通常也会按预期运行。对缺失指标的聚合操作表现得就像该指标不存在一样。
列格式
每一列存储为两部分:
- 一个经 Jackson 序列化的
ColumnDescriptor。 - 该列的二进制数据。
ColumnDescriptor 是内部 Druid ColumnDescriptor 类的一个经 Jackson 序列化的实例。它允许使用 Jackson 的多态反序列化,以最小的代码影响添加新的序列化方法。它包含关于该列的一些元数据(例如:类型、是否为多值)以及一个序列化/反序列化逻辑列表,用于反序列化剩余的二进制数据。
多值列
多值列允许单行中的某一列包含多个字符串。您可以将其视为字符串数组。如果数据源使用多值列,则分片文件中的数据结构看起来会略有不同。假设在上面的示例中,第二行同时标记了 Ke$ha 和 Justin Bieber 两个主题,如下所示:
1: Dictionary
{
"Justin Bieber": 0,
"Ke$ha": 1
}
2: List of column data
[0,
[0,1], <--Row value in a multi-value column can contain an array of values
1,
1]
3: Bitmaps
value="Justin Bieber": [1,1,0,0]
value="Ke$ha": [0,1,1,1]
^
|
|
Multi-value column contains multiple non-zero entries
注意列数据列表中第二行的变化以及 Ke$ha 的位图。如果某行在某一列中有多个值,它在列表中的条目就是一个值数组。此外,列表中有 n 个值的行在位图中会有 n 个非零值的条目。
压缩
默认情况下,Druid 使用 LZ4 来压缩字符串、长整型、浮点型和双精度型列的值块。Druid 使用 Roaring 来压缩字符串列的位图和数值空值。我们建议您使用这些默认值,除非您已经对数据进行了实验,且查询模式表明非默认选项在您的特定情况下表现更好。
Druid 还支持 Concise 位图压缩。对于字符串列位图,Roaring 和 Concise 之间的区别在高基数列中最明显。在这种情况下,Roaring 在匹配许多值的过滤器上速度要快得多,但在某些情况下,由于 Roaring 格式的开销,Concise 的占用空间可能更小(但在匹配许多值时仍然较慢)。您是在分片级别配置压缩,而不是针对单个列。详情请参阅 IndexSpec。
分片标识
分片标识符通常包含分片数据源、间隔开始时间(ISO 8601 格式)、间隔结束时间(ISO 8601 格式)和版本信息。如果数据在时间范围之外进行了进一步分片,分片标识符还会包含一个分区号。
datasource_intervalStart_intervalEnd_version_partitionNum
分片 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
如果您使用新的模式重新索引数据,Druid 会为新创建的分片分配一个新的版本 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
分片 (Sharding)
单个时间间隔和数据源可以存在多个分片。这些分片为一个间隔形成一个 块 (block)。根据用于对数据进行分片的 shardSpec 类型,Druid 查询可能仅在 块 完成时才会结束。例如,如果一个块由以下三个分片组成:
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 时间间隔的查询才能完成。
线性分片规范(Linear shard specs)是此规则的例外。线性分片规范不强制要求“完整性”,因此即使分片未完全加载,查询也可以完成。
例如,如果实时摄取创建了三个使用线性分片规范分片的片段,且仅加载了其中两个片段,查询将返回这两个片段的结果。
分片组件
一个分片包含多个文件:
-
version.bin4 个字节,表示当前分片版本的整数。例如,对于 v9 分片,版本为 0x0, 0x0, 0x0, 0x9。
-
meta.smoosh一个包含其他
smoosh文件内容元数据(文件名和偏移量)的文件。 -
XXXXX.smooshSmoosh (
.smoosh) 文件包含连接在一起的二进制数据。这种文件合并减少了访问数据时必须打开的文件描述符数量。文件大小为 2 GB 或更小,以保持在 Java 中内存映射ByteBuffer的限制内。Smoosh 文件包含以下内容:- 数据中每一列的独立文件,包括一个引用分片时间戳的
__time列文件。 - 一个包含额外分片元数据的
index.drd文件。
- 数据中每一列的独立文件,包括一个引用分片时间戳的
在代码库中,分片具有内部格式版本。当前分片格式版本为 v9。
更新分片的影响
Druid 使用版本控制来管理更新,以创建一种多版本并发控制 (MVCC)。这些 MVCC 版本与上述讨论的分片格式版本是不同的。
请注意,跨越多个分片时间间隔的更新仅在每个间隔内是原子的。它们在整个更新范围内不是原子的。例如,如果您有以下分片:
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 分片。