架构

什么是Druid

Druid是一个高效的数据查询系统,主要解决的是对于大量的基于时序的数据进行聚合查询。数据可以实时摄入,进入到Druid后立即可查,同时数据是几乎是不可变。通常是基于时序的事实事件,事实发生后进入Druid,外部系统就可以对该事实进行查询。

Druid 简介,架构,部署,python连接,hue链接druid_segments


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

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

除了上述五个节点,Druid还有三个外部依赖:

  • Zookeeper集群
  • 元数据存储实例:Mysql
  • Deep Storage:HDFS/或者分布式文件系统

参考:
https://help.aliyun.com/document_detail/72987.html?spm=a2c4g.11186623.6.675.5ebb7ba7mPowo3

Segments

Druid 把它的索引存储到一个Segment文件中,Segment文件是通过时间来分割的。

Segment数据结构

对于摄入到Druid的数据的列,主要分三种类型,时间列(timestamp)、维度列(dimensions)和指标列(聚合列metrics)。如下图

Druid 简介,架构,部署,python连接,hue链接druid_segments_02

  • 时间列 每一条数据都必须有此列,此列会决定数据所在的Segment(后面会介绍),所有的查询也都需要此列,在上面对应的数据列为timestamp,在系统中会存储的字段名为__time;
  • 维度列 Druid会针对这些列建立bitmap的倒排索引,用于筛选数据,结合时间列便能快速找到需要查询的数据。根据假设的分析模型,我们只需要publisher和advertiser,而其他例如gender和country并非我们需要分析的列,便可以在建立模型的时候忽略,需要注意的是Druid只有String型的维度列,如果我们需要对维度列做数字范围的筛选,需要做特殊处理
  • 聚合列(metric column) Druid会根据我们定义的时间粒度,再结合维度列在数据接入的时候对数据进行聚合,并根据聚合类型,用不同的数据结构来保存这一列。这一列算是Druid比较复杂的一个概念,与下面介绍的预聚合有很大关系,稍后我会进一步解释。在假设的分析模型中,我们需要对click列求sum。

于是我们可以建立一个简单的分析模型:

"dataSchema": {
    "dataSource": "wiki",
    "parser": {
      "type": "string",       #说明读取的每一行数据都是都是string类型
      "parseSpec": {
        "format": "json",     #说明数据的格式需要通过json来解析
        "timestampSpec": {    #指明时间列字段和格式
          "column": "timestamp",
          "format": "auto"
        },
        "dimensionsSpec": {   #指明需要索引的维度,只有加到此处的维度才可以查询
          "dimensions": ["publisher", "advertiser"]
        }
      }
    },
    "metricsSpec": [{
      "type" : "count",  # 统计行数
      "name" : "rows"
    },{
       "name": "click_sum",
       "fieldName": "click",
       "type": "longSum"    #整数求和
    }],
    "granularitySpec" : {     #预聚合相关的配置
      "segmentGranularity" : "HOUR",
      "queryGranularity" : "HOUR"
    }
  }

上面的分析模型是一个简单的使用,Druid本身的配置性非常强,我会在后续章节介绍这些配置的实现原理。

预聚合

上面的样例数据我们可以称之为原始数据,为我们直接采集到的数据,每天这样的数据有千亿甚至万亿。基于这些原始数据,如果我们需要分析点击量,成本是非常高的,因为我们尽管我们可以通过维度筛选出一部分数据,但是数据量还是很巨大,需要扫描这些数据来聚合,如此便会直接导致查询时长加长。为了避免这种情况,Druid采用了预聚合的设计,在数据接入的时候,就开始根据定义的维度,对数据进行聚合。Druid的预聚合分为两个级别,第一级是时间粒度,第二级是维度。

时间粒度

例子中模型的配置有segmentGranularity 和queryGranularity两个关键的参数,可以理解为一个是段的粒度,一个是查询的最小粒度。首先,Druid会根据timestamp和segmentGranularity来决定数据所属的段;然后,再根据timestamp和queryGranularity来决定存储在Druid的__time。

维度

