Druid原理架构
Druid原理架构
张欢19933 发表于4个月前
Druid原理架构
  • 发表于 4个月前
  • 阅读 54
  • 收藏 1
  • 点赞 0
  • 评论 0

腾讯云 技术升级10大核心产品年终让利>>>   

简介

Druid是一个为在大数据集之上做实时统计分析而设计的开源数据存储。这个系统集合了一个面向列存储的层,一个分布式、shared-nothing的架构,和一个高级的索引结构,来达成在秒级以内对十亿行级别的表进行任意的探索分析。一个开源的、分布式、列存储、实时分析的数据存储。

特性

亚秒级查询:druid提供了快速的聚合能力以及亚秒级的OLAP查询能力,多租户的设计,是面向用户分析应用的理想方式
实时数据注入:druid支持流数据的注入,并提供了数据的事件驱动,保证在实时和离线环境下事件的实效性和统一性,也可以通过mapreduce离线注入数据
可扩展的PB级存储:druid集群可以很方便的扩容到PB的数据量,每秒百万级别的数据注入。即便在加大数据规模的情况下,也能保证时其效性
多环境部署:druid既可以运行在商业的硬件上,也可以运行在云上。它可以从多种数据系统中注入数据,包括hadoop,spark,kafka,storm和samza等

适用场景

第一:适用于清洗好的记录实时录入,但不需要更新操作
第二:支持宽表,不用join的方式(换句话说就是一张单表)
第三:可以总结出基础的统计指标,可以用一个字段表示
第四:对时区和时间维度(year、month、week、day、hour等)要求高的(甚至到分钟级别)
第五:实时性很重要
第六:对数据质量的敏感度不高
第七:用于定位效果分析和策略决策参考

主要概念

Realtime节点

实时摄取数据,它们负责监听输入数据流并让其在内部的Druid系统立即获取,Realtime节点同样只响应broker节点的查询请求,返回查询结果到broker节点。旧数据会被从Realtime节点转存至Historical节点。

  • 实时节点封装了导入和查询事件数据的功能,经由这些节点导入的事件数据可以立刻被查询。
  • 实时节点只关心一小段时间内的事件数据,并定期把这段时间内收集的这批不可变事件数据导入到Druid集群里面另外一个专门负责处理不可变的批量数据的节点中去。
  • 实时节点通过Zookeeper的协调和Druid集群的其他节点协调工作。实时节点通过Zookeeper来宣布他们的在线状态和他们提供的数据

  • 实时节点为所有传入的事件数据维持一个内存中的索引缓存, 随着事件数据的传入,这些索引会逐步递增,并且这些索引是可以立即查询的,查询这些缓存于JVM的基于堆的缓存中的事件数据,Druid就表现得和行存储一样
  • 为了避免堆溢出问题,实时节点会定期地、或者在达到设定的最大行限制的时候,把内存中的索引持久化到磁盘去
  • 这个持久化进程会把保存于内存缓存中的数据转换为基于列存储的格式,所有持久化的索引都是不可变的,并且实时节点会加载这些索引到off-heap内存中使得它们可以继续被查询
  • 上图实时节点缓存事件数据到内存中的索引上,然后有规律的持久化到磁盘上。在转移之前,持久化的索引会周期性地合并在一起。查询会同时命中内存中的和已持久化的索引
  • 所有的实时节点都会周期性的启动后台的计划任务搜索本地的持久化索引,后台计划任务将这些持久化的索引合并到一起并生成一块不可变的数据,这些数据块包含了一段时间内的所有已经由实时节点导入的事件数据,我们称这些数据块为”Segment”。在传送阶段,实时节点将这些segment上传到一个永久持久化的备份存储中,通常是一个分布式文件系统,例如S3或者HDFS,Druid称之为”Deep Storage”。

