Druid的架构设计

一、Druid的数据模型

数据模型的详细信息参考摄入概述页面的druid数据模型,接下来章节我们将会介绍与用户使用过的其他的数据库系统的差别,并说一说指导方法和常见做法

  • druid数据是存储在DataSource里面的,它类似于传统RDBMS里面的表
  • druid DataSource数据插入的时候,既可以使用Rollup也可以不使用。当Rollup开启的时候,druid会在数据插入的时候部分的聚合我们的数据,并自动的减少它的行数,降低存储容量,并提升查询性能。当Rollup关闭的时候,我们插入一条数据,druid就会保存一行数据,并不会进行预聚合
  • druid中的每一行数据都必须有时间戳,数据都是根据时间进行分区的,并且每一次查询都必须根据时间进行过滤。查询结果也可以根据时间桶进行处理,比如分钟、小时、天……
  • 除了时间戳列,所有在druid DataSource里面的列,要么是维度要么是指标,它们遵守OLAP数据的标准命名规范
  • 典型的生产DataSource有几十到几百个列
  • 维度列是原样存储的,所以他们可以被过滤,分组,或者根据查询时间聚合。它们一般是单个字符串,字符串数组,整形或者浮点型数字
  • 指标列是预聚合存储的,所以他们只能根据查询时间进行聚合,不能进行过滤和分组。它们常常被存储为数字(整数或浮点数),当然也可以存储复杂的对象比如HyperLogLog、sketches 或者 approximate quantile sketches. 当Rollup关闭的时候,在插入数据的时候也能存储指标,但是当Rollup打开的时候才更有意义。

二、如果你有其他数据库的使用背景:

1、关系型模型(比如 Like Hive or PostgreSQL)

    Druid的DataSource一般情况下等价于关系型数据库的表。Druid lookups 类似于数据仓库式的维度表,就像下面你将要看到的,如果你能避免Druid一些不适合场景,那么推荐使用非规范化的结构。
    关系型数据库的通常实践是规范化,通过把数据存储在多个表里面来降低或消除冗余。例如:在“sales”表里面,关系型建模的最佳实践是增加一个product_id列的外键来参考“products"表。“products"表里面有"product id", "product name"和 "product category"列。这可以防止产品名称和类别需要在“sales”表中引用同一产品的不同行上重复。
     在druid里面,扁平化的DataSource在查询的时候通常不需要join,像上面例子里面的“sales”表,在druid里面将会把"product id", "product name"和 "product category"作为维度直接存储在sales DataSource里面,不需要额外的products表。完全扁平化的架构极大的提高了性能,因为在查询的时候不需要做join操作。另外还有一个额外的提升是,它允许druid的查询层直接操作根据字典编码压缩的数据。可能与直觉相反,相比于规范化的架构,它并没有极大的耗费存储空间,因为Druid使用字典编码来有效地为字符串列每行存储一个整数
     如果需要,druid DataSource可以通过使用lookups特性实现部分的规范化,它们大致相当于关系数据库中的维度表。在查询的时候,我们需要使用 druid SQL lookup函数,或原生的lookup扩展函数,来代替我们在关系型数据库中使用join关键字。因为lookup table会在查询的时候增加内存消耗和更多的计算开销,只有在我们需要更新lookup表,并且需要快速的将修改的内容体现在表的已经摄入的行的时候推荐使用。

使用druid进行关系型数据建模的建议:

  • druid DataSource没有主键和唯一键
  • 尽量非规范化,如果我们周期性的需要更新维度或者 查询表,并将修改体现在已经插入的数据中,考虑使用部分规范化的 lookups
  • 如果你需要关联两个非常大的分布式表,我们需要先将数据加载到druid,druid不支持查询的时候关联两个DataSource。lookups 也不能提供帮助,因为每个查找表的完整副本存储在每个Druid服务器上,所以对于大表来说这不是一个好的选择
  • 考虑到我们是否会开启汇总来做预聚合,或者我们想禁用汇总来原样加载已经存在的数据。druid的汇总功能类似于在关系型模型里面创建一个汇总表。

2、时序数据库模型(比如OpenTSDB 或者 InfluxDB)

和时序数据库类似,druid的数据模型需要一个时间戳,但是druid不是一个时序数据库,但是它是一个存储时序数据的很好的选择。它灵活的数据模型允许它既能存储时序数据也能存储非时序数据,甚至在同一个数据源里面。
为时序数据获得最佳的压缩和查询性能,它也像时序数据库常做的一样,进行分区和根据指标名排序。详情可以看 分区和排序部分内容

使用druid进行时序数据建模的建议:

druid并不认为数据点是时序数据的一部分。替代的是,druid对每个点都分开进行插入和聚合
创建一个维度,该维度指示数据点所属系列的名称。维度通常被叫做指标或名称。不要把这里的维度和druid的metrics搞混淆了。为了获得最佳的性能,将它放在dimensionSpec声明的一系列的维度的第一个(这有助于提高局部性;有关详细信息,请参阅分区和排序)
为我们其他的数据点的属性创建其他的维度。这些维度在其他的时序数据库中常被叫做tags
为我们的查询需求根据聚合类型创建指标。典型的包括 sum,min,max。如果我们想要计算百分数或分位数,需要使用druid的近似聚合
考虑开启Rollup功能,它将会使druid自动的合并多条数据为一行。它对于如果我们想以特定的时间粒度而不是数据产生的时间来存储数据非常有用。对于我们想把时序数据和非时序数据存储在同一个DataSource里面也非常有用。
如果事先不知道插入的数据的列,那么我们就使用空维度列表来触发自动检测维度列。