Druid的预聚合过程也是其索引的过程,通常Druid会在内存中使用ConcurrentSkipListMap存储数据,以TimeAndDims(时间和维度值信息)作为key,根据配置的metricsSpec对数据进行聚合,聚合的结果作为value。为了避免OOM,最后会以bitmapIndex的数据格式周期性的落地到磁盘存储。

于是,样例数据最后存储的格式大概为:

Segment sampleData_2011-01-01T01:00:00:00Z_2011-01-01T02:00:00:00Z_v1_0

__time	publisher	advertiser	rows	click_sum
2011-01-01T01:00:00Z	ultratrimfast.com	google.com	1	0
2011-01-01T01:00:00Z	bieberfever.com	google.com	3	1
Segment sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_0

__time	publisher	advertiser	rows	click_sum
2011-01-01T02:00:00Z	ultratrimfast.com	google.com	2	1

segmentGranularity为小时,所以对应的段都是小时级别。queryGranularity为小时,聚合的结果对应的__time也是整点,会将每小时的数据全部聚合到其所属小时的开始时间。 另外说明,倒数第二例为总共的行数,最后一列为点击数。sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_0中的v1通常为Task启动时获取的TaskLock的version,可以理解为Task启动的时间。

对于时间列和指标列处理比较简单,直接用LZ4压缩存起来就ok,一旦查询知道去找哪几行,只需要将它们解压,然后用相应的操作符来操作它们就可以了。维度列就没那么简单了,因为它们需要被过滤和聚合,因此每个维度需要下面三个数据结构。

一个map,Key是维度的值,值是一个整型的id
一个存储列的值得列表,用1中的map编码的list
对于列中的每个值对应一个bitmap,这个bitmap用来指示哪些行包含这个个值。

对于上图的Page列,它的存储是这样的

1: 字典
{
    "Justin BIeber": 0,
    "Ke$ha":         1
}

2. 值的列表
[0,
 0,
 1,
 1]

3. bitMap
value="Justin Bieber": [1, 1, 0, 0]
value="Ke$ha":         [0, 0, 1, 1]

历史节点

每个历史节点维持一个和Zookeeper的长连接监测一组path来获取新的Segment信息。历史节点互相不进行通信,他们依靠zk来等待协调节点来协调。
协调节点负责把新的Segment分发给历史节点,协调节点通过在zk的指定路径下创建一个entry来向历史节点做分发。
当历史节点发现一个新的entry出现在path中,它首先会检查本地文件缓存看有有没Segment信息,如果没有Segment信息,历史节点会从zk上下载新的Segment的元信息。Segment的元信息包括Segment存在Deep Storage的位置和如何解压和处理Segment。一旦一个历史节点完成对一个Segment的处理,这个历史节点会在zk上的一个路径声明对这个Segment提供查询服务,此刻这个Segment就可以查询了。

查询节点

Broker节点负责将查询路由到历史节点和实时节点,Broker节点通过zk来知道哪些Segment存在哪个节点上。Broker也会把查询的结果进行Merge
大多数Druid查询包含一个区间对象,这个对象用来指定查询所要查的区间段。Druid的Segment也通过时间段进行分割散落在整个集群中。假设有一个简单的数据源,这个数据源有七个Segment,每个Segment包含一周中的某一天的数据。任何一个时间范围超过一天的查询都会落到不止一个Segment上。这些Segment可能分布在集群中不同的节点上。因此这种查询就会涉及到多个节点。
为了确定发送到哪个节点上,Broker会从Historial和RealTime的节点来获取他们提供查询的Segment的信息,然后构建一个时间轴,当收到特定的时间区间的查询时,Broker通过时间轴来选择节点。
Broker节点会维护一个LRU缓存,缓存存着每个Segment的结果,缓存可以是一个本地的缓存或者多个节点共用的外部的缓存如 memcached。当Broker收到查询时候,它首先将查询映射成一堆Segment的查询,其中的一个子集的结果可能已经存在缓存中,他们可以直接从缓存中拉出来,那些没在缓存中的将被发送到相应节点。

协调节点