例如:

  • 节点启动于13:47,并且只会接受当前小时和下一小时的事件数据。当事件数据开始导入后,节点会宣布它为13:00到14:00这个时间段的Segment数据提供服务
  • 每10分钟(这个时间间隔是可配置的),节点会将内存中的缓存数据刷到磁盘中进行持久化,在当前小时快结束的时候,节点会准备接收14:00到15:00的事件数据,一旦这个情况发生了,节点会准备好为下一个小时提供服务,并且会建立一个新的内存中的索引。
  • 随后,节点宣布它也为14:00到15:00这个时段提供一个segment服务。节点并不是马上就合并13:00到14:00这个时段的持久化索引,而是会等待一个可配置的窗口时间,直到所有的13:00到14:00这个时间段的一些延迟数据的到来。这个窗口期的时间将事件数据因延迟而导致的数据丢失减低到最小。
  • 在窗口期结束时,节点会合并13:00到14:00这个时段的所有持久化的索引合并到一个独立的不可变的segment中,并将这个segment传送走,一旦这个segment在Druid集群中的其他地方加载了并可以查询了,实时节点会刷新它收集的13:00到14:00这个时段的数据的信息,并且宣布取消为这些数据提供服务。
  • 在流式处理领域,有两种数据处理模式,一种为Stream Push,另一种为Stream Pull。

    Stream Pull 
    如果Druid以Stream Pull方式自主地从外部数据源拉取数据从而生成Indexing Service Tasks,我们则需要建立Real-Time Node。Real-Time Node主要包含两大“工厂”:一个是连接流式数据源、负责数据接入的Firehose(中文翻译为水管,很形象地描述了该组件的职责);另一个是负责Segment发布与转移的Plumber(中文翻译为搬运工,同样也十分形象地描述了该组件的职责)。在Druid源代码中,这两个组件都是抽象工厂方法,使用者可以根据自己的需求创建不同类型的Firehose或者Plumber。Firehose和Plumber给我的感觉,更类似于Kafka_0.9.0版本后发布的Kafka Connect框架,Firehose类似于Kafka Connect Source,定义了数据的入口,但并不关心接入数据源的类型;而Plumber类似于Kafka Connect Sink,定义了数据的出口,也不关心最终输出到哪里。

    Stream Push 
    如果采用Stream Push策略,我们需要建立一个“copy service”,负责从数据源中拉取数据并生成Indexing Service Tasks,从而将数据“推入”到Druid中,我们在druid_0.9.1版本之前一直使用的是这种模式,不过这种模式需要外部服务Tranquility,Tranquility组件可以连接多种流式数据源,比如Spark-Streaming、Storm以及Kafka等,所以也产生了Tranquility-Storm、Tranquility-Kafka等外部组件。

Historical 节点

对“historical”数据(非实时)进行处理存储和查询的地方。historical节点响应从broker节点发来的查询,并将结果返回给broker节点。它们在Zookeeper的管理下提供服务,并使用Zookeeper监视信号加载或删除新数据段.。

Coordinator Nodes会定期(默认为1分钟)去同步元信息库,感知新生成的Segment,将待加载的Segment信息保存在Zookeeper中在线的Historical Nodes的load queue目录下,当Historical Node感知到需要加载新的Segment时,首先会去本地磁盘目录下查找该Segment是否已下载,如果没有,则会从Zookeeper中下载待加载Segment的元信息,此元信息包括Segment存储在何处、如何解压以及如何如理该Segment。Historical Node使用内存文件映射方式将index.zip中的XXXXX.smoosh文件加载到内存中,并在Zookeeper中本节点的served segments目录下声明该Segment已被加载,从而该Segment可以被查询。对于重新上线的Historical Node,在完成启动后,也会扫描本地存储路径,将所有扫描到的Segment加载如内存,使其能够被查询。

历史节点从deep storage下载不可变的segment。segment在可以被查询之前必须要先加载到内存中

Broker 节点

接收来自外部客户端的查询,并将这些查询转发到Realtime和Historical节点。当Broker节点收到结果,它们将合并这些结果并将它们返回给调用者。由于了解拓扑,Broker节点使用Zookeeper来确定哪些Realtime和Historical节点的存在。

  • Broker节点扮演着历史节点和实时节点的查询路由的角色。
  • Broker节点知道发布于Zookeeper中的关于哪些segment是可查询的和这些segment是保存在哪里的,Broker节点就可以将到来的查询请求路由到正确的历史节点或者是实时节点,
  • Broker节点也会将历史节点和实时节点的局部结果进行合并,然后返回最终的合并后的结果给调用者

缓存:Broker节点包含一个支持LRU失效策略的缓存。这个缓存可以使用本地堆内存或者是一个外部的分布式 key/value 存储,例如Memcached

  • 每次Broker节点接收到查询请求时,都会先将查询映射到一组segment中去。这一组确定的segment的结果可能已经存在于缓存中,而不需要重新计算。
  • 对于那些不存在于缓存的结果,Broker节点会将查询转发到正确的历史节点和实时节点中去,一旦历史节点返回结果,Broker节点会将这些结果缓存起来以供以后使用,这个过程如下图所示
  • 注意:实时数据永远不会被缓存,因此查询实时节点的数据的查询请求总是会被转发到实时节点上去。实时数据是不断变化的,因此缓存实时数据是不可靠的

  • 上图:结果会为每一个segment缓存。查询会合并缓存结果与历史节点和实时节点的计算结果
  • 缓存也可作为数据可用性的附加级别。在所有历史节点都出现故障的情况下,对于那些命中已经在缓存中缓存了结果的查询,仍然是可以返回查询结果的

