Druid是一个多进程,分布式的架构,它被设计为对云部署友好的并且操作简单。每一种Druid进程类型都可以配置的,并且可独立扩展,在我们的集群里面能给我们提供最大的灵活性。这种设计还增强了容错性:一台机器的宕机不会立即影响其他的机器。
进程和服务
Druid有好几种进程类型,简要描述如下:
Coordinator :该进程管理集群数据的可用性
Overlord :该进程控制数据摄入工作的分配
Broker:该进程处理外部的查询
Router:该进程是可选的,它能路由请求到broker,coordinator 和overload
Historical:该进程存储可查询的数据
MiddleManager:该进程负责摄入数据
Druid进程可以以任意的方式部署,不过为了容易部署,我们建议把他们部署为三个服务:master,query,data
master: 运行 coordinator 和overload进程,管理数据可用性和摄入
query:运行broker和可选的路由进程,处理外部的查询
data:运行historical 和 middlemanager进程,执行摄入工作和存储所有的可查询数据
更详细的进程和服务器介绍,参考:druid 进程和服务器
外部依赖
除了它自己的进程类型,druid还需要额外的三个依赖:它们旨在利用现有的基础设置,如果有的话
底层存储
每一个Druid服务都能访问共享的文件存储。在集群模式下,典型的采用分布式对象存储,比如S3或者HDFS或者网络文件系统。单机模式下,它就采用本地磁盘。Druid把所有摄入到系统里面的数据都存储在底层存储里。
Druid仅仅使用底层存储来备份我们的数据,并通过底层存储来在Druid进程直接传输数据。对于查询的结果,historical进程不是读取存储,替代的是通过从它们的本地磁盘预读取segment进行响应。这意味着Druid从不需要在查询的时候访问底层存储,这能帮忙它提供低延迟的查询。这也意味着我们必须有充足的磁盘空间来同时满足底层存储和通过historical进程加载我们需要查询的数据
底层存储是 Druid灵活的,容错的设计的重要部分,即使每个数据服务器都丢失并重新配置,Druid也可以从底层存储启动。
详情请见 deep storage
元数据存储
元数据存储的有各种系统共享元数据比如可用segment信息和任务信息。集群模式下,它会使用像postgreSQL或者MySQL的关系型数据库。单机部署的时候,会使用本地存储的 Apache Derby 数据库
详情请看 metadata storage
zookeeper
用来作为内部服务发现,协调和leader选举
详情请见zookeeper
架构图
下图使用推荐的master/query/data服务器结构展示了查询和这个架构的数据流向,
DataSource 和 segment
Druid数据保存在DataSource里面,它类似于关系型数据库的表。每个DataSource会根据时间分区,可选的通过其他属性进一步分区。每一个时间范围被称作chunk(例如:一天,如果我们的DataSource按天分区)。在chunk里面,数据被分区到一个或多个segment里面。每个segment是一个文件,通常包含多达几百万行的数据。因为segment是按照时间组织到chunk里面,有时候可以认为segment是像下面的时间轴一样:
一个DataSource可以拥有成百上千个segment。一开始每个segment都是由middlemanager创建的,从那一刻起,它就是可变的和不可提交的。segment的创建过程包括下面几个步骤,旨在生成一个紧凑的数据文件和支持快速查询:
转换成列格式
生成bitmap索引
使用各种算法进行压缩:
1、字典编码字符串的列,使用ID进行最小化存储
2、使用位图压缩位图索引
3、所有的列按照对应的类型压缩
segment被周期性的提交和发布。此时它们被写入到底层存储,变成不可变的,并从middlemanager移动到historical 进程。segment的入口信息也会被写入metadata store。入口信息是这个segment自描述的metadata,包括segment的概述,它的大小,和它在底层存储的位置。这些入口信息就是coordinator用来获取集群里面有哪些数据。
关于segment文件的格式,参考 segment files。
索引和切换
索引是新的segment创建的机制,切换是segment被发布并且被historical 进程使用的机制。该机制在索引端的工作方式如下:
索引任务开始运行和创建一个新的segment。它在开始创建的时候就需要确定segment的唯一标识。对于正在追加的任务(比如kafka任务,或者处于追加模式的指标任务)通过调用Overlord的allocate API在已经存在的segment集合里面添加一个新的分区。对于处于重写的任务(比如Hadoop任务,或者不处于追加模式的索引任务)通过锁来创建新版本号和新的segment集合
如果索引任务是实时任务(像kafka任务),这个segment立即就可以被查询,它是可用的,但是没有发布。
当索引任务已经从segment里面读取了数据,它把这些数据推向底层存储,并往metadata存储里面写一条记录来发布它
如果索引任务是一个实时任务,它将等待historical 进程加载segment。如果一个索引任务不是实时的,它会立即存在。
在coordinator和historical端是这样工作的:
coordinator周期性的拉取新发布的segment的metadata 存储数据(默认是一分钟)
当coordinator发现一个已经发布的segment就使用它,当它不可用的时候,coordinator会选择让historical进程去加载这个segment并指示historical这样做
historical加载segment并开始使用它
此时如果索引任务在等待切换,它将会退出。
segment标识
segment的标识有四个组成部分:
DataSource名称
时间间隔(segment包含的时间片,它和在摄入时间segmentGranularity声明的相一致)
版本号(一般是当segment集合第一次启动的时候的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
这个标识符标识一个segment:DataSource:clarity-cloud0在时间片:2018-05-21T16:00:00.000Z/2018-05-21T17:00:00.000Z,版本号:2018-05-21T15:56:09.909Z ,分区号:1
segment的版本号
也许你很好奇前面提到的版本号是什么,或者也许你没有,那样更好,你就可以跳过这一节了。
这是为了支持批量重写模式。在druid里面,如果所有数据的都是追加的,那么对于每个时间片来说都是一个单一的版本。但是当我们重写数据的时候,就会发生相同的DataSource,相同的时间间隔,却以一个搞的版本号创建的新的segment集合。这是向druid系统的其他部分发送一个信号:旧版本应该从集群中删除,使用新版本替换它。
对用户来说这个切换是瞬间完成的,因为druid首先加载新数据(但是不会立即允许查询),然后新数据被加载,然后所有的新查询都会使用新的segment。最后几分钟之后删除旧的segment。
segment的生命周期
每个segment都有生命周期,它主要参与下面三个主要阶段:
1、metadata 存储:一旦segment被构造出来,segment 元数据(通常是只有几KB的JSON格式内容)就被存储在metadata store里面。把往metadata store插入segment记录的过程叫做发布。这些元数据记录有一个叫做used的Boolean类型标志,这个标志控制这segment是否可以被查询。实时任务创建的segment在它发布之前就可以被查询,这是因为这些segment已经完成并且不会再接收任何额外的数据的时候才会被发布。
2、底层存储:当一个segment被构造之后,segment的数据文件就会被推送到底层存储。在元数据被发布到metadata存储之前就会立即发生。
3、查询的可用性:在druid 数据服务上进行查询的时候,segment是可用的,比如实时任务或者historical 进程。
我们可以使用druid SQL的sys.segments表来检查当前活动的segment的状态,它包括下列标志:
is_published: 当segment 元数据已经被发布到metadata 存储并且 used 为true的时候为true
is_available: 在实时任务或在historical 进程,当segment可以被查询的时候为true
is_realtime:当segment在实时任务上面可用的时候为true。如果DataSource使用实时摄入,在一开始就是true,当segment发布和切换的时候变成false
is_overshadowed:当segment已经发布(used 设置为true)并且被其他的已发布的segment遮挡的时候。通常这是一个临时状态,很快这个segment的used标志自动设置为false。
查询处理
查询首先通过broker,broker会定位到哪些segment将会被查询。segment会根据时间进行精简,也可能会按我们的DataSource分区设置根据其他属性精简。随后broker会定位到哪些historical和middlemanager处理这些segment,并发送重写过的子查询给这些进程。historical/middlemanager进程会接收这些查询,处理它们并返回结果。broker会接收到这些结果并将结果进行合并为最终结果,然后再返回给最初的调用者。
broker精简是druid限制每个查询必须扫描数据的总数的非常重要的方式,但是不是唯一的方式。比broker使用精简更细粒度的控制是过滤器,每个segment中的索引结构允许Druid在查看任何数据行之前,找出哪些行(如果有的话)与过滤器匹配。一旦Druid知道哪些行与特定查询匹配,它就只访问该查询所需的特定列。在这些列中,Druid可以从一行跳到另一行,避免读取和查询与过滤器不匹配的数据。
druid使用三种不同的技术来最大化查询性能:
对每个查询进行segment精简
在每个segment,使用索引来定位哪些行会被访问
在每个segment,对特定的查询只访问特定的行和列