1 Druid概述
1.1 什么是Druid
数据分析的基础架构可以分为以下几类:
- 使用Hadoop/Spark进行分析
- 将Hadoop/Spark的结果导入 RDBMS 中提供数据分析
- 将结果保存到容量更大的 NoSQL 数据库中,解决数据分析的存储瓶颈,例如:HBase
- 将数据源进行流式处理,对接流式计算框架(如Storm、Spark、Flink),结果保存到 RDBMS或NoSQL中
- 将数据源进行流式处理,对接分析数据库,例如:Druid
Druid
是一个开源的数据分析引擎工具,为实时和历史数据的次秒级(多于一秒)查询设计。主要应用于对数据的OLAP查询,
Druid
提供低延迟(实时)的数据摄取、灵活的数据探索、快速的数据聚合。现有的
Druid
部署已支持扩展到数万亿时间和 PB
级数据
1.2 与其他OLAP技术对比
SparkSQL / Impala / ClickHouse
,支持海量数据,灵活性强,但对响应时间是没有保证的。当数据量和计算复杂度增加后,响应时间会变慢,从秒级到分钟级,甚至小时级都有可能。
搜索引擎架构的系统(
Elasticsearch
等),在入库时将数据转换为倒排索引。牺牲了灵活性换取很好的性能,在搜索类查询上能做到亚秒级响应,但是对于扫描聚合为主的查询,随着处理数据量的增加,响应时间也会退化到分钟级。
Druid / Kylin
,则在入库时对数据进行预聚合,进一步牺牲灵活性换取性能,以实现对超大数据集的秒级响应。
- Kylin 利用 Hadoop/HBase 做计算和存储,使用 SQL 查询,提供 JDBC/ODBC 驱动与常见 BI 工具集成
- Druid 有自己独立的分布式集群,能够实时摄入数据,有自己的查询接口(与BI兼容性较弱),通常多用于实时要求高的场景
目前没有一个
OLAP
分析引擎能在数据量、灵活程度、性能(吞吐
&
并发)做到完美,需要基于自己的业务场景进行取舍和选型。
1.3 技术特点
Apache Druid
是一个开源的、分布式、实时
OLAP
分析工具。
Druid
的核心设计结合了数据仓库、时间序列数据库和搜索系统的思想,适用于多种场景的高性能数据实时分析。Druid
将这三个系统中的每个系统的关键特征合并到其接收层、存储格式、查询层和核心体系结构中。
时间序列数据库主要用于指处理,带时间标签(按照时间的顺序变化)的数据,带时间标签的数据也称为时间序列数据。
时间序列数据主要由电力行业、化工行业等各类型实时监测、检查与分析设备所采集、产生的数据,这些工业数据的典型特点是:产生频率快(每一个监测点一秒钟内可产生多条数据)、严重依赖于采集时间(每一条数据均要求对应唯一的时间)、测点多信息量大(常规的实时监测系统均有成千上万的监测点,监测点每秒钟都产生数据,每天产生几十GB
的数据量)。
1
、主要特点
- 列式存储 Druid 独立的存储和压缩每一列,只需要读取特定查询所需的内容,这可以支持快速扫描、排名和聚合
- 流式和批量摄取(Ingestion) 支持 Apache Kafka、HDFS、AWS S3、stream processors 等现成连接器
- 本地的搜索索引 Druid 为字符串创建倒排索引,以支持快速搜索和排序
- 灵活的 schema Druid 可以处理变化的 schema 和嵌套数据
- 基于时间优化 partition Druid 基于时间智能的对数据进行分区,基于时间的查询比传统数据库要快得多
- 支持 SQL Druid支持本机的 JSON 语言,还支持基于 HTTP 或者 JDBC 的 SQL
- 水平扩展性 Druid 已经用户生产环境中,每秒接收数百万个事件,保存多年的数据并提供次秒级查询
- 操作简单 只需要增加或删除服务器即可扩展或缩小规模,Druid 会自动平衡,容错架构通过服务器的故障进行路由
2、集成
Druid
是开源大数据技术的补充,包括
Apache Kafka
,
Apache Hadoop
,
Apache Flink
等,通常位于存储或处理层与最终应用之间,充当查询层或数据分析服务。
3
、
Ingestion
(摄取)
Druid
支持流式传输和批量摄取。
Druid
连接到数据源,包括:
Kafka
(用于流数据加载),或分布式文件系统,如HDFS(用于批处理数据加载)。
Druid
在
“
索引
”
过程中将数据源中的原始数据转换为支持高效读取的优化格式(
Segment
,段)。
4、存储
Druid的数据存储采用列式存储格式。根据列的类型(字符串,数字等),应用不同的压缩和编码方法,根据列类型构建不同类型的索引。
Druid为字符串列构建倒排索引,以进行快速搜索和过滤。Druid可按时间对数据进行智能分区,以实现面向时间的快速查询。
Druid在摄取数据时对数据进行预聚合,节省大量存储空间。
5、查询方式
Druid支持JSON、SQL两种方式查询数据
1.4 应用场景
Druid擅长的部分
- 对于大部分查询场景可以亚秒级响应
- 事件流实时写入与批量数据导入兼备
- 数据写入前预聚合节省存储空间,提升查询效率
- 水平扩容能力强
- 社区活跃
是否需要使用Druid
- 处理时间序列事件
- 快速的聚合以及探索式分析
- 近实时分析亚秒级响
- 存储大量(TB级、PB级)可以预先定义若干维度的事件
- 无单点问题的数据存储
2 Druid 架构与原理
2.1 基础架构
- Coordinator node 进程管理群集上的数据可用性。从metastore中读取Segment的元数据,并决定哪些Segments需要被加载到集群中。使用ZooKeeper查看已经存在的历史节点,了解集群各个节点负载情况。创建一个ZK的条目告诉历史节点加载、删除、或者移动Segments。主要负责历史节点的数据负载均衡,以及通过规则管理数据的生命周期。协调节点告诉历史节点加载新数据、卸载过期数据、复制数据、 和为了负载均衡移动数据
Coordinator 是周期性运行的(由 druid.coordinator.period 配置指定,默认执行间隔为 60s); Coordinator 需要维护和 ZooKeeper 的连接,以获取集群的信息。Segment 和 Rule 的信息保存在元数据库中,所以也需要维护与元 数据库的连接。
- Overlord node 进程控制数据提取工作负载的分配。进程监视 MiddleManager 进程,并且是Druid 数据摄入的主节点。负责将提取任务分配给MiddleManagers 并协调 Segement 发布,包括接受、拆解、分配 Task,以及创建 Task 相关的锁,并返回 Task 的状态。
- Historical node 进程存储可查询数据。提供对Segment的数据查询服务。与ZooKeeper通信,上报节点信息,告知ZK自己拥有哪些Segments。从ZooKeeper中获取执行任务。加载生成好的数据文件,以供数据查询。Historical node是整个集群查询性能的核心所在,Historical 会承担绝大部分的 segment 查询
Historical 进程从 Deep Storage 中下载 Segment,并响应有关这些 Segment 的查询请求(这些请求来自Broker 进程); Historical 进程不处理写入请求; Historical 进程采用了无共享架构设计,它知道如何去加载和删除 Segment,以及如何基于 Segment 来响应查询。 即便 底层的深度存储无法正常工作,Historical 进程还是能针对其已同步的 Segments,正常提供查询服务。
- MiddleManager node 进程负责提取数据。及时摄入实时数据,生成 Segment 数据文件。
MiddleManager 进程是执行提交任务的工作节点。MiddleManagers 将任务转发给在不同 JVM 中运行的 Peon 进程。 MiddleManager、Peon、Task 的对应关系是,每个 Peon 进程一次只能运行一个Task 任务,但一个 MiddleManager 却可以管理多个 Peon 进程。
- Broker node 进程处理来自外部客户端的查询。负责将查询请求分发到历史节点和实时节点,并聚合这些节点返回的查询结果数据。Broker节点通过zooeeper知道Segment都存放在哪些节点上。接收客户端查询请求,并将这些查询转发给 Historicals 和 MiddleManagers。当 Brokers 从这些子查询中收到结果时,它们会合并这些结果并将它们返回给调用者。
Broker节点负责转发Client查询请求的; Broker通过zookeeper能够知道哪个Segment在哪些节点上, 将查询转发给相应节点; 所有节点返回数据后,Broker会将所有节点的数据进行合并,然后返回给Client;
- Router 进程是可选的进程,可以将请求路由到Broker、Coordinator、Overlords。
Router 进程可以在 Brokers、Overlords 和 Coordinators 进程之上,提供一层统一的 API网关。 Router 进程是 可选的,如果集群数据规模已经达到了 TB级别,需要考虑启用(druid.router.managementProxy.enabled=true)。 一旦集群规模达到一定的数量级,那么发生故障的概率就会变得不容忽视,而 Router 支持将请求只发送给健康的节点,避免 请求失败。 同时,查询的响应时间和资源消耗,也会随着数据量的增长而变高,而 Router 支持设置查询的优先级和负载均衡 策略,避免了大查询造成的队列堆积或查询热点等问题
根据线程的服务类型分为:
- Master:Coordinator & Overload 进程,管理数据可用性和数据摄取
- Data:Historical & MiddleManager,执行提取工作负载并存储所有可查询数据
- Query:Broker & Router,处理来自外部客户端的查询
2.2 外部依赖
- Deep Storage:深度存储,例如HDFS或者S3。不是用来存储查询数据的。而是作为数据的备份或者进程间数据交换。存放生成的 Segment 数据文件,并供历史服务器下载, 对于单节点集群可以是本地磁盘,而对于分布式集群一般是 HDFS。
Druid使用deep storage来做数据的备份,也作为在Druid进程之间在后台传输数据的一种方式。 当响应查询时,Historical首先从本地磁盘读取预取的段,这也意味着需要在deep storage和加载的数据的Historical 中拥有足够的磁盘空间。
- Metadata Storage:元数据存储,可以用RDBMS。存储 Druid 集群的元数据信息,如 Segment 的相关信息,一般使用 MySQL
- ZooKeeper:服务发现、leader选举、服务协调。为 Druid 集群提供以执行协调服务。如内部服务的监控,协调和领导者选举
Coordinator 节点的 Leader 选举 Historical 节点发布 Segment 的协议 Coordinator 和 Historical 之间 load / drop Segment 的协议 Overlord 节点的 Leader 选举 Overlord 和 MiddleManager 之间的 Task 管理
架构演进
Apache Druid
初始版本架构图
~ 0.6.0
(
2012~2013
)
0.7.0 ~ 0.12.0
(
2013~2018
)
Apache Druid
旧架构图
——
数据流转
Apache Druid
旧架构图
——
集群管理
0.13.0 ~
当前版本(
2018~now
)
Lambda 架构
从大的架构上看,
Druid
是一个
Lambda
架构。
Lambda
架构是由
Storm
的作者
Nathan Marz
提出的一个实时大数据处理框架。
Lambda
架构设计是为了在处理大规模数据时,同时发挥流处理和批处理的优势:
- 通过批处理提供全面、准确的数据
- 通过流处理提供低延迟的数据
从而达到平衡延迟、吞吐量和容错性的目的,
为了满足下游的即席查询,批处理和流处理的结果会进行合并
。
Lambda
架构包含三层,
Batch Layer
、
Speed Layer
和
Serving Layer
:
- Batch Layer:批处理层。对离线的历史数据进行预计算,为了下游能够快速查询想要的结果。由于批处理基于完整的历史数据集,准确性可以得到保证。批处理层可以用 Hadoop、Spark 和 Flink 等框架计算
- Speed Layer:加速处理层。处理实时的增量数据,这一层重点在于低延迟。加速层的数据不如批处理层那样完整和准确,但是可以填补批处理高延迟导致的数据空白。加速层可以用 Storm、Spark streaming 和 Flink 等框架计算
- Serving Layer:合并层。将历史数据、实时数据合并在一起,输出到数据库或者其他介质,供下游分析
流式数据的链路为:
Row data
→
Kafka
→
Streaming processor (Optional,
实时
ETL)
→
Kafka
(
Optional
)
→
Druid
→
Application / User
批处理数据的链路为:
Raw data
→
Kafka
(
Optional
)
→
HDFS
→
ETL process
(
Optional
)
→
Druid
→
Application / User
3.数据存储
- Druid中的数据存储在被称为DataSource中,DataSource类似RDBMS中的 Table
- 每个DataSource按照时间划分,每个时间范围称为一个Chunk(比如按天分区,则一个chunk为一天)
- 在Chunk中数据被分为一个或多个Segment Segment是数据实际存储结构,Datasource、Chunk只是一个逻辑概念
- Segment是按照时间组织成的Chunk,所以在按照时间查询数据时,效率非常高
- 每个Segment都是一个单独的文件,通常包含几百万行数据
数据分区:
- Druid处理的是事件数据,每条数据都会带有一个时间戳,可以使用时间进行分区
- 上图指定了分区粒度为为天,那么每天的数据都会被单独存储和查询
Segment
内部存储结构
- Druid采用列式存储,每列数据都是在独立的结构中存储
- Segment中的数据类型主要分为三种
1. 时间戳。每一行数据,都必须有一个timestamp,Druid一定会基于时间戳来分片
2.维度列。用来过滤filter或者组合groupby的列,通常是string、float、double、int类型
3.指标列。用来进行聚合计算的列,指定的聚合函数 sum、average 等
MiddleManager
节点接受到
ingestion
的任务之后,开始创建
Segment
:
- 转换成列存储格式
- 用bitmap来建立索引(对所有的dimension列建立索引)
- 使用各种压缩算法:
1. 所有的列使用LZ4压缩
2.所有的字符串列采用字典编码/标识以达到最小化存储
3.对位图索引使用位图压缩
Segment
创建完成之后,
Segment文件就是不可更改的,被写入到深度存储
(目的是为了防止
MiddleManager
节点宕机后,Segment
的丢失)。 然后
Segment
会加载到
Historical
节点,
Historical节点可以直接加载到内存中。
同时,
metadata store 也会记录下这个新创建的Segment的信息
,如结构,尺寸,深度存储的位置等等。 Coordinator节点需要这些元数据来协调数据的查找。
4.索引服务
- 索引服务:数据导入并创建 segments 数据文件的服务
- 索引服务是一个高可用的分布式服务,采用主从结构作为架构模式,索引服务由三大组件构成
1.overlord 作为主节点
2.middlemanager是从节点
3.peon用于运行一个task
索引服务架构图如下图所示:
索引服务由三部分组件组成:
- Overlord组件 负责创建task、分发task到middlemanager上运行,为task创建锁以及跟踪task运行状态并反馈给用户
- MiddleManager组件 作为从节点,负责接收主节点分配的任务,然后为每个task启动一个独立的JVM进程来完成具体的任务
- Peon(劳工)组件 由middlemanager启动的一个进程用于运行一个task任务
索引服务架构与 Yarn 的架构类似:
- Overlaod => ResourceManager,负责集群资源管理和任务分配
- MiddleManager => NodeManager,负责接受任务和管理本节点的资源
- Peon => Container,执行节点上具体的任务
Task
类型有很多,包括:
- index hadoop task:Hadoop索引任务,利用Hadoop集群执行MapReduce任务以完成segment数据文件的创建,适合体量比较大的segments数据文件的创建任务
- index kafka task:用于Kafka数据的实时摄入,通过Kafka索引服务可以在Overlord上配置一个KafkaSupervisor,通过管理Kafka索引任务的创建和生命周期来完成 Kafka 数据的摄取
- merge task:合并索引任务,将多个segments数据文件按照指定的聚合方法合并为一个segments数据文件
- kill task : 销毁索引任务,将执行时间范围内的数据从Druid集群的深度存储中删除
5.索引及压缩机制
Druid
的查询时延低性能好的主要是因为采用了五个技术点:
- 数据预聚合
- 列式存储、数据压缩
- Bitmap 索引
- mmap(内存文件映射方式)
- 查询结果的中间缓存
1、数据预聚合
- Druid通过一个roll-up的处理,将原始数据在注入的时候就进行汇总处理
- Roll-up可以压缩我们需要保存的数据量
- Druid会把选定的相同维度的数据进行聚合操作,可减少存储的大小
- Druid可以通过 queryGranularity 来控制注入数据的粒度。 最小的queryGranularity 是 millisecond(毫秒级)
Roll-up
聚合前:
Roll-up
聚合后:
2、位图索引
Druid
在摄入的数据示例:
- 第一列为时间,Appkey和Area都是维度列,Value为指标列
- Druid会在导入阶段自动对数据进行Rollup,将维度相同组合的数据进行聚合处理
- 数据聚合的粒度根据业务需要确定
按天聚合后的数据如下:
Druid
通过建立位图索引,实现快速数据查找。
Bitmap
索引主要为了加速查询时有条件过滤的场景。
Druid
在生成索引文件的时候,对每个列的每个取值生成对应的 Bitmap
集合。如下图所示:
索引位图可以看作是
HashMap<String, Bitmap>
- key就是维度的取值
- value就是该表中对应的行是否有该维度的值
以SQL查询为例:
1)boolean条件查询
select sum(value)
from tab1
where time='2020-10-01'
and appkey in ('appkey1', 'appkey2')
and area='北京'
执行过程分析:
- 根据时间段定位到segment
- Appkey in ('appkey1', 'appkey2') and area='北京' 查到各自的bitmap
1. (appkey1 or appkey2) and 北京
2.(110000 or 001100) and 101010 = 111100 and 101010 = 101000
3.符合条件的列为:第1行 & 第3行,这几行 sum(value) 的和为 40
2
)
group by
查询
select area, sum(value)
from tab1
where time='2020-10-01'
and appkey in ('appkey1', 'appkey2')
group by area
该查询与上面的查询不同之处在于将符合条件的列
- appkey1 or appkey2
- 110000 or 001100 = 111100
- 将第1行 到 第4行取出来
- 在内存中做分组聚合。结果为:北京:40、深圳:60