实时OLAP引擎Druid.io——工作原理

  |   0 评论   |   3,491 浏览

Druid是什么?

Druid是一个为了在大型(设计为PB级别)数据集上进行实时查询而设计的开源数据分析和存储系统,它的设计宗旨是在面对代码部署、机器故障和其他意外情况时能提供服务并保持100%时间的正常运行。它也可以用于后台服务,但是设计明确地针对永远在用的服务。

Druid目前允许以类似于Dremel和PowerDrill的方式进行单表查询。同时增加了一些新特性:

  1. 为局部嵌套数据结构提供列式存储格式

  2. 分布式查询树模型

  3. 为快速过滤做索引

  4. 实时摄入数据和查询(摄入的数据是立即可用于查询)

  5. 高容错的分布式体系架构

至于和其他系统的比较,Druid功能介于Dremel和PowerDrill之间,Druid实现了几乎Dremel的所有功能(Dremel可以处理任意嵌套数据结构,而Druid只能处理单一的数组类型),从PowerDrill借鉴了一下比较实用的数据布局和压缩方法。

Druid是一个大数据流、单一的数据摄取产品的不错的选择。特别是如果你的目标是想构建一个无停机和流入的数据以时间为导向的产品,Druid很适合。当谈查询速度的话题时,要明确一个很重要说明,"fast"的意义是什么:Druid完全有可能实现不到一秒钟完成对数以万亿行数据的查询(官方已经测试成功)。

系统架构

Druid 集群由不同类型的节点组成,每个类型的节点被设计用来执行一组特定的任务 。不同的节点类型操作相对独立,并且不同的节点类型之间仅有极少的交互。因此,集群内通讯故障对数据可用性的影响微乎其微。为了解决复杂的数据分析问题,不同的节点类型组合在一起形成一个完成的工作体系。

节点类型

  • Historical是处理存储和查询“历史”数据(包括系统中已经存在足够长时间以提交的任何流数据)的主要工具。Historical进程从deep storage中下载segments并响应有关这些segments的查询。Historical不接受写请求。

  • MiddleManager将新数据摄取到集群中。负责从外部数据源读取数据并发布新的Druid segments。

  • Broker代理接收客户端查询并将这些查询转发给Historicals和MiddleManagers。当Brokers从这些子查询中收到结果时,它们会合并这些结果,然后将它们返回给调用者。用户通常会查询Broker,而不是直接查询Historicals或MiddleManagers。

  • Coordinator监视Historical流程。它们负责将segments分配给特定的Server,并确保segments在Historical节点之间的rebalance。

  • Overlord监视MiddleManager进程,并且是数据摄入Druid的控制器。它们负责将提取任务分配给MiddleManagers并协调segment发布。

  • Router是可选的,用于在Druid Brokers,Overlords和Coordinator之前提供统一的API网关。它们是可选的,因为您也可以直接访问Druid Brokers, Overlords, and Coordinator。

关于各节点的详细讨论请参考《实时OLAP引擎Druid.io——节点类型

外部依赖

除了这些节点还有三个外部依赖系统

  1. ZooKeeper集群主要作用是帮助群集服务发现和维护当前数据的拓扑结构

  2. 一个元数据实例,用户维护系统中segments的元数据

  3. 一个深度存储系统,负责存储segments(如hdfs、S3)

部署

Druid的进程既可以单独部署(物理服务器、虚拟服务器、容器),也可以在共享服务器上共存。一种常见的部署方式如下:

  • "Data" 服务器运行Historical和MiddleManager进程.

  • "Query" 服务器运行Broker和Router进程(可选).

  • "Master" 服务器运行Coordinator和Overlord进程. 这些服务器上也可以运行ZooKeeper节点.

这种架构背后的想法是使Druid集群在生产中大规模部署变得简单。例如,深度存储和元数据存储与集群其余部分的分离意味着Druid进程具有极大的容错能力:即使每个Druid服务器都出现故障,您仍然可以利用deep storage和元数据重新启动集群。

