Druid采用多进程,分布式的架构;其架构易于运维及部署,便于部署在云环境中。每个Druid进程都可以被独立地配置和横向扩展,这种设计一方面赋予了Druid集群最大的灵活性和可扩展性,另一方面以提供了更高的容错性:一个组件的中断不会立即影响其他组件。
架构概述
1、架构图
2、进程和服务器
Druid主要包括以下几种进程类型:
- 协调进程(Coordinator):管理集群上的数据可用性。 该进程监控Data服务器上的Historical进程,负责将segments分配给特定的服务器,并确保segments在Historical进程间负载均衡。
- 统治进程(Overlord):控制数据摄入工作负载的分配。 该进程负责协调Data服务器上的MiddleManager进程,同时也是数据摄入Druid的控制器,负责将数据摄入任务分配给MiddleManager并协调发布新的Segment数据。
- 查询进程(Broker):处理外部客户端的查询请求。 该进程负责从外部客户端接收查询请求,并把这些查询请求拆分成子查询后转发到Data服务器上的Historical和MiddleManager进程。当Broker接收到这些子查询的结果后,会合并子查询结果并将结果返回给调用者。终端用户实际上都是和Broker做交互,而不会直接查询Historical和MiddleManager。
- 路由进程(Router):可选进程,可将请求路由到Brokers, Coordinators和Overlords。 该进程是为Brokers,Overlards, Coordinators提供统一网关服务的可选进程。用户也可以不使用Router而将请求直接发送给Brokers,Overlards和Coordinators。此外,该进程上运行了Druid Console,一个datasources、segments、tasks、数据进程(Historicals和MiddleManagers),以及coordinator动态配置的管理界面。用户可以在该Console上运行Sql或本地Druid查询。
- 历史进程(Historical):存储可查询的数据。 该进程是处理存储和查询“历史”数据(历史数据包括所有已经被committed的流数据)的主要工具。 历史进程从深度存储中下载Segment数据并响应有关这些Segment的查询。该进程不支持写入操作。
- 中间管理者进程(MiddleManager):负责摄入数据。 该进程负责将数据摄入到集群中,负责从外部数据源中读取数据,也负责发布新的Segment。
- 劳工进程(Peon):Peon进程是由中间管理器生成的任务执行引擎。每个Peon运行一个单独的JVM,并负责执行单个任务。Peon进程总是和产生它们的MiddleManager进程在同一个主机上运行。
Druid进程可以任意部署,但是为了便于部署,我们建议将它们组织成三种服务器类型:master、query和data。
- Master服务器:运行Coordinator和Overlord进程,管理数据可用性和摄入。 它负责启动新的摄入作业,并协调“Data服务器”上的数据可用性。
- Query服务器:运行Broker和可选的Router进程,处理外部客户端的查询请求。 它是提供用户和客户端应用程序与之交互的终端,将查询路由到数据服务器或其他查询服务器(也可以选择代理Master服务器请求)。
- Data服务器:运行Historical和MiddleManager进程,执行摄入工作并保存所有可查询数据。
3、外部依赖
除了上述进程类型以外,Druid还有三种外部组件,它们可以用来利用已有的基础设施:
- 深度存储(Deep Storage):可以被所有Druid服务器访问的共享文件存储。通常是一个类似S3或HDFS的分布式存储系统,或者是一个可以mount的网络文件系统。Druid利用Deep Storage来存储所有摄入的数据。
- 元数据存储(Metadata Storage):包含各种共享系统的metadata如segment可用性信息和task信息,这通常是一个传统的RDBMS如PostgreSQL或MySQL。
- Zookeeper:用于内部服务的发现,协调以及leader选举。
- 这种设计的初衷是为了让Druid在大规模生产环境中能够被容易的运维。比如,将Metadata Storage和Deep Storage从其他部件中分离能够让Druid系统是高度容错的:即使所有的Druid服务器都挂了,我们也可以很容易的从Metadata Storage和Deep Storage中重新启动一个集群。
查询及数据摄入架构详述
如上图所示,从数据流转的角度来看,数据从架构图的左侧进入系统,分为实时流数据与批量数据。实时流数据会被实时节点(Realtime Node)消费,然后实时节点将生成的Segment数据文件上传到数据文件存储库(DeepStorage);而批量数据经过Druid集群消费后,会被直接上传到数据文件存储库(DeepStorage)。同时,查询节点会响应外部的查询请求,并分别从实时节点(Realtime Node)与历史节点(Historical Node)查询到的结果合并后返回。
Realtime Nodes负责消费实时数据,实时数据首先会被直接加载进实时节点内存中的堆结构缓存区,当条件满足时,缓存区里的数据会被冲写到硬盘上形成一个数据块(Segment Split),同时实时节点又会立即将新生成的数据块加载到内存中的非堆区,因此无论是堆结构缓存区还是非堆区里的数据,都能够被查询节点(Broker Node)查询。实时节点数据块的生成示意图如下图所示:
同时,实时节点会周期性的将磁盘上同一个时间段内生成的所有数据块合并为一个大的数据块(Segment),即Segment Merge操作。合并好的Segment会立即被实时节点上传到数据文件存储库(Deep Storage)中,随后协调节点(Coordinator Node)会指导一个历史节点(Historical Node)去文件存储库将新生成的Segment下载到其本地磁盘中。当历史节点成功加载到Segment后,会通过分布式协调服务(Coordination)在集群中声明其从此刻开始负责提供该Segment的查询,当实时节点收到该声明后也会立即向集群声明其不再提供该Segment的查询,接下来查询节点会转从该历史节点查询此Segment的数据。而对于全局数据来说,查询节点会同时从实时节点(少量当前数据)与历史节点(大量历史数据)分别查询,然后做一个结果的整合,然后再返回给用户。Druid的这种架构在一定程度上借鉴了命令查询职责分离模式(Command Query Responsibility Segregation,CQRS)。
1、实时节点
实时节点主要负责即时摄入实时数据,以及生成Segment数据文件,它拥有超强的数据摄入速度。
实时节点通过Firehose来消费实时数据,Firehose是Druid中消费实时数据的模型,可以有不同的具体实现。同时,实时节点会通过另一个用于生成Segment数据文件的模块Plumber按照指定的周期,按时将本周期内生产出的所有数据块合并成一个大的Segment数据文件。
Segment数据文件从制造到传播要经历一个完整的流程。步骤如下:
- 实时节点生产出Segment数据文件,并将其上传到DeppStorage中。
- Segment数据文件的相关元数据信息被存放到MetaStore(即MySQL)里。
- Master节点(即Coordinator节点)从MetaStore里得知Segment数据文件的相关元数据信息后,将其根据规则的设置分配给符合条件的历史节点。
- 历史节点得到指令后会主动从DeepStorage中拉取指定的Segment数据文件,并通过Zookeeper向集群声明其负责提供该Segment数据文件的查询服务。
- 实时节点丢弃该Segment数据文件,并向集群声明其不再提供该Segment数据文件的查询服务。
实时节点拥有很好的可扩展性与高可用性。我们可以使用一组实时节点组成一个Kafka Consumer Group来共同消费同一个Kafka Topic的数据,各个节点会负责独立消费一个或多个该Topic所包含的Partition数据,并保证同一个Partition不会被多于一个的实时节点消费。当每一个实时节点完成部分数据的消费后,会主动将数据消费进度(Kafka Topic Offset)提交到Zookeeper集群。这样,当这个节点不可用时,该Kafka Consumer Group会立即在组内对所有可用节点进行Partition的重新分配,接着所有节点将会根据记录在Zookeeper集群里的每一个Partition的Offset来继续消费未曾被消费的数据,从而保证所有数据在任何时候都会被Druid集群至少消费一次,进而实现了这个角度上的高可用性。同理,当集群中添加新的实时节点时也会触发相同的事件,从而保证了实时节点能够轻松实现线性扩展。
2、历史节点
历史节点负责加载已生成好的数据文件以提供数据查询。Druid的数据文件不可更改,历史节点的工作就是专注于提供数据查询。
历史节点在启动的时候,首先会检查自己的本地缓存中已经存在的Segment数据文件,然后从DeepStorage中下载属于自己但目前不在自己本地磁盘上的Segment数据文件。无论是何种查询,历史节点都会首先将相关Segment数据文件从磁盘加载到内存,然后再提供查询服务,如下图所示:
原则上历史节点的查询速度与其内存空间大小和所负责的Segment数据文件大小之比成正比关系。
历史节点拥有极佳的可扩展性与高可用性,新的历史节点被添加后,会通过Zookeeper被协调节点发现,然后协调节点将会自动分配相关的Segment给它;原有的历史节点被移出集群后,同样会被协调节点发现,然后协调节点会将原本分配给它的Segment重新分配给其他处于工作状态的历史节点。
3、查询节点
查询节点对外提供数据查询服务,并同时从实时节点与历史节点查询数据,合并后返回给调用方。在常规情况下,Druid集群直接对外提供查询的节点只有查询节点,而查询节点会将从实时节点与历史节点查询到的数据合并后返回给客户端。如下图所示。
Druid使用了Cache机制来提高自己的查询效率。Druid提供了两类介质作为Cache以供选择:
- 外部Cache,比如Memcached;
- 本地Cache,比如查询节点或历史节点的内存作为Cache;
如果用查询节点的内存作为Cache,查询的时候回首先访问其Cache,只有当不命中的时候才会去访问历史节点与实时节点以查询数据。使用查询节点的缓存如下图所示:
一般一个集群只需要一个查询节点即可,但为防止单个节点失败导致无查询节点可用的情况,通常会多加一个查询节点到集群中,同时,可使用Nginx作为gateway来完成负载均衡。
4、协调节点
协调节点负责历史节点的数据负载均衡,以及通过规则管理数据的生命周期。
对于整个Druid集群来说,并没有真正意义上的Master节点,因为实时节点与查询节点能自行管理并不听命于任何其他节点;对于历史节点来说,协调节点便是它们的Master节点,因为协调节点将会给历史节点分配数据,完成数据分布在历史节点间的负载均衡。当协调节点不可访问时,历史节点虽然还能向外提供查询服务,但已经不接收新的Segment数据了。
Druid利用针对每个DataSource设置的规则(Rule)来加载(Load)或丢弃(Drop)具体的数据文件以管理数据生命周期。可以对一个DataSource按顺序添加多条规则,对于一个Segment数据文件来说,协调节点会逐条检查规则,当碰到当前Segment数据文件符合某条规则的情况时,协调节点会立即命里历史节点对该Segment数据文件执行这条规则——加载或丢弃,并停止检查余下的规则,否则继续检查下一条设置好的规则。
Druid允许用户对某个DataSource定义其Segment数据文件在历史节点中的副本数量,默认为1,即仅有一个历史节点提供某Segment数据文件的查询,存在单点问题。
索引服务架构详述
1、索引服务
除了通过实时节点生产出Segment数据文件外,Druid还提供了一组名为索引服务(Indexing Service)的组件同样能够制造Segment数据文件。
索引服务包含一组组件,并以主从结构作为其架构方式,其中统治节点(Overlord Node)为主节点,中间管理者(MiddleManager)为从节点,索引服务架构如下图所示:
2、统治节点
统治节点作为索引服务的主节点,对外负责接收任务请求,对内负责将任务分解并下发到从节点即中间管理者上。统治节点有两种运行模式:
- 本地模式(Local Mode):默认模式。本地模式下的Overload不仅负责集群的任务协调分配工作,还会负责启动一些苦工(Peon)来完成具体的任务。
- 远程模式(Remote Mode):该模式下,Overload和MiddleManager运行在不同的节点上,它仅负责任务的协调分配工作,不负责完成具体的任务。
Overload提供了RESETful的访问形式,所以客户端可以通过HTTP POST形式向请求节点提交任务。
- http://<OVERLORD_IP>:<port>/druid/indexer/v1/task //提交任务
- http://<OVERLORD_IP>:<port>/druid/indexer/v1/task/{task_id}/shutdown //杀死任务
3、中间管理者与苦工
中间管理者就是索引服务的工作节点,负责接收统治节点分配的任务,然后启动相关苦工即独立的JVM来完成具体的任务。
数据查询
Druid原生查询是采用JSON格式,通过HTTP传送。Druid不支持标准的SQL语言查询。Imply.io的PlyQL可以支持SQL查询。
Broker接收到查询请求后,请求处理过程如下:
- Broker首先会检查哪些segment拥有与该查询相关的数据,这时候会通过查询的时间以及datasource的其他分区信息来裁剪掉没有用的segment;
- Broker会检查哪些Historical和Midd摄入leManager进程拥有这些segment,并将重写的子查询请求发送给相应的进程;
- Historical/MiddleManager进程执行所收到的子请求,处理完以后返回结果给borker;
- Broker收到结果以后合并所有查询结果,将合并后的查询结果返回给最初的请求者。
通过查询时间裁剪数据是Druid用来减少每个查询所需要扫描数据量的重要方式,Druid还通过其他方式来进一步减少查询所需要扫描的数据:查询所用的过滤条件是比时间更细粒度的,Druid会进一步利用内部的索引结果来找出符合过滤条件的行;然后找出需要被访问的列,在处理列时只会扫描符合条件的行,避免访问那些不符合过滤条件的数据。
总之,Druid采用三种不同的技术来最大化查询性能:
- 根据查询时间,裁剪不需要被访问到的segment;
- 在segment内部,使用索引识别符合查询条件的行;
- 在segment内部,只访问那些被查询的列在符合条件的行上的数据。
参考资料:
- Druid 的官方网站
- Druid.IO介绍系列之汇总篇
- Druid实时大数据分析原理与实践,欧阳辰