协调节点负责Segment的管理和分发,协调节点指挥历史节点来加载或者删除Segment,以及Segment的冗余和平衡Segment。协调节点会周期性的进行扫描,每次扫描会根据集群当前的状态来决定进一步的动作。和历史节点和Broker一样,协调节点通过zk来获取Segment信息,同时协调节点还通过数据库来获取可用的Segment信息和规则。在一个Segment提供查询之前,可用的历史节点会按照容量去排序,容量最小的具有最高的优先级,协调节点就会让它去加载这个Segment然后提供服务。

  • 清理Segment,Druid会将集群中的Segment和数据库中的Segment进行对比,如果集群有的的数据库中没有的会被清理掉。同时那些老的被新的替换的Segment也会被清理掉。
  • Segment可用性, 历史节点可能因为某种原因不可用,协调节点会发现节点不可用了,会将这个节点上的Segment转移到其他的节点。Segment不会立即被转移,如果在配置的时间段内节点恢复了,历史节点会从本地缓存加载Segment。恢复服务
  • Segment负载均衡,协调节点会找到Segment最多的节点和Segment最少的节点,当他们的比例超过一个设定的值的时候,协调节点会从Segment最多的节点转移到Segment最少的节点

索引服务

索引服务是一个高可用的,分布式的服务来运行索引相关的Task。索引服务会创建或者销毁Segment。索引服务是一个Master/Slave架构。索引服务是三个组件的集合

  • peon组件用来跑索引任务。
  • Middle Manager组件用来管理peons
  • Overlord向MiddleManager分发任务。

索引服务

Druid 简介,架构,部署,python连接,hue链接druid_Coordinator_03


Overlord节点负责接受任务,协调任务分发,创建锁,和返回状态给调用者。Overlord节点可以以本地模式或者远程模式运行。本地模式会直接创建Peon,远程模式会通过Middle Manager创建任务。

实时节点

实时节点提供实时索引服务,通过实时节点索引的数据立即可查。实时节点会周期性的构建Segment,并且把这些Segment推到历史节点并修改元数据。

Druid 简介,架构,部署,python连接,hue链接druid_Historical_04


参考:https://www.jianshu.com/p/852bb8cfed6b

采用技术

除了 MPP 架构外,它还运用到了四点重要的技术,分别是:

  • 预聚合
  • 列式存储
  • 字典编码
  • 位图索引

预聚合算是 Druid 的一个非常大的亮点,通过预聚合可以减少数据的存储以及避免查询时很多不必要的计算。

Druid 简介,架构,部署,python连接,hue链接druid_Broker_05

特性

为分析而设计:为OLAP工作流的探索性分析而构建,支持各种过滤、聚合和查询等类;

快速的交互式查询:Druid的低延迟数据摄取架构允许事件在他们创建后毫秒内可被查询到;

高可用性:Druid的数据在系统更新时依然可用,规模的扩大和缩小都不会造成数据丢失;

可扩展:Druid已实现每天能够处理数十亿事件和TB级数据。

使用场景

1、需要交互式聚合和快速探究大量数据时;

2、需要实时查询分析时;

3、具有大量数据时,如每天数亿事件的新增、每天数10T数据的增加;

4、对数据尤其是大数据进行实时分析时;

5、需要一个高可用、高容错、高性能数据库时。

Druid 使用建议

本小节主要想结合实际问题,给大家提供一些 Druid 的使用建议,供大家参考。

①什么样的业务适合用 Druid?

建议如下:

时序化数据:Druid 可以理解为时序数据库,所有的数据必须有时间字段。
实时数据接入可容忍丢数据(tranquility):目前 tranquility 有丢数据的风险,所以建议实时和离线一起用,实时接当天数据,离线第二天把今天的数据全部覆盖,保证数据完备性。
OLAP 查询而不是 OLTP 查询:Druid 查询并发有限,不适合 OLTP 查询。
非精确的去重计算:目前 Druid 的去重都是非精确的。
无 Join 操作:Druid 适合处理星型模型的数据,不支持关联操作。
数据没有 update 更新操作,只对 segment 粒度进行覆盖:由于时序化数据的特点,Druid 不支持数据的更新。

k8s集群部署

参考git:https://github.com/626626cdllp/k8s/tree/master/druid

python 操作druid

参考git:

https://pythonhosted.org/pydruid/

https://druid.apache.org/blog/2014/04/15/intro-to-pydruid.html

hue链接druid

http://gethue.com/quick-task-how-to-query-apache-druid-analytic-database/