下图显示了查询和数据流怎样通过这些节点

druid-architecture.png

Segments和数据存储

Druid数据存储在“datasources”中,类似于传统RDBMS中的表。每个datasource按时间划分,并可选择进一步按其他属性划分。每个时间范围称为“chunk”(例如,如果您的datasource按天分区,则为一天)。在chunk内,数据被划分为一个或多个“segments”。每个segments都是单个文件,通常包含多达几百万行数据。由于segments被组织成时间chunk,参考下图将更容易理解一些

druid-timeline.png

索引过程的输出称为一个"segment",Segments是Druid数据存储的基本存储结构。Segments包含一个数据集内的各种维度和度量值,以列的排列方式存储,以及这些列的索引。

Segments存储在LOB的store/file结构的"deep storage"中。在进行查询时,通过历史节点首先下载数据到历史节点的本地磁盘然后将磁盘数据映射到内存。

如果历史节点挂掉,它将不能对外提供查询它本地加载的Segments的服务,但是这些Segments还是存储在Deep Storage中,其他的历史节点还可以很轻松的把需要的Segments下载下来对外提供服务。这就意味着,即使从集群中移除所有的历史节点,也能够保证存储在Deep Storage中的Segments不会丢失。同时也意味着即使Deep Storage不可用,历史节点仍然可以对外服务,因为节点已经从Deep Storage中将相应的Segments拉取到了本地磁盘。


datasources可能只有几个segments,最多可达数十万甚至数百万个segments。每个segments从在MiddleManager上创建开始生命周期,并且在那时,它是可变的和未提交的。segment构建过程包括以下步骤,旨在生成紧凑的数据文件并支持快速查询:

  • 转换为列式存储格式

  • 创建位图索引

  • 使用各种压缩算法:

    • 所有的列使用LZ4压缩

    • 所有的字符串列采用字典编码/标识以达到最小化存储

    • 对位图索引使用位图压缩

然后,segments被定期提交和发布。此时,它们被写入deep storage,变为不可变,并从MiddleManagers转移到Historical。关于该segments的条目也被写入元数据存储。此条目是关于g该segment的自描述元数据,包括segment的schema、segment的大小、segment在Deep Storage中的存贮位置。这些条目被Coordinator用于了解群集上哪些数据可用。

查询处理

查询请求首先进入Broker(代理节点),Broker将识别哪些segments具有可能与该查询相关的数据。segments列表始终按时间进行修剪,也可能会被其他属性修剪,具体取决于数据源的分区方式。然后,Broker将识别哪些Historicals和MiddleManagers正在为这些segments提供服务,并向每个进程发送rewritten的子查询。 Historical / MiddleManager进程将接受查询,处理它们并返回结果。Broker接收结果并将它们合并在一起以获得最终结果,并将其返回给原始调用者。通过这种方式,代理甚至可以在看到一条数据之前,过滤掉不匹配的所有数据。


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


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

  • 为每个查询修剪访问哪些段。 

  • 在每个段内,使用索引来标识必须访问的行。 

  • 在每个段中,仅读取与特定查询相关的特定行和列。

外部依赖

Deep storage

Druid仅使用deep storage来做数据的备份,和作为在Druid进程之间在后台传输数据的一种方式。当响应查询时,Historical不会从deep storage读取数据,而是在提供任何查询之前从其本地磁盘读取预取的段。这意味着Druid在查询期间永远不需要访问deep storage,从而帮助它提供最佳的查询延迟。这也意味着您必须在deep storage和计划加载的数据的Historical中拥有足够的磁盘空间。

Metadata storage

元数据存储保存各种系统元数据,例如段可用性信息和任务信息。

Zookeeper

Druid使用ZooKeeper(ZK)来管理当前的集群状态。


读后有收获可以支付宝请作者喝咖啡