可用性:在所有的Zookeeper都中断的情况下,数据仍然是可以查询的。如果Broker节点不可以和Zookeeper进行通信了,它会使用它最后一次得到的整个集群的视图来继续将查询请求转发到历史节点和实时节点,Broker节点假定集群的结构和Zookeeper中断前是一致的。在实践中,在我们诊断Zookeeper的故障的时候,这种可用性模型使得Druid集群可以继续提供查询服务,为我们争取了更多的时间

 说明:通常在ShareNothing的架构中,如果一个节点变得不可用了,会有一个服务将下线的这个节点的数据搬迁到其他节点,但是如果这个节点下线后又立即重启,而如果服务在一下线的时候就开始搬迁数据,是会产生跨集群的数据传输,实际上是没有必要的。因为分布式文件系统对同一份数据会有多个副本,搬迁数据实际上是为了满足副本数.而下线又重启的节点上的数据不会有什么丢失的,因此短期的副本不足并不会影响整体的数据健康状况.何况跨机器搬迁数据也需要一定的时间,何不如给定一段时间如果它真的死了,才开始搬迁

Coordinator 节点

协调节点负责 历史节点的负载均衡,并通过规则管理数据的生命周期。

  • 规则(Rules):定期(默认一分钟)同步mysql中整个集群的数据拓扑图、元信息库中所有有效的Segment信息以及规则库,从而决定下一步应该做什么;
  • 负载均衡:根据zk中每个historical node负责的segment量,做负载均衡;
  • 副本(replication):在coordinator的UI中配置rules时,可以同时配置加载segment的备份数量,这些备份数量会以load balance的形式,分配到多个historical上面。这个备份数量与hdfs的segment备份数量不一样,hdfs那个保证深度存储的数据不会丢失,historical上面备份是为了保证当某个historical挂掉的时候,其他存储了备份segment的节点能接着提供查询服务。

Coordinator会定期(默认一分钟)同步mysql中整个集群的数据拓扑图、元信息库中所有有效的Segment信息以及规则库,从而决定下一步应该做什么。对于有效且未分配的Segment,Coordinator Node首先按照Historical Node的容量进行倒序排序,即最少容量拥有最高优先级,新的Segment会优先分配到高优先级的Historical Node上。Coordinator Node不会直接与Historical Node打交道,而是在Zookeeper中Historical Node对应的load queue目录下创建待加载Segment的临时信息,等待Historical Node去加载该Segment。 
       Coordinator在每次启动后都会对比zookeeper中保存的当前数据拓扑图以及元信息库中保存的数据信息,所有在集群中已被加载的、在元信息库中标记为失效或者不存在的Segment会被Coordinator Node记录在remove list中,其中也包括Segment对应的新旧version,旧version的Segments同样也会被放入到remove list中,最终被逻辑丢弃。 
       对于离线的Historical Node,Coordinator Node会默认该Historical Node上所有的Segment已失效,从而通知集群内的其他Historical Node去加载该Segment。但是,在生产环境中,我们会遇到机器临时下线,Historical Node在很短时间内恢复服务的情况,那么如此“简单粗暴”的策略势必会加重整个集群内的网络负载。对于这种场景,Coordinator会为集群内所有已丢弃的Segment保存一个生存时间(lifetime),这个生存时间表示Coordinator Node在该Segment被标记为丢弃后,允许不被重新分配最长等待时间,如果该Historical Node在该时间内重新上线,则Segment会被重新置为有效,如果超过该时间则会按照加载规则重新分配到其他Historical Node上。 
       考虑一种最极端的情况,如果集群内所有的Coordinator Node都停止服务,整个集群对外依然有效,不过新Segment不会被加载,过期的Segment也不会被丢弃,即整个集群内的数据拓扑会一直保持不变,直到新的Coordinator Node服务上线。

Indexer节点

形成一个加载批处理和实时数据到系统中的集群,同时会对存储在系统中的数据变更(也称为索引服务)做出响应。Indexing Service是负责“生产”Segment的高可用、分布式、Master/Slave架构服务。主要由三类组件构成:负责运行索引任务(indexing task)的Peon,负责控制Peon的MiddleManager,负责任务分发给MiddleManager的Overlord;三者的关系可以解释为:Overlord是MiddleManager的Master,而MiddleManager又是Peon的Master。其中,Overlord和MiddleManager可以分布式部署,但是Peon和MiddleManager默认在同一台机器上。

Overlord 
Overlord负责接受任务、协调任务的分配、创建任务锁以及收集、返回任务运行状态给调用者。当集群中有多个Overlord时,则通过选举算法产生Leader,其他Follower作为备份。

