Druid 的目标是提供一个能够在大数据集上做实时数据摄入与查询的平台,然而对于大多数系统而言,提供数据的快速摄入与提供快速查询是难以同时实现的两个指标。例如对于普通的RDBMS,如果想要获取更快的查询速度,就会因为创建索引而牺牲掉写入的速度,如果想要更快的写入速度,则索引的创建就会受到限制。Druid通常是基于时序的事实事件,事实发生后进入Druid,外部系统就可以对该事实进行查询。

Druid系统架构

Druid是一组系统,按照职责分成不同的角色。目前存在五种节点类型:

  • Historical: 历史节点的职责主要是对历史的数据进行存储和查询,历史节点从Deep Storage下载Segment,然后响应Broker对于Segment的查询将查询结果返回给Broker节点,它们通过Zookeeper来声明自己存储的节点,同时也通过zookeeper来监听加载或删除Segment的信号。
  • Coordinator:协调节点监测一组历史节点来保证数据的可用和冗余。协调节点读取元数据存储来确定哪些Segment需要load到集群中,通过zk来感知Historical节点的存在,通过在Zookeeper上创建entry来和Historical节点通信来告诉他们加载或者删除Segment。
  • Broker:节点接收外部客户端的查询,并且将查询路由到历史节点和实时节点。当Broker收到返回的结果的时候,它将结果merge起来然后返回给调用者。Broker通过ZooKeeper来感知实时节点和历史节点的存在。
  • MiddleManager:负责接收数据。
  • Overlord:控制数据提取工作负载的分配。
  • Router可选的进程,可以将请求路由到Broker,Coordinator和Overlords。
  • Indexing Service: 索引服务是一些worker用来从实时获取数据或者批量插入数据。
  • Realtime:获取实时数据

Druid还依赖于外部的三个组件:

  • ZooKeeper :用于内部服务发现、协调和领导选举。
  • Metadata Storage :元数据存储实例,如:MySQL
  • Deep Storage :每个druid服务器都可以访问共享文件存储。通常是一个分布式对象存储,如S3或HDFS。

Druid架构以及数据存储-LMLPHP

数据流动图 

Druid架构以及数据存储-LMLPHP

数据存储

LSM-Tree适合哪种写操作要远远大于DELETE/UPDATE/QUERY的应用场景,这正好符合Druid的使用场景,所以Druid的文件组织方式与LSM-Tree类似。

Datasources

Druid将数据组织成Read-Optimized的结构,而这也是Druid能够支持交互式查询的关键。Druid中的数据存储在被称为datasource中,类似RDMS中的table。每个datasource按照时间划分,如果你有需求也可以进一步按其它属性划分。每个时间范围称为一个chunk(比如你按天分区,则一个chunk为一天)。在chunk中数据由被分为一个或多个segment(segment是数据实际存储结构,Datasource、Chunk只是一个逻辑概念),每个segment都是一个单独的文件,通常包含几百万行数据,这些segment是按照时间组织成的chunk,所以在按照时间查询数据时,效率非常高。

Datasource包含下面三个重要的概念: 

  1. 时间列(Timestamp):每行数据的时间值,默认使用UTC时间格式,保存到毫秒级别,本列是数据聚合以及范围查询的重要指标 
  2. 维度列(Dimension):标识数据行的列,可以是一列,也可以是多列 
  3. 指标列(Metric):用来做计算或是统计的列,可以是一列,也可以是多列

相对于其他数据库,Druid Datasource最大的特点是在输入存储时,就可以对数据进行聚合操作,该特性不仅可以节省存储的空间,而且可以提高聚合查询的效率。

Druid架构以及数据存储-LMLPHP

数据分区

任何分布式存储/计算系统,都需要对数据进行合理的分区,从而实现存储和计算的均衡,以及数据并行化。而Druid本身处理的是事件数据,每条数据都会带有一个时间戳,所以很自然的就可以使用时间进行分区。比如上图,我们指定了分区粒度为为天,那么每天的数据都会被单独存储和查询。

使用时间分区我们很容易会想到一个问题,就是很可能每个时间段的数据量是不均衡的(想一想我们的业务场景),而Duid为了解决这种问题,提供了“二级分区”,每一个二级分区称为一个Shard(这才是物理分区)。通过设置每个Shard的所能存储的目标值和Shard策略,来完成shard的分区。Druid目前支持两种Shard策略:Hash(基于维值的Hash)和Range(基于某个维度的取值范围)。上图中,2000-01-01和2000-01-03的每个分区都是一个Shard,由于2000-01-02的数据量比较多,所以有两个Shard。