3、日志聚合模型(比如Elasticsearch or Splunk)

与日志聚合模型一样,druid也使用倒排索引来快速的查询和过滤。druid的查询能力不如这些成熟的系统,但是它的分析能力非常出色。Druid和这些系统之间的主要数据建模差异在于在将数据摄取到Druid中时必须更加明确。Druid列具有特定的类型,而且Druid目前不支持数据嵌套。

三、通用建议和最佳实践
1、Rollup:

druid在摄入数据的时候以最小化的存储行数来汇总数据。这是一种汇总和预聚合的形式。更多细节参考摄入文档的 rollup部分

2、分区和排序

对我们的数据进行最优化的分区和排序能极大的影响数据量和性能。更多细节参考摄入文档的 partitioning部分

3、对基数比较大的列使用摘要

当处理基数比较大的列的时候,比如用户ID或者唯一ID,建议使用摘要进行近似分析而不是操作真实值。当我们使用摘要来插入数据时,druid并不存储原始值,但是相反,它存储了一个“摘要”,可以在查询时输入到以后的计算中。摘要的常用用例包括计数区分和分位数计算。每个草图都是为一种特定的计算而设计的。
通常情况下使用摘要有两个主要目的:在查询的时候进行汇总和降低存储容量
摘要提高汇总比例是因为它允许我们将多个不同的值生成相同的摘要。例如如果有两行除了用户ID之外都是相同的(可能两个用户同时执行了相同的操作),则将它们存储在count-distinct的草图中而不是按原样存储,这意味着您可以将数据存储在一行而不是两行中。我们将无法根据用户id检索或计算精确的非重复计数,但是我们仍然能够计算近似的非重复计数,并且将减少存储空间。
摘要减少了查询时的内存占用,因为它们限制了需要在服务器之间清洗的数据量。例如,在分位数计算中,Druid不需要将所有数据点发送到计算中心,以便对它们进行排序和计算分位数,而只需要发送数据点的摘要。这可以将数据传输需要减少到仅千字节。
druid摘要的详细信息,参考近似聚合页面

4、字符串vs数字维度

如果一个用户希望摄入一个列是数字类型的维度(整形,浮点数),那就需要在dimensionspec里面特别声明这个列的类型。如果没有声明,druid将会以字符串的类型摄入列
在字符串和数字列之间需要做一下性能取舍。数字列比字符串列更快的进行分组。但是不像字符串列,数字列没法进行索引,这样它们在过滤的时候就会比较慢。我们需要根据我们的经验来选择最合适的策略
怎么样配置数字类型的维度,参考dimensionspec文档

5、第二时间戳

druid的架构要求必须包含主时间戳,主时间戳将被用于分区和排序我们的数据,所以这个字段将是我们经常用来进行过滤的。Druid能够快速定位和检索与主时间戳列的时间范围相对应的数据。
如果我们的数据里面还有其他的时间戳,我们可以将剩余的作为第二时间戳。最好的方式是将他们以毫秒的长整形维度插入进来。根据需要我们可以通过 transformSpect和表达式(例如timestamp_parse)来进行格式化,当返回的时候都是毫秒的时间戳
在查询的时候,我们可以对第二时间戳使用 SQL时间函数,比如 millis_to_timestamp,time_floor等等,如果我们使用原生的druid查询,还能使用表达式

6、嵌套维度

目前druid还不支持嵌套维度,嵌套的维度需要扁平化处理,举个例子,如果我们的数据是这样的:
{"foo":{"bar": 3}}
在索引它之前,我们需要转换成这样:
{"foo_bar": 3}
druid可以处理扁平化的JSON, Avro, or Parquet,详细信息参考:fattenSpec

7、摄入事件的计数

当汇总启动以后,count的聚合方式在查询的时候不会真实告诉你摄入的总行数。druid DataSource的总行数可能比摄入的数据的总行数要小。
在这种情况下,count的集合方式在摄入的时候被用来统计event的总数。需要注意的是当我们查询这个指标的时候,需要使用longSum聚合方式。count 聚合类型会在查询的时候返回这段时间druid的行数,这个数字可以确定汇总的比率

举例说明:如果我们摄入的数据是:
"metricsSpec" : [ { "type" : "count", "name" : "count" },
如果我们要查询摄入数据的行数:
"aggregations": [ { "type": "longSum", "name": "numIngestedEvents", "fieldName": "count" }

8、无架构的维度

如果摄取规范中的维度字段为空,druid 将把除了timestamp列、已经被排除的维度或者指标列以外的都看做是维度,需要注意的是如果采用无架构维度,则所有的维度将被当做字符串处理。

9、一个字段即作为维度也作为指标

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

例如:无架构维度,重复相同的列:
{"device_id_dim":123, "device_id_met":123}
我们的metricsSpec需要这样写:
{ "type" : "hyperUnique", "name" : "devices", "fieldName" : "device_id_met" }

device_id_dim将会自动被当做维度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值