Overlord可以运行在local(默认)和remote两种模式下,如果运行在local模式下,则Overlord也负责Peon的创建与运行工作,当运行在remote模式下时,Overlord和MiddleManager各司其职,Overlord接受实时/批量数据流产生的索引任务,将任务信息注册到Zookeeper的/task目录下所有在线的MiddleManager对应的目录中,由MiddleManager去感知产生的新任务,同时每个索引任务的状态又会由Peon定期同步到Zookeeper中/Status目录,供Overlord感知当前所有索引任务的运行状况。

Overlord对外提供可视化界面,通过访问http://:/console.html,我们可以观察到集群内目前正在运行的所有索引任务、可用的Peon以及近期Peon完成的所有成功或者失败的索引任务。

MiddleManager 
MiddleManager负责接收Overlord分配的索引任务,同时创建新的进程用于启动Peon来执行索引任务,每一个MiddleManager可以运行多个Peon实例。

在运行MiddleManager实例的机器上,我们可以在${ java.io.tmpdir}目录下观察到以XXX_index_XXX开头的目录,每一个目录都对应一个Peon实例;同时restore.json文件中保存着当前所有运行着的索引任务信息,一方面用于记录任务状态,另一方面如果MiddleManager崩溃,可以利用该文件重启索引任务。

Peon 
Peon是Indexing Service的最小工作单元,也是索引任务的具体执行者,所有当前正在运行的Peon任务都可以通过Overlord提供的web可视化界面进行访问。

 

总体架构

  • 查询路径:红色箭头:①客户端向Broker发起请求,Broker会将请求路由到②实时节点和③历史节点
  • Druid数据流转:黑色箭头:数据源包括实时流和批量数据. ④实时流经过索引直接写到实时节点,⑤批量数据通过IndexService存储到DeepStorage,⑥再由历史节点加载. ⑦实时节点也可以将数据转存到DeepStorage

外部依赖

除了上面介绍的节点角色外,Druid还依赖于外部的三个组件:ZooKeeper, Metadata Storage, Deep Storage。

  • Zookeeper  为集群服务发现和维持当前的数据拓扑而服务
  • Metadata Storage  用户存储segment,configuration 等的metadata信息; 服务创建segments后,会向metadatastore中写一个新的标记, coordinatenode监控metadatastore来获取有哪些新的数据需要被重新load,或者有哪些旧的数据需要被去除。查询的时候并不需要metadatastore的数据。 在生产集群中,mysql 和postgresql是比较常用的metadatastor, derby可以用于单机测试环境
  • Deep Storage deepstorage作为segments一种持久的备份。 服务创建segments后,上传到deepstore。 coordinatenode从deepstorage下载segments。查询的时候也不会用到deepstorage。 常用的deepstorage有S3和hdfs。

MetaStore和ZooKeeper中保存的信息是不一样的. ZooKeeper中保存的是Segment属于哪些节点. 而MetaStore则是保存Segment的元数据信息。为了使得一个Segment存在于集群中,MetaStore存储的记录是关于Segment的自描述元数据: Segment的元数据,大小,所在的DeepStorage。元数据存储的数据会被协调节点用来知道集群中可用的数据应该有哪些(Segment可以通过实时节点转存或者批量数据直接写入).

zookeeper的作用

  • 实时节点在转存Segment到DeepStorage, 会写入自己转存了什么Segment
  • 协调节点管理历史节点,它负责从ZooKeeper中获取要同步/下载的Segment,并指派任务给具体的历史节点去完成
  • 历史节点从ZooKeeper中领取任务,任务完成后要将ZooKeeper条目删除表示完成了任务
  • Broker节点根据ZooKeeper中的Segment所在的节点, 将查询请求路由到指定的节点
  • 对于一个查询路由路径,Broker只会将请求分发到实时节点和历史节点, 因此元数据存储和DeepStorage都不会参与查询中(看做是后台的进程).

详细架构

① 实时数据写入到实时节点,会创建索引结构的Segment.
② 实时节点的Segment经过一段时间会转存到DeepStorage
③ 元数据写入MySQL; 实时节点转存的Segment会在ZooKeeper中新增一条记录
④ 协调节点从MySQL获取元数据,比如schema信息(维度列和指标列)
⑤ 协调节点监测ZK中有新分配/要删除的Segment,写入ZooKeeper信息:历史节点需要加载/删除Segment
⑥ 历史节点监测ZK, 从ZooKeeper中得到要执行任务的Segment
⑦ 历史节点从DeepStorage下载Segment并加载到内存/或者将已经保存的Segment删除掉
⑧ 历史节点的Segment可以用于Broker的查询路由 

标签: Druid 原理架构
共有 人打赏支持
粉丝 29
博文 373
码字总数 194210
×
张欢19933
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: