Druid 随笔
Druid 是一个开源的分布式数据存储,其核心设计来自于数据仓库、时间序列数据库、搜索系统的想法,为大范围的用例创造了一个统一的实时分析的系统。Druid 会将关键特性合并到它的摄取层,存储格式、查询层和和新架构中。
Druid 是一个开源的数据存储工具,为实时和历史数据的次秒级(多于一秒)查询设计。主要应用于对事件数据商业智能(Online analytical processing, OLAP)查询,Druid 提供低延迟(实时)的数据摄取、灵活的数据探索、快速的数据聚合。现有的 Druid 部署已经扩展到数万亿时间和 PB 级数据,目前是最常用的面向用户的分析应用。
关键特点
-
列式存储
Druid 独立的存储和压缩每一列,只需要读取特定查询所需的内容,这可以支持快速扫描、排名和聚合。
-
本地的搜索索引
Druid 为字符串创建倒排索引,以支持快速搜索和排序。
-
流式和批量摄取
支持 Apache Kafka, HDFS, AWS S3, stream processors 等现成连接器
-
灵活的 schema
Druid 可以处理变化的 schema 和嵌套数据
-
基于时间优化 partition
Druid 基于时间智能的对数据进行分区,因此基于时间的查询比传统数据库要快得多
-
支持 SQL
Druid 支持本机的 JSON 语言,还支持基于 HTTP 或者 JDBC 的 SQL。
-
水平扩展性
Druid 已经用户生产环境中,每秒接收数百万个事件,保存多年的数据并提供次秒级查询。
-
操作简单
只需要增加或删除服务器即可扩展或缩小规模,Druid 会自动平衡,容错架构通过服务器的故障进行路由。
优点
-
次秒级的 OLAP 查询
Druid 独特的架构能够支持快速的多维过滤、特殊属性分组、快速聚合等。
-
实时流摄入
Druid 使用无锁摄入,允许同时摄入并查询高维、高容量的数据集,在事件发生后即可探索他们。
-
分析应用程序
拥有许多为多租户设计的功能,面向用户的分析应用程序,可并发为上千万的用户提供服务。
-
成本效益高
Druid 在规模上是很有经济效益的,并且有许多降低成本的特性,用简单的配置平衡成本和性能。
-
高可用性
Druid 支持需要一直运行的 Saas 实现,Druid 支持滚动更新,所以在软件更新时数据查询仍然可用,在集群扩展或缩小期间也不会丢失数据
Druid vs Elasticsearch
ES 是一个基于 Apache Lucene 的搜索系统,它为无 schema 的文档提供全文搜索,并且提供对原始事件级数据的访问。ES 不断增加对分析和聚合的支持,ES 在数据摄取和聚合时使用的资源要比 Druid高得多。ES 并不支持在摄取时进行数据的汇总和上卷,所以也就不能将需要存储的数据进行压缩,导致 ES 需要更大的存储要求。
Druid 更注重于OLAP 工作流,以低成本优化高性能(快速聚合和摄取)。Druid 对于结构化数据支持一些基本搜索,但不支持全文搜索,也不支持完全非结构化数据。Measures 必须在 Druid schema 中定义,这样才能完成数据的汇总和上卷。
速览
集成
Druid 集成了许多开源数据技术的补充,包括 Kafka、Flink、Hadoop等。Druid 通常位于存储层/处理层与最终用户之间,作为查询层服务分析工作。
数据摄取
Druid 同时支持流和批量数据摄取,其连接到原始数据的源,通常是消息总线,如 Kafka(用于流数据加载)、HDFS(用于批量数据加载)。
在 indexing 过程中,Druid 将源中存储的原始数据转换成更易于阅读的格式(Druid 中被称作 segment)。
数据存储
和许多分析数据存储一样,Druid 将数据存储在 columns 中,根据 column 的不同类型,使用不同的压缩和编码算法,同样 Druid 也会根据 column 的不同类型建立不同的索引。
与搜索系统类似,Druid 为字符串类型的 column 建立倒排索引,用于快速搜索和筛选。与时间序列数据库类似,Druid 根据时间智能 partition,以快速的面向时间的查询。
和许多传统的系统不一样,Druid 可以在摄取阶段选择性的预聚合数据,这个预聚合数据的步骤称为 roll-up(上卷),可以显著节省存储空间。
数据查询
Druid 支持 JSON-over-HTTP 和 SQL 的方式查询数据,除了标准的 SQL 操作符,Druid 还支持独特的操作符,利用他的近似算法快速计数(count)、排名(rank)、分位数(quantiles)。
架构
Druid 拥有一个支持微服务的架构,可以被认为可拆卸的数据库。Druid 中的每个核心服务(数据摄取、查询、协调)都可以单独或联合部署在商业硬件上。
Druid 显式的命名每个主服务,允许操作员根据用例和工作负载对每个服务进行微调。例如,如果工作负载需要,可以将更多额资源分配给数据摄入,而将更少的资源分配给查询。
Druid 的服务如果失败,不会影响其他服务的运行。
操作
Druid 是为了全天候工作而设计的,因此 Druid 需要一些特性来保证运行时间和无数据丢失。
-
数据复制
所有的数据在 Druid 中被复制(使用可配置的次数),因此单服务器故障完全不影响查询。
-
服务间相互独立
Druid 显式的命名了它的主要服务,每个服务都可以根据用例进行微调,服务可以独立失败而不影响其他服务。例如摄取数据服务失败,Druid 不会继续加载新数据,但是之前的数据仍然可以提供查询服务。
-
自动数据备份
Druid 自动将索引数据备份到文件系统,例如 HDFS,可以失去整个集群,从数据备份中快速恢复。
-
滚动更新
可以通过滚动更新的方式更新整个集群,不会停机也不会对用户造成影响。所有的 Druid 版本都可以向后兼容以前的版本。
不适用的场景:
- 不支持使用主键对现有数据进行低延迟更新。Druid 支持流式插入,但是不支持流式更新,更新是通过后台批处理作业完成的。
- 对于构建离线报表系统,对于查询延迟不是很关心。(能够支持,但是不太适合完全可以用 MapReduce 等工具来做)
- 执行 big join 操作,接收很长时间才能完成的查询。(Druid 容易内存爆掉不出结果,更适用于 MapReduce 等工具来做)
设计
Druid 拥有多进程、分布式的架构,设计成 cloud 友好操作方便的特点。每个 Druid 进程可以独立的被配置和伸缩,这样可以最大化的集群的灵活性。Druid 还支持高容错,其中一个组件挂了不会影响其他组件。
进程和服务器
Druid 有多种进程类型:
- Coordinator 进程管理集群上的数据可用性,例如 unuse 就是由当前进程控制的。
- Overlord 进程控制分配数据摄入工作负载。
- Broker 进程处理外部客户端的查询。
- Router 进程是可选的,用于路由到 Coordinator、Overlord、Broker 的请求。
- Historical 进程存储可查询的数据。
- MiddleManager 进程负责摄取数据。
Druid 可以通过任意方式部署,但是为了便于部署,建议将他们组织成三种服务器类型:Master、Query、Data
- Master 运行 Coordinators 与 Overlord 进程,负责管理数据的摄取和可用性。
- Query 运行 Broker 和可选的 Router 进程,处理外部客户端的查询。
- Data 运行 Historical 和 MiddleManager 进程,执行数据摄取工作负载并存储所有可查询的数据。
外部依赖关系
为了能够利用现有的基础设施,Druid 还包含了三个外部依赖关系。
Deep Storage
Druid 的每个 server 都可以访问的共享文件存储。在分布式中,一般使用 S3、HDFS,单机部署中,一般使用本地文件系统。
Deep Storage 仅仅是摄入数据的备份,并不通过 Deep Storage 响应查询,响应查询的是 Historical 进程,其内存中会保存 segment 数据。但是当 Historical 挂掉时,需要从 Deep Storage 中读取数据重新启动。注意 Deep Storage 中的数据存储在磁盘中,Historical 的数据存储在内存中,是根据不同的作用、定位选择不同的存储方式。
Deep Storage 是提供弹性、高容错特性的重要设计之一,即使 Data node 节点故障时,Druid 也可以从 Deep Storage 中引导启动。
Metadata storage
Metadate Storage 持有各种共享系统元数据,例如 segment 使用信息和 task 信息。在分布式部署中,通常使用传统的关系型数据库存储,例如 PostgreSQL、MySQL;在单机部署中,通常使用本地存储的 Apache Derby 数据库。
ZooKeeper
用于内部的网络服务发现、Coordination、leader 选举
结构图
存储设计
Datasources and segments
Druid 数据被存储在 Datasources 中,每个 Datasource 被时间分区,也可以进一步使用属性分区。每个时间区间被称为 chunk,在一个 chunk 内,数据被分为一个或多个 segments,每个 segment 是一个单独的文件,通常包含几百万行数据。
每个 segment都是被 MiddleManager 创建的,在被创建时,是可变的和未提交的。segment 的创建包含以下步骤,目标是创建一个紧凑的,可以支持快速查询:
- 转化为列式存储
- 使用 bitmap 进行索引
- 使用不同的算法进行压缩
- 对字符串列进行id存储最小化的字典编码
- bitmap 索引使用 bitmap 压缩
- 根据列的类型感知压缩
segment 被周期性的提交和发布,此时他们被写入 deep storage,变得不可变,从 MiddleManager 转移到 Historical 进程中。segment 的条目同样被写入 metadata store 中,条目是关于 segment 的字描述元数据,包括 segment 的 schema、大小、在 deep storage 的位置等。这些条目信息被 Coordinator 使用去感知集群上的什么数据应该被提供。
索引和切换
indexing 是创建新 segment 的机制;handoff 是 segment 通过 Historical 进程被发布和开始提供服务的机制。这种机制在 indexing 侧是这样工作的:
segment 标识
segment 拥有一个四部分的标志符:
- 数据源名称
- 时间间隔
- 版本数字
- 分区名称
segment 版本控制
在 overwriting 时使用,append 时不变。
segment 生命周期
- metadata store
当 segment 完成构建后,元数据被存储到 metadata 数据库中,将 segment 的信息作为一条记录插入到 metadata sotre 中的操作叫做 publishing。这些记录有一个 tag 叫做 used,其控制segment 是否可被查询。在实时任务中,segment 在发布之前就可被提供查询,只有在 segment 完成时不会接收新增行时,才会被 publish。
- Deep sotrage
segment 被构造之后,就会被发送到 deep storage 中,这个操作发生在将 segment metadata 发送到 metadata store 之前。
- 查询可用性
segment 在某些 Druid server 上被查询可用,如实时任务和 Historical 进程。
查询进程
查询首先进入 Broker,Broker 会识别哪些 segment 拥有数据适合相应查询。Borker 随后将会识别哪些 Historical 和 MiddleManager 提供这些 segment 的查询,然后发送一个改写后的子查询到每个进程中。Historical 和 MiddleManager 将会接收查询,处理并提供结果。Borker 接收到结果并将他们 merge 带最终回答中。
Broker pruning 是 Druid 限制每个查询必须扫描的数据量的重要方式,但是不是唯一方式。Filter 比 pruning 更细粒度,每个 segment 内的索引结构允许 Druid在查看数据之前确定哪些行匹配 filter。一旦 Druid 知道哪些行可以匹配特定的查询,它就只访问查询需要的特定列,在这些列中,Druid 可以从一行跳到另一行,避免读取不匹配过滤器的数据。
所以 Druid 使用三种不同的技术来最大化查询性能:
- 裁剪每个查询使用的 segment
- 在每个 segment 中,使用索引识别哪些行需要被访问
- 在每个 segment 中,只读取与特定查询相关的特定行和列