1 架构设计
Druid是一个多进程、分布式架构。
每个Druid进程都可以独立配置和扩展,在集群上提供一定的灵活性。
这种设计还提升了容错率:一个组件的中断不会立即到影响其他组件。
1)进程与服务
Druid有若干不同类型的进程,简单描述如下:
- Coordinator进程:管理集群中数据可用性
- Overlord进程:控制数据摄取负载分配
- Broker进程:处理来自外部客户端的查询请求
- Router进程:是一个可选进程,可以将请求路由到Brokers、Coordinators和Overlords
- Historical进程:存储可查询的数据
- MiddleManager进程:负责摄取数据
Druid进程可以按照您喜欢的方式部署,但是为了便于部署,我们建议将它们组织成三种服务器类型:Master、Query和Data:
- Master:运行Coordinator和Overlord进程,管理数据可用性和摄取
- Query:运行Broker和可选的Router进程,处理来自外部客户端的请求
- Data:运行Historical和MiddleManager进程,执行摄取负载和存储所有可查询的数据
关于服务类型的详细介绍请参考这篇教程:a3 架构设计 进程与服务
2)外部依赖
除了内置的进程类型外,Druid同时有三个外部依赖,它们旨在利用现有的基础设施(如果有的话)。
即每个Druid服务器都可以访问的共享文件存储。
在集群部署中,通常使用一个像S3或HDFS这样的分布式对象存储,或者是一个网络挂载的文件系统。在单服务器部署中,通常使用本地磁盘。
Druid使用深度存储来存储任何已被系统接收的数据,且只使用深度存储作为数据备份,并作为在后台Druid进程之间传输数据的一种方式。
为了响应查询,Historical进程不选择在深层存储中读取数据,而是从本地磁盘读取预缓存的段。这意味着Druid在查询期间不需要访问深层存储,这有助于它提供尽可能低的查询延迟,这也意味着您必须在深层存储和所有Historical进程中都预留足够的磁盘空间来存储计划加载的数据。
深度存储是Druid弹性、容错设计的重要组成部分。即使每个数据服务器都丢失并重新配置,Druid也可以从深层存储启动。
元数据存储包含各种共享系统元数据,如数据段的可用性信息和任务信息。
在集群部署中,通常使用像PostgreSQL或MySQL这样的传统RDBMS;
在单服务器部署中,通常使用本地存储的Apache Derby数据库。
用于内部服务发现、协调和领导选举。
3)架构图
下图显示了建议使用的Master/Query/Data服务组织,以及查询动作、数据是如何在此体系结构中流动的:
4)存储设计
Druid数据被存储在datasources中,datasource类似于传统RDBMS中的表。
每一个数据源可以根据时间进行分区,可选地还可以进一步根据其他属性进行分区。每一个时间范围称为一个”块(chunk)“。在一个块中,数据被分为一个或者多个“段(segments)”。每个段是一个单独的文件,一般情况下由数百万条数据组成。
由于段被组织成时间块,因此通常情况我们将其理解为下图所示形式:
一个数据源可能有几个段到几十万甚至几百万个段。每个段都是在MiddleManager上创建的,但此时段是可变的和未提交的。段构建过程包括以下步骤,其目的在于生成支持快速查询的数据文件:
- 转换为列格式
- 构建位图索引
- 使用不同的算法进行压缩
- 字符串列id存储最小化的字典编码
- 对位图索引做压缩
- 所有列的类型感知压缩
段被周期性地提交、发布。此时,它们将被写入到深度存储且无法进行修改,同时从MiddleManager移动到Historical进程中。
有关段的信息也将写入到元数据存储中,这个信息是一个自描述的信息,包括段的schema、大小以及在深度存储中的位置,Coordinator可以根据这些信息来知道集群上应该有哪些数据是可用的。
-
索引和切换(Indexing and handoff)
索引(Indexing)是创建新段的一种机制,切换(handoff)是发布新段并开始由Historical进程提供服务的机制。
该机制在索引端的工作方式如下:
- 索引任务开始运行并生成新段。必须先在索引任务构建段之前确定段的标识符,对于一个追加数据类型的任务(例如Kafka任务或者其他追加模式的索引任务),这将通过调用Overlord的allocate API来在现有的段集合中添加一个新的分区。对于一个重写类型的任务(例如Hadoop任务,或者一个非追加模式的索引任务)这是通过锁定间隔并创建新的版本号和新的段集来完成的。
- 如果一个索引任务是实时任务(像kafka任务),那么段在此刻可以被立即查询,它是可用的,但是未发布。
- 索引任务完成对段的数据读取后,会将其推送到深层存储,然后通过将记录写入元数据存储来发布。
- 如果索引任务是实时任务,则此时它将等待Historical进程加载段。如果索引任务不是实时任务,它将立即退出。
在Coordinator和Historical方面:
- 对于新发布的段,Coordinator会周期性(默认是1分钟)的拉取元数据存储信息
- 当Coordinator发现一个段是发布且可以被使用的、但是不可用的状态时,它会选一个Historical进程来加载这个段
- Historical加载这个段并开始为其服务
- 此时,如果索引任务正在等待切换,它将退出
段都有一个由四部分组成的标识符,包含以下组件:
- 数据源名称
- 时间间隔(包含段的时间块,这与摄取时指定的
segmentGranularity
有关)
- 版本号(通常是ISO8601时间戳,对应于段集首次启动的时间)
- 分区号(整数,在datasource+interval+version中是唯一的,不一定是连续的)。
例如这个一个段标识符,数据源为clarity-cloud0
, 时间块为2018-05-21T16:00:00.000Z/2018-05-21T17:00:00.000Z
, 版本为2018-05-21T15:56:09.909Z
以及分区编号为1
:
clarity-cloud0_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T15:56:09.909Z_1
分区号为0的段(块中的第一个分区)忽略分区号,如下例所示,该段与上一个时间块位于同一时间块中,但分区号为0而不是1:
clarity-cloud0_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T15:56:09.909Z
上一节中描述到的版本号支持批处理模式覆盖。
在Druid中,如果您所做的只是附加数据,那么每个时间块只有一个版本。但是当您覆盖数据时,幕后发生的事情是,使用相同的数据源、相同的时间间隔、但更高的版本号创建一组新的段。这向Druid系统的其他部分发出了一个信号:旧版本应该从集群中删除,新版本应该替换它。
这个切换对用户来说似乎是瞬间发生的,因为Druid通过首先加载新数据(但不允许查询它)来处理这个问题,然后在新数据全部加载后,将所有新查询切换为使用这些新段。
每个段都有一个生命周期,涉及以下三个主要领域:
- 元数据存储:段的元数据(一个小的JSON,通常不超过几个KB)在段构建完成后存储在元数据存储中,将段的记录插入到元数据存储中称为发布(Publishing)。这些元数据记录中有一个
used
的布尔标识,控制着段是否可查询。被实时任务创建的段在发布之前是可用的,因为它们仅在完成之时发布,并且不再接受额外的数据行
- 深度存储:一旦构建了一个段,在将元数据发布到元数据存储之前就立刻将段数据文件推送到深度存储
- 可查询性:在某些Druid数据服务器上段是可以进行查询的,如实时任务或Historical进程
可以使用Druid SQL查询sys.segments
表检查当前活动段的状态,它包括以下标志:
is_published
: 如果段元数据已发布到元数据存储且used
是true的话,则为true
is_available
: 如果段当前可用于查询(实时任务或Historical进程),则为true
is_realtime
: 如果段仅在实时任务上可用,则为true。对于使用实时摄取的数据源,这通常从true开始,然后在发布和切换段时变为false
is_overshadowed
: 如果段已发布(used
设置为true),并且被某些其他已发布段完全覆盖,则为true。一般来说,这是一个过渡状态,处于该状态的段很快将其used
标志自动设置为false
5)查询处理
查询首先进入Broker,Broker首先鉴别哪些段可能与本次查询有关。
段的列表总是按照时间进行筛选和修剪的,当然也可能由其他属性,具体取决于数据源的分区方式。
然后,Broker将确定哪些Historical和MiddleManager为这些段提供服务、并向每个进程发送一个子查询。 Historical和MiddleManager进程接收查询、处理查询并返回结果,Broker将接收到的结果合并到一起形成最后的结果集返回给调用者。
Broker精简是Druid限制每个查询扫描数据量的一个重要方法,但不是唯一的方法。
对于比Broker更细粒度级别的精简筛选器,每个段中的索引结构允许Druid在查看任何数据行之前,找出哪些行(如果有的话)与筛选器集匹配。
一旦Druid知道哪些行与特定查询匹配,它就只访问该查询所需的特定列。
在这些列中,Druid可以从一行跳到另一行,避免读取与查询过滤器不匹配的数据。
因此,Druid使用三种不同的技术来最大化查询性能:
- 精简每个查询访问的段
- 在每个段中,使用索引标识必须访问哪些行
- 在每个段中,只读取与特定查询相关的特定行和列