【干货】一文理解Druid原理架构(时序数据库,不是ali的数据库连接池)
druid.io(以下简称druid)是2013年底开源出来的, 主要解决的是对实时数据以及较近时间的历史数据的多维查询提供高并发(多用户),低延时,高可靠性的问题。
druid简介:
- druid是一个为在大数据集之上做实时统计分析而设计的开源数据存储。这个系统集合了一个面向列存储的层,一个分布式、shared-nothing的架构,和一个高级的索引结构,来达成在秒级以内对十亿行级别的表进行任意的探索分析。
- 互联网技术的快速增长催生了各类大体量的数据,hadoop很大的贡献在于帮助企业将他们那些低价值的事件流数据转化为高价值的聚合数据,这适用于各种应用
- 但hadoop擅长的是存储和获取大规模数据,但是它并不提供任何性能上的保证它能多快获取到数据。此外,虽然hadoop是一个高可用的系统,但是在高并发负载下性能会下降
- hadoop是一个很好的后端、批量处理和数据仓库系统。在一个需要高并发并且保证查询性能和数据可用性的并需要提*品级别的保证的需求,hadoop并不能满足,因此创建了druid,一个开源的、分布式、列存储、实时分析的数据存储。在许多方面,druid和其他olap系统有很多相似之处,交互式查询系统,内存数据库(mmdb),众所周知的分布式数据存储。其中的分布式和查询模型都参考了当前的一些搜索引擎的基础架构.
druid的一些特点:
- druid是一个开源的,分布式的,列存储的,适用于实时数据分析的系统,文档详细,易于上手,druid的一些特性总结如下;
- druid支持亚秒级的olap查询分析,druid采用了列式存储/倒排索引/位图索引等关键技术,能够在亚秒级别内完成海量数据的过滤/聚合以及多位分析等操作。druid使用bitmap indexing加速column-store的查询速度,使用了一个叫做concise的算法来对bitmap indexing进行压缩,使得生成的segments比原始文本文件小很多
- druid的高可用性和高扩展性,druid采用分布式,sn(share-nothing)架构,管理类节点课配置ha,工作节点功能单一,不互相依赖,耦合性低,各种节点挂掉都不会使druid停止工作,例如如果不需要streaming data ingestion完全可以忽略realtime node,这些都是的druid在集群的管理,容灾,容错,扩容等方面变得非常容易;
- 实时流数据分析。区别于传统分析型数据库采用的批量导入数据进行分析的方式,druid提供了实时流数据分析,采用lsm(long structure merge)-tree结构使druid拥有极高的实时写入性能;同时实现了实时数据在亚秒级内的可视化。
- 丰富的数据分析功能。针对不同用户群体,druid提供了友好的可视化界面、类sql查询语言以及rest 查询接口。
- druid的一些“局限”:
- segment的不可修改性简化了druid的实现,但是如果你有修改数据的需求,必须重新创建segment,而bitmap indexing的过程是比较耗时的;
- druid能接受的数据的格式相对简单,比如不能处理嵌套结构的数据
druid使用场景:
- 1:适用于清洗好的记录实时录入,但不需要更新操作
- 2:支持宽表,不用join的方式(换句话说就是一张单表)
- 3:可以总结出基础的统计指标,可以用一个字段表示
- 4:对时区和时间维度(year、month、week、day、hour等)要求高的(甚至到分钟级别)
- 5:实时性很重要
- 6:对数据质量的敏感度不高
- 7:用于定位效果分析和策略决策参考
druid本身包含5个组成部分:
broker nodes, historical nodes, realtime nodes, coordinator nodes和indexing services. 分别的作用如下:
- broker nodes: 负责响应外部的查询请求,通过查询zookeeper将请求划分成segments分别转发给historical和real-time nodes,最终合并并返回查询结果给外部;
- historial nodes: 负责’historical’ segments的存储和查询。其会从deep storage中load segments,并响应broder nodes的请求。historical nodes通常会在本机同步deep storage上的部分segments,所以即使deep storage不可访问了,historical nodes还是能serve其同步的segments的查询;
- real-time nodes: 用于存储和查询热数据,会定期地将数据build成segments移到historical nodes。一般会使用外部依赖kafka来提高realtime data ingestion的可用性。如果不需要实时ingest数据到cluter中,可以舍弃real-time nodes,只定时地batch ingestion数据到deep storage;
- coordinator nodes: 可以认为是druid中的master,其通过zookeeper管理historical和real-time nodes,且通过mysql中的metadata管理segments
- druid中通常还会起一些indexing services用于数据导入,batch data和streaming data都可以通过给indexing services发请求来导入数据。
broker node
- 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的架构中,如果一个节点变得不可用了,会有一个服务将下线的这个节点的数据搬迁到其他节点,但是如果这个节点下线后又立即重启,而如果服务在一下线的时候就开始搬迁数据,是会产生跨集群的数据传输,实际上是没有必要的。因为分布式文件系统对同一份数据会有多个副本,搬迁数据实际上是为了满足副本数.而下线又重启的节点上的数据不会有什么丢失的,因此短期的副本不足并不会影响整体的数据健康状况.何况跨机器搬迁数据也需要一定的时间,何不如给定一段时间如果它真的死了,才开始搬迁
historical node
- 历史节点封装了加载和处理由实时节点创建的不可变数据块(segment)的功能。在很多现实世界的工作流程中,大部分导入到druid集群中的数据都是不可变的,因此,历史节点通常是druid集群中的主要工作组件。
- 历史节点遵循shared-nothing的架构,因此节点间没有单点问题。节点间是相互独立的并且提供的服务也是简单的,它们只需要知道如何加载、删除和处理不可变的segment (注:shared nothing architecture是一 种分布式计算架构,这种架构中不存在集中存储的状态,整个系统中没有资源竞争,这种架构具有非常强的扩张性,在web应用中广泛使用)
- 类似于实时节点,历史节点在zookeeper中通告它们的在线状态和为哪些数据提供服务。加载和删除segment的指令会通过zookeeper来进行发布,指令会包含segment保存在deep storage的什么地方和怎么解压、处理这些segment的相关信息
- 在历史节点从deep storage下载某一segment之前,它会先检查本地缓存信息中看segment是否已经存在于节点中,如果segment还不存在缓存中,历史节点会从deep storage中下载segment到本地
- 一旦处理完成,这个segment就会在zookeeper中进行通告。此时,这个segment就可以被查询了。历史节点的本地缓存也支持历史节点的快速更新和重启,在启动的时候,该节点会检查它的缓存,并为任何它找到的数据立刻进行服务的提供,如下图:
- 历史节点从deep storage下载不可变的segment。segment在可以被查询之前必须要先加载到内存中
- 历史节点可以支持读一致性,因为它们只处理不可变的数据。不可变的数据块同时支持一个简单的并行模型:历史节点可以以非阻塞的方式并发地去扫描和聚合不可变的数据块
tiers: 历史节点可以分组到不同的tier中,哪些节点会被分到一个tier中是可配置的。tier的目的是可以根据segment的重要程度来分配高或低的优先级来进行数据的分布。
- 可以为不同的tier配置不同的性能和容错参数。例如,可以使用一批很多个核的cpu和大容量内存的节点来组成一个“热点数据”的tier,这个“热点数据”集群可以配置来用于下载更多经常被查询的数据。
- 一个类似的”冷数据”集群可以使用一些性能要差一些的硬件来创建,“冷数据”集群可以只包含一些不是经常访问的segment
可用性: 历史节点依赖于zookeeper来管理segment的加载和卸载。
- 如果zookeeper变得不可用的时候,历史节点就不再可以为新的数据提供服务和卸载过期的数据,因为是通过http来为查询提供服务的
- 对于那些查询它当前已经在提供服务的数据,历史节点仍然可以进行响应。这意味着zookeeper运行故障时不会影响那些已经存在于历史节点的数据的可用性。
coordinator node
- 主要负责数据的管理和在历史节点上的分布。协调节点告诉历史节点加载新数据、卸载过期数据、复制数据、和为了负载均衡移动数据。
- druid为了维持稳定的视图,使用一个多版本的并发控制交换协议来管理不可变的segment。如果任何不可变的segment包含的数据已经被新的segment完全淘汰了,则过期的segment会从集群中卸载掉。
- 协调节点会经历一个leader选举的过程,来决定由一个独立的节点来执行协调功能,其余的协调节点则作为冗余备份节点
- 协调节点会周期性(一分钟)的执行来确定集群的当前状态,它通过在运行的时候对比集群的预期状态和集群的实际状态来做决定。和所有的druid节点一样,协调节点维持一个和zookeeper的连接来获取当前集群的信息(数据拓扑图、元信息库中所有有效的segment信息以及规则库)
- 协调节点也维持一个与mysql数据库的连接,mysql包含有更多的操作参数和配置信息。
- 其中一个存在于mysql的关键信息就是历史节点可以提供服务的所有segment的一个清单,这个表可以由任何可以创建segment的服务进行更新,例如实时节点。
- mysql数据库中还包含一个rule表来控制集群中segment的是如何创建、销毁和复制
rules:rules管理历史segment是如何在集群中加载和卸载的。
- rules指示segment应该如何分配到不同的历史节点tier中,每一个tier中应该保存多少份segment的副本。
- rules还可能指示segment何时应该从集群中完全地卸载。rules通常设定为一段时间,例如,一个用户可能使用rules来将最近一个月的有价值的segment载入到一个“热点数据”的集群中,最近一年的有价值的数据载入到一个“冷数据”的集群中,而将更早时间前的数据都卸载掉。
- 协调节点从mysql数据库中的rule表加载一组rules。rules可能被指定到一个特定的数据源,或者配置一组默认的rules。协调节点会循环所有可用segment并会匹配第一条适用于它的rule
负载均衡:在典型的生产环境中,查询通常命中数十甚至上百个segment,由于每个历史节点的资源是有限的,segment必须被分布到整个集群中,以确保集群的负载不会过于不平衡。
- 要确定最佳的负载分布,需要对查询模式和速度有一定的了解。通常,查询会覆盖一个独立数据源中最近的一段邻近时间的一批segment。平均来说,查询更小的segment则更快
- 这些查询模式提出以更高的比率对历史segment进行复制,把大的segment以时间相近的形式分散到多个不同的历史节点中,并且使存在于不同数据源的segment集中在一起
- 为了使集群中segment达到最佳的分布和均衡,根据segment的数据源、新旧程度、和大小,开发了一个基于成本的优化程序
副本/复制(replication):
- 协调节点可能会告诉不同的历史节点加载同一个segment的副本。每一个历史节点tier中副本的数量是完全可配置。
- 设置一个高级别容错性的集群可以设置一个比较高数量的副本数。segment的副本被视为和原始segment一样的,并使用相同的负载均衡算法
- 通过复制segment,单一历史节点故障对于整个druid集群来说是透明的,不会有任何影响
可用性:
- 协调节点有zookeeper和mysql这两个额外的依赖,协调节点依赖zookeeper来确定集群中有哪些历史节点
- 如果zookeeper变为不可用,协调节点将不可以再进行segment的分配、均衡和卸载指令的发送。不过,这些都不会影响数据的可用性
- 对于mysql和zookeeper响应失效的设计原则是一致的:如果协调节点一个额外的依赖响应失败了,集群会维持现状
- druid使用mysql来存储操作管理信息和关于segment如何存在于集群中的segment元数据。如果mysql下线了,这些信息就在协调节点中变得不可用,不过这不代表数据不可用
- 如果协调节点不可以和mysql进行通信,他们会停止分配新的segment和卸载过期的segment。在mysql故障期间broker节点、历史节点、实时节点都是仍然可以查询的
realtime node
- 实时节点封装了导入和查询事件数据的功能,经由这些节点导入的事件数据可以立刻被查询。
- 实时节点只关心一小段时间内的事件数据,并定期把这段时间内收集的这批不可变事件数据导入到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这个时段的数据的信息,并且宣布取消为这些数据提供服务。
indexing server
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各司其职,根据图3.6所示,overlord接受实时/批量数据流产生的索引任务,将任务信息注册到zookeeper的/task目录下所有在线的middlemanager对应的目录中,由middlemanager去感知产生的新任务,同时每个索引任务的状态又会由peon定期同步到zookeeper中/status目录,供overlord感知当前所有索引任务的运行状况。
overlord对外提供可视化界面,通过访问http://:/console.html,我们可以观察到集群内目前正在运行的所有索引任务、可用的peon以及近期peon完成的所有成功或者失败的索引任务。
middlemanager
middlemanager负责接收overlord分配的索引任务,同时创建新的进程用于启动peon来执行索引任务,每一个middlemanager可以运行多个peon实例。
在运行middlemanager实例的机器上,我们可以在${ .tmpdir}目录下观察到以xxx_index_xxx开头的目录,每一个目录都对应一个peon实例;同时restore.json文件中保存着当前所有运行着的索引任务信息,一方面用于记录任务状态,另一方面如果middlemanager崩溃,可以利用该文件重启索引任务。
peon
peon是indexing service的最小工作单元,也是索引任务的具体执行者,所有当前正在运行的peon任务都可以通过overlord提供的web可视化界面进行访问。
架构介绍:
- 查询路径:红色箭头:①客户端向broker发起请求,broker会将请求路由到②实时节点和③历史节点
- druid数据流转:黑色箭头:数据源包括实时流和批量数据. ④实时流经过索引直接写到实时节点,⑤批量数据通过indexservice存储到deepstorage,⑥再由历史节点加载. ⑦实时节点也可以将数据转存到deepstorage
- druid的集群依赖了zookeeper来维护数据拓扑. 每个组件都会与zookeeper交互,如下:
- 实时节点在转存segment到deepstorage, 会写入自己转存了什么segment
- 协调节点管理历史节点,它负责从zookeeper中获取要同步/下载的segment,并指派任务给具体的历史节点去完成
- 历史节点从zookeeper中领取任务,任务完成后要将zookeeper条目删除表示完成了任务
- broker节点根据zookeeper中的segment所在的节点, 将查询请求路由到指定的节点
- 对于一个查询路由路径,broker只会将请求分发到实时节点和历史节点, 因此元数据存储和deepstorage都不会参与查询中(看做是后台的进程).
metadata storage 与 zookeeper
- metastore和zookeeper中保存的信息是不一样的. zookeeper中保存的是segment属于哪些节点. 而metastore则是保存segment的元数据信息
- 为了使得一个segment存在于集群中,metastore存储的记录是关于segment的自描述元数据: segment的元数据,大小,所在的deepstorage
- 元数据存储的数据会被协调节点用来知道集群中可用的数据应该有哪些(segment可以通过实时节点转存或者批量数据直接写入).
druid还包含3个外部依赖,与其说是依赖,不如说正式druid开放的架构,用户可以根据自己的需求使用不同的外部组建
- mysql:存储druid中的各种metadata(里面的数据都是druid自身创建和插入的),在druid_0.9.1.1版本中,元信息库druid主要包含十张表,均以“druid_”开头,例如张表:”druid_config”(通常是空的), “druid_rules”(coordinator nodes使用的一些规则信息,比如哪个segment从哪个node去load)和“druid_segments”(存储每个segment的metadata信息);
- deep storage: 存储segments,druid目前已经支持本地磁盘,nfs挂载磁盘,hdfs,s3等。deep storage的数据有2个来源,一个是batch ingestion, 另一个是real-time nodes;
- zookeeper: druid使用zookeeper作为分布式集群内部的通信组件,各类节点通过curator framework将实例与服务注册到zookeeper上,同时将集群内需要共享的信息也存储在zookeeper目录下,从而简化集群内部自动连接管理、leader选举、分布式锁、path缓存以及分布式队列等复杂逻辑。
- ① 实时数据写入到实时节点,会创建索引结构的segment.
- ② 实时节点的segment经过一段时间会转存到deepstorage
- ③ 元数据写入mysql; 实时节点转存的segment会在zookeeper中新增一条记录
- ④ 协调节点从mysql获取元数据,比如schema信息(维度列和指标列)
- ⑤ 协调节点监测zk中有新分配/要删除的segment,写入zookeeper信息:历史节点需要加载/删除segment
- ⑥ 历史节点监测zk, 从zookeeper中得到要执行任务的segment
- ⑦ 历史节点从deepstorage下载segment并加载到内存/或者将已经保存的segment删除掉
- ⑧ 历史节点的segment可以用于broker的查询路由
- 由于各个节点和其他节点都是最小化解耦的, 所以下面两张图分别表示实时节点和批量数据的流程:
- 数据从kafka导入到实时节点, 客户端直接查询实时节点的数据.
- 批量数据使用indexservice,接收post请求的任务,直接产生segment写到deepstorage里.deepstorage中的数据只会被历史节点使用.
所以这里要启动的服务有: indexservice(overlord), historical, coordinator(协调节点通知历史节点下载segment)
说明:文章非原创,看了好多文章整理组合到一起的.
大数据相关的伙伴可以关注我的公众号,分享大数据干货和面试进阶!
上一篇: 0.5 Linux的联通性命令汇总