Druid数据摄取 Schema设计


1 Druid数据模型

有关一般的信息,请查看这篇教程:Druid数据摄取 概述

本教程将讨论来自其他类型系统的常见做法。

  • Druid数据存储在数据源
  • 在摄取数据过程,rollup是可选的。
  • 每一行数据必须有时间戳。Druid里的数据默认按时间进行分区。查询结果也可以按指定的时间段(如分钟、小时、天等)做进一步的细分。
  • 除了timestamp列之外,Druid数据源中的所有列都是dimensionsmetrics
  • dimension列按数据原有格式存储,因此可以在查询时对其进行筛选、分组或聚合。它们总是单个字符串、字符串数组、单个long、单个double或单个float。
  • Metrics列是预聚合存储的,因此只能在查询过程中进行聚合,即不能进行筛选或分组操作。

2 与其他设计模式类比

1)关系模型(如Hive或者PostgreSQL)

Druid数据源通常相当于关系数据库中的表。

Druid的lookups特性类似于数据仓库中的维度表

关系数据建模涉及规范化的思想,即将数据拆分为多个表,从而减少数据冗余。

在Druid中建模关系数据的技巧:

  • Druid数据源没有主键或唯一键。
  • 如果可能的话,去规格化。如果需要定期更新dimensions/lookup并将这些更改反映在已接收的数据中,请考虑使用lookups进行部分规范化。
  • 如果需要将两个大型的分布式表连接起来,则必须在将数据加载到Druid之前执行此操作。Druid不支持两个数据源的查询时间连接。
  • 考虑是否要为预聚合启用rollup,或者是否要禁用rollup并按原样加载现有数据。Druid中的rollup类似于在关系模型中创建摘要表
2)时序模型(如OpenTSDB或者InfluxDB)

与时间序列数据库类似,Druid的数据模型需要时间戳,即时序模型。

为了在Druid中实现时序数据的最佳压缩和查询性能需要按照metric名称进行分区和排序。

在Druid中构建时序模型时序数据的技巧:

  • “数据点是时间序列的一部分”这一思想在Druid并不通用。相反,Druid会对每一时间点上的数据进行摄取和聚合。
  • 创建一个维度,该维度指示数据点所属系列的名称。这个维度通常被称为”metric”或”name”。
  • 为附着到数据点的属性创建其他维度。在时序数据库系统中,这些通常称为”标签”。
  • 创建能够与查询聚合类型相对应的Druid Metrics。通常包括”sum”、”min”和”max”(在long、float或double中的一种)。
  • 考虑是否启用rollup。
  • 如果不知道实现要摄取哪些列,请使用空的维度列表来触发维度列的自动检测
3)日志聚合模型(如ElasticSearch、Splunk)

与日志聚合系统类似,Druid提供反向索引,用于快速搜索和筛选。Druid的搜索能力通常不如这些系统发达,其分析能力通常更为发达。Druid和这些系统之间的主要数据建模差异在于,在将数据摄取到Druid中时,必须更加明确。Druid列具有特定的类型,而Druid目前不支持嵌套数据。

在Druid中建模日志数据的技巧:

  • 如果您提前不知道要摄取哪些列,请使用空维度列表来触发维度列的自动检测
  • 如果有嵌套数据,请使用展平规范将其扁平化
  • 如果您主要有日志数据的分析场景,请考虑启用rollup这意味着您将失去从Druid中检索单个事件的能力,但您可能获得大量的压缩和查询性能提升

3 一般提示以及最佳实践

1)Rollup

Druid可以在接收数据时将其汇总,以最小化需要存储的原始数据量。这是一种汇总或预聚合的形式。

2)分区与排序

对数据进行最佳分区和排序会对占用空间和性能产生重大影响。

3)Sketches高基维处理

在处理高基数列(如用户ID或其他唯一ID)时,请考虑使用草图(sketches)进行近似分析,而不是对实际值进行操作。当您使用草图(sketches)摄取数据时,Druid不存储原始原始数据,而是存储它的“草图”,它可以在查询时输入到以后的计算中。草图的常用场景包括count-distinct和分位数计算。每个草图都是为一种特定的计算而设计的。

一般来说,使用草图有两个主要目的:改进rollup和减少查询时的内存占用。

草图可以提高rollup比率,因为它们允许您将多个不同的值折叠到同一个草图中。例如,如果有两行除了用户ID之外都是相同的(可能两个用户同时执行了相同的操作),则将它们存储在count-distinct sketch中而不是按原样,这意味着您可以将数据存储在一行而不是两行中。您将无法检索用户id或计算精确的非重复计数,但您仍将能够计算近似的非重复计数,并且您将减少存储空间。

草图减少了查询时的内存占用,因为它们限制了需要在服务器之间洗牌的数据量。例如,在分位数计算中,Druid不需要将所有数据点发送到中心位置,以便对它们进行排序和计算分位数,而只需要发送点的草图。这可以将数据传输需要减少到仅千字节。

4)字符串 VS 数值维度

如果用户希望将列摄取为数值类型的维度(Long、Double或Float),则需要在dimensionsSpec的dimensions部分中指定列的类型。如果省略了该类型,Druid会将列作为默认的字符串类型。

字符串列和数值列之间存在性能折衷。数值列通常比字符串列更快分组。但与字符串列不同,数值列没有索引,因此可以更慢地进行筛选。您可能想尝试为您的用例找到最佳选择。

5)辅助时间戳

Druid schema必须始终包含一个主时间戳,主时间戳用于对数据进行分区和排序,因此它应该是您最常筛选的时间戳。Druid能够快速识别和检索与主时间戳列的时间范围相对应的数据。

如果数据有多个时间戳,则可以将其他时间戳作为辅助时间戳摄取。最好的方法是将它们作为毫秒格式的Long类型维度摄取。

6)嵌套维度

在编写本文时,Druid不支持嵌套维度。嵌套维度需要展平,例如,如果您有以下数据:

{"foo_bar": 3}

然后在编制索引之前,应将其转换为:

{"foo_bar": 3}

Druid能够将JSON、Avro或Parquet输入数据展平化。

7)计数接收事件数

启用rollup后,查询时的计数聚合器(count aggregator)实际上不会告诉您已摄取的行数。它们告诉您Druid数据源中的行数,可能小于接收的行数。

在这种情况下,可以使用摄取时的计数聚合器来计算事件数。但是,需要注意的是,在查询此Metrics时,应该使用longSum聚合器。查询时的count聚合器将返回时间间隔的Druid行数,该行数可用于确定rollup比率。

为了举例说明,如果摄取规范包含:

...
"metricsSpec" : [
      {
        "type" : "count",
        "name" : "count"
      },
...

您应该使用查询:

...
"aggregations": [
    { "type": "longSum", "name": "numIngestedEvents", "fieldName": "count" },
...
8)无schema的维度列

如果摄取规范中的dimensions字段为空,Druid将把不是timestamp列、已排除的维度和metric列之外的每一列都视为维度。

注意,当使用无schema摄取时,所有维度都将被摄取为字符串类型的维度。

一个具有唯一ID的工作流能够对特定ID进行过滤,同时仍然能够对ID列进行快速的唯一计数。如果不使用无schema维度,则通过将Metric的name设置为与维度不同的值来支持此场景。如果使用无schema维度,这里的最佳实践是将同一列包含两次,一次作为维度,一次作为hyperUnique Metric。这可能涉及到ETL时的一些工作。

例如,对于无schema维度,请重复同一列:

{"device_id_dim":123, "device_id_met":123}

同时在metricsSpec中包含:

{ "type" : "hyperUnique", "name" : "devices", "fieldName" : "device_id_met" }