Segment

Shard经过持久化之后就称为了Segment,Segment是数据存储、复制、均衡(Historical的负载均衡)和计算的基本单元了。Segment具有不可变性,一个Segment一旦创建完成后(MiddleManager节点发布后)就无法被修改,只能通过生成一个新的Segment来代替旧版本的Segment。

Segment为Druid中数据的物理存储格式,Segment通过以下特性来支撑Druid的高性能:

  • 数据的横向切割:横向切割主要只指站在时间范围的角度,将不同时间段的数据存储在不同的Segment文件中(时间范围可以通过segmentGranularity进行设置),查询时只需要根据时间条件遍历对应的Segment文件即可。
  • 数据的纵向切割:面向列进行进行数据压缩
  • 使用BitMap等技术对数据访问进行优化

Segment内部存储结构

Druid架构以及数据存储-LMLPHP

因为Druid采用列式存储,所以每列数据都是在独立的结构中存储(并不是独立的文件,是独立的数据结构,因为所有列都会存储在一个文件中)。Segment中的数据类型主要分为三种:时间戳、维度列和指标列。

对于时间戳列和指标列,实际存储是一个数组,Druid采用LZ4压缩每列的整数或浮点数。当收到查询请求后,会拉出所需的行数据(对于不需要的列不会拉出来),并且对其进行解压缩。解压缩完之后,在应用具体的聚合函数。

对于维度列不会像指标列和时间戳这么简单,因为它需要支持filter和group by,所以Druid使用了字典编码(Dictionary Encoding)和位图索引(Bitmap Index)来存储每个维度列。每个维度列需要三个数据结构:

  1. 需要一个字典数据结构,将维值(维度列值都会被认为是字符串类型)映射成一个整数ID。
  2. 使用上面的字典编码,将该列所有维值放在一个列表中。
  3. 对于列中不同的值,使用bitmap数据结构标识哪些行包含这些值。

Druid针对维度列之所以使用这三个数据结构,是因为:

  1. 使用字典将字符串映射成整数ID,可以紧凑的表示结构2和结构3中的值。
  2. 使用Bitmap位图索引可以执行快速过滤操作(找到符合条件的行号,以减少读取的数据量),因为Bitmap可以快速执行AND和OR操作。
  3. 对于group by和TopN操作需要使用结构2中的列值列表。

"Page"维度列为例,可以具体看下Druid是如何使用这三种数据结构存储维度列:

1. 使用字典将列值映射为整数
{
"Justin Bieher":0,
"ke$ha":1
}
2. 使用1中的编码,将列值放到一个列表中
[0,0,1,1]
3. 使用bitmap来标识不同列值
value = 0: [1,1,0,0] //1代表该行含有该值,0标识不含有
value = 1: [0,0,1,1]

查询流程

查询首先进入Broker,Broker将识别哪些段具有可能与该查询相关的数据。段列表始终按时间进行修剪,也可能会被其他属性修剪,具体取决于数据源的分区方式。然后,Broker将识别哪些Historicals和MiddleManagers正在为这些段提供服务,并向每个进程发送重写的子查询。 Historical / MiddleManager进程将接受查询,处理它们并返回结果。Broker接收结果并将它们合并在一起以获得最终答案,并将其返回给原始调用者。

Broker修剪是Druid限制每个查询必须扫描的数据量的重要方式,但这不是唯一的方法。对于比Broker可用于修剪更细粒度的过滤器,每个段内的索引结构允许Druid在查看任何数据行之前确定哪些(如果有)行匹配过滤器集。一旦Druid知道哪些行与特定查询匹配,它只访问该查询所需的特定列。在这些列中,Druid可以在行之间跳过,避免读取与查询过滤器不匹配的数据。

所以Druid使用三种不同的技术来最大化查询性能:

  • 修剪为每个查询访问哪些段。
  • 在每个段内,使用索引来标识必须访问的行。
  • 在每个段内,仅读取与特定查询相关的特定行和列。

 

参考:

https://druid.apache.org/docs/latest/design/

 Druid高效架构

Druid(一)——Druid架构概览

Druid 架构详解

Druid.io

08-04 14:42