Druid学习笔记(2)Druid架构剖析
1. 前言
Druid 的目标是提供一个能够在大数据集上做实时数据摄入与查询的平台,然而对于大多数系统而言,提供数据的快速摄入与提供快速查询是难以同时实现的两个指标。例如对于普通的RDBMS,如果想要获取更快的查询速度,就会因为创建索引而牺牲掉写入的速度,如果想要更快的写入速度,则索引的创建就会受到限制。而Druid却可以完美的对两者进行结合,本文将对Druid如何实现这种结合做一个简单的介绍。
2. Druid数据流
下图为Druid的数据流,包括数据摄入,元数据,查询,三方面的流程
2.1数据摄入
数据可以通过实时节点以及批处理的方式进入Druid,对于实时节点而言,数据流的数据被实时节点消费后,当满足条件后,实时节点会将收到的数据生成为Segment文件并上传到DeepStorage.批量数据经过Druid消费后,会被直接上传到DeepStorage中。
2.2元数据
当数据文件上传到DeepStorage后,Coordinator节点会通知历史节点将Segment的文件从DeepStorage上下载到本地磁盘。
2.3查询
在数据摄入的同事,Broker节点可以接受查询请求,并将分别从实时节点与历史节点的查询的结果合并后返回
3. 架构设计
前文提到Druid在数据摄入与查询的性能方面,做到了很好结合,本章节将详细分析Druid是如何做到的。
3.1 常见的文件组织方式
除了内存数据库之外的大多数数据库,数据基本都是存在磁盘上,而磁盘的访问操作相对于内存操作而言是非常耗时的操作,提高数据库性能的关键点之一就是减少对磁盘的访问次数。
为了减少访问次数,每个数据库基本都有自己特殊的数据结构来帮助提高查询效率(也可以叫索引),考虑到数据查询一般都是一个范围的数据,所以相关结构一般都不会考虑使用HASH结构,而会使用Tree结构,下面对几种常见的Tree结构进行简单的说明
二叉查找树(Binary Search Tree)
就是一颗二叉有序树,保证左子树上的所有节点都小于根节点,保证右子树上的所有节点都大于根节点。优点是简单,缺点是出现数据倾斜时,效率会很低
平衡二叉树
针对二叉查找树的问题,平衡二叉树出现了,该种树结构的缺点是树高为Log2N,树的高度越高,查找效率越低
B+树
在传统的关系数据库中,B+树以及其衍生树是被用来作为索引数据结构最多的书,特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。详细介绍请参考:<高性能MySql进化论(六):常见索引类型的原理及其特点的介绍> http://blog.csdn.net/eric_sunah/article/details/14045991:
B+树的缺点来自于其优点,当存储数据为亿级别时,随着插入操作的不断产生,叶子节点会慢慢的分裂,可能导致原本连续存放的数据,存放在不同的物理磁盘块位置上,做范围查询时,导致较高的磁盘IO,导致性能下降
日志合并树(LSM)
对于写操作而言,顺序写的效率会远远大于随机写的速度,而上述的B+树就违反了该原则。1992年日志结构(Log Structured)的新型索引结构产生了,该种类型的思想主要是将磁盘看成是一个大的日志,每次数据都最佳到末端,从而提高了写的性能。此时的日志结构索引对于随机读取的效率很低。
1996年,日志结构合并树(Log structured Merge-Tree-LSM)的索引结构被提出。该种结构包含了LS的优点,同时又通过将数据文件预排序来克服随机查性能不足的问题。
LSM的名声大噪归功于HBASE以及Cassandra,随着这两个Apache顶级项目的发展,LSM技术也在不断的发展
LSM-tree是由两个或两个以上存储数据的结构组成的。最简单的LSM-tree由两个部件构成。一个部件常驻内存,称为C0树(或C0),可以为任何方便键值查找的数据结构,另一个部件常驻硬盘之中,称为C1树(或C1),其数据结构与B-tree类似。C1中经常被访问的结点也将会被缓存在内存中。如下图所示:
当插入一条新的数据条目时,首先会向日志文件中写入插入操作的日志,为以后的恢复做准备。然后将根据新条目的索引值将新条目插入到C0中。将新条目插入内存的C0中,不需要任何与硬盘的I/O操作,但内存的存储代价比硬盘的要高上不少,因此当C0的大小达到某一阈值时,内存存储的代价会比硬盘的I/O操作和存储代价还高。故每当C0的大小接近其阈值时,将有一部分的条目从C0滚动合并到硬盘中的C1,以减少C0的大小,降低内存存储数据的代价。
除了C0,C1结构,LSM Tree通常还会结合日志文件(commit log,在Hbase中叫Hlog)来为数据恢复做保障,C0,C1,commit log的协作顺序大概为:
- 新插入的数据首先写到commit log中,该操作叫WAL(Write Ahead LOG)
- 写完commit log后,数据写到C0
- 打到一定条件后,C0中的数据被Flush到C1,并删除对应的commit log
- C0,C1数据可同时提供查询
- 当c0数据出问题了,可以使用commit log与c1中的内容回复C0
LSM-Tree的结构非常有利于海量数据的写入,但是在查询方面还是存在不足,为了解决查询性能问题,一般采用如下策略进行弥补:
1. 定期的对C1上的小的文件进行合并。
2. 对C1使用布隆过滤器,以加速查询数据是否在某个C1中的判定。
3.2 Druid中的LSM-Tree
LSM-Tree适合哪种写操作要远远大于DELETE/UPDATE/QUERY的应用场景,这正好符合Druid的使用场景,所以Druid的文件组织方式与LSM-Tree类似。
对于可以摄取实时数据的实时节点而言,涉及操作大致如下:
1. 实时数据首先会被加载到实时节点内存中的堆结构缓冲区
2. 当条件满足时,缓冲区的数据会被flush到磁盘上变成一个数据块
3. 将磁盘上的数据块加载到内存中的非堆区
4. 查询节点可以同时从堆缓冲区与非堆区进行数据查询
上述描述,可以用下面的图来进行表示:
对于已经落地到实时节点的磁盘的数据块,还会进行如下处理:
1. 实时节点周期性的将统一时间段内的数据块文件合并成一个大的文件
2. 生成好的大文件会立即被上传到Deep Storage
3. 协调节点感知到有新的数据块文件被上传到DeepStorage后,会协调某个历史节点对相关文件进行下载
4. 历史节点加载完相关数据后,会通过协调节点对外声明对于该文件内容的查询,都由自己提供。产生该文件的实时节点也会对外声明,不再负责对应数据的查询
从上述内容可以看出,Druid的类LSM-Tree结构有以下特点:
1. 类LSM-Tree的架构,保证了Druid的高性能写入
2. 通过“查询职责分离模式+不支持更新操作” 保证了组件职责的单一以及数据处理的简单性,保证了查询性能的高效性
3.3 数据存储结构
Druid的高性能,除了来自类LSM-Tree的贡献,其DataSource以及Segment的完美设计也功不可没。
3.3.1 Datasource
Druid中的Datasource可以理解为RDBMS中的表,其包含下面三个重要的概念:
1. 时间列(Timestamp):每行数据的时间值,默认使用UTC时间格式,保存到毫秒级别,本列是数据聚合以及范围查询的重要指标
2. 维度列(Dimension):标识数据行的列,可以是一列,也可以是多列
3. 指标列(Metric):用来做计算或是统计的列,可以是一列,也可以是多列
相对于其他数据库,Druid Datasource最大的特点是在输入存储时,就可以对数据进行聚合操作,该特性不仅可以节省存储的空间,而且可以提高聚合查询的效率。
3.3.2 Segment
Segment为Druid中数据的物理存储格式,Segment通过以下特性来支撑Druid的高性能:
- 数据的横向切割:横向切割主要只指站在时间范围的角度,将不同时间段的数据存储在不同的Segment文件中(时间范围可以通过segmentGranularity进行设置),查询时只需要更具时间条件遍历对应的Segment文件即可。
- 数据的纵向切割:面向列进行进行数据压缩
- 使用BitMap等技术对数据访问进行优化
4. 组件介绍
4.1 实时节点
实时节点主要负责实时数据摄入,以及生成Segment文件。
4.1.1 获取数据与生成Segment文件
实时节点通过Firehose来消费实时数据,Firehose是Druid中的消费实时数据模型,可以有不同的实现,Druid自带了一个基于Kafka High Level API实现的对于Kafka的数据消费(druid-kafka-eight Firehose)。
除了Firehose,实时节点上还有一个重要的角色叫Plumber,主要负责按照指定的周期,对数据文件进行合并。
实时节点提供Pull以及Push两种方式对数据进行摄取,详细介绍将在后续文中进行详细描述。
4.1.2 高可用
当使用druid-kafka-eight从Kafka进行数据消费时,该Firehose可以让实时节点具有很好的可扩展性。当启动多个实时节点时,将使用Kafka Consumer Group的方式从Kafka进行数据获取,通过Zookeeper来维护每个节点的offset情况,无论是增加节点,还是删除节点,通过High API都可以保证Kafka的数据至少被Druid的集群消费一次。Kafka Consumer Group的详细说明,请参考:http://blog.csdn.net/eric_sunah/article/details/44243077
通过druid-kafka-eight实现的高可用机制,可用下图进行表示:
通过druid-kafka-eight保证的高可用,仔细分析可以发现会存在生成的segment文件不能被传到Deepstorage的缺陷,解决该问题可以通过两个办法
- 重启实时节点
- 使用Tranquility+Index Service的方式对Kafka的数据进行精确的消费与备份。由于Tranquility可以通过Push的方式将制定的数据推到Druid集群,一次它可以对同一个Partition数据创建多个副本,当某个数据消费任务失败时,系统可以准确的使用另外一个相同任务所创建的Segment数据块。
4.2 历史节点
历史节点的职责比较单一,主要是将segment数据文件加载到内存以提供数据查询,由于Druid不支持数据变更,因此历史节点就是加载文件与提供查询。
4.2.1 内存方式提供查询
Coordinator Nodes会定期(默认为1分钟)去同步元信息库,感知新生成的Segment,将待加载的Segment信息保存在Zookeeper中,当Historical Node感知到需要加载新的Segment时,首先会去本地磁盘目录下查找该Segment是否已下载,如果没有,则会从Zookeeper中下载待加载Segment的元信息,此元信息包括Segment存储在何处、如何解压以及如何如理该Segment。Historical Node使用内存文件映射方式将index.zip中的XXXXX.smoosh文件加载到内存中,并在Zookeeper中本节点的served segments目录下声明该Segment已被加载,从而该Segment可以被查询。对于重新上线的Historical Node,在完成启动后,也会扫描本地存储路径,将所有扫描到的Segment加载如内存,使其能够被查询。
无论何种查询,历史节点都会先将segment数据加载到内存,然后再提供查询
==由于历史节点提供的查询服务依赖于内存,所以内存的大小直接影响到历史节点的性能。==
4.2.2 数据分层(Tiers)
在面对海量数据存储时,一般都会使用数据分层的存储策略,常见策略如下:
1. 热数据:经常被访问,数据量不大,查询延迟要求低
2. 温数据:不经常被访问,数据量较大,查询延迟要求尽量低
3. 冷数据:偶尔被访问,数据量占比最大,查询延迟不用很快
Druid中,历史节点可以分成不同的层次,相同层次中的所有节点都采用相同的配置。 可以为每一层设置不同的性能和容错参数。
4.2.3 高可用
历史节点拥有较好的高可用特性,协调节点可以通过Zookeeper感知到历史节点的增加或是删除操作。当新增历史节点时,协调可以自动分配Segment给新增的节点,当移除历史节点时,协调节点会将该历史节点上的数据分配给其他处于Active状态的历史节点。
历史节点依赖于Zookeeper进行Segment数据的加载和卸载操作。如果Zookeeper变得不可用,历史节点将不能再进行数据加载和卸载操作。但是因为查询功能使用的是HTTP服务,所以Zookeeper出现异常后,不会影响历史节点上对以加载数据的查询。
4.3 查询节点
查询节点负责接收查询请求,并将实时节点以及历史节点的查询结果合并后返回。
4.3.1 缓存
Druid提供了两类介质提供Cache功能:
1. 外部Cache,例如Memcache
2. 内部Cache,使用查询节点或是历史节点的内存
Broker Node默认使用LRU缓存策略,查询的时候会首先访问Cache,如果Cache没有命中,才会继续访问历史/实时节点。
对于每次查询的结果,Historical Node返回的结果,Broker Node认为是“可信的”,会缓存下来,而Real-Time Node返回的实时数据,Broker Node认为是可变的,“不可信的”,所以不会缓存。所以对每个查询请求,如果涉及到实时节点,则该请求总是会转到实时节点。
Cache也可以理解为对数据额外的备份,即使说有的历史节点都挂了,还是有可能从Cache中查到对应的数据。
Cache的原理图如下:
4.3.2 高可用
可以通过Nginx+(N*Broker Node)的方式达到查询节点高可用的效果,该种部署模式下,无论查询请求落到哪个查询节点,返回的结果都是相同的。
4.4 协调节点
协调节点主要负责管理历史节点的负载均衡以及根据规则管理数据的生命周期
4.4.1 历史节点负载均衡
在典型的生产环境中,查询通常会触及几十个甚至几百个Segment 。由于每个历史节点都资源有限,所以必须在集群中均衡的分配Segment。均衡的策略主要基于成本的优化,例如考虑时间和大小,远近等因素。
对于历史节点而言,协调节点就是其Master节点,协调节点出问题时,历史节点虽然还可以提供查询功能,但不会再接收新的segment数据。
4.4.2 数据生命周期
Druid利用针对每个DataSource设置的Rule来加载或丢弃具体的数据文件。规则用来表名表明应该如何分配Segment到不同的历史节点层,以及一个分段的在每个层应该有多少个副本等。规则还可以用来指定什么时候应该删除那些Segment。
可以对一个Datasource添加多条规则,对于某个Segment来说,协调节点会逐条检查规则,当检测到某个Segment符合某个规则时,就命令对应的历史节点执行对应的操作。
4.4.3 Replication
Druid允许用户指定某个Datasource的Segment副本数,默认为1,即对于某个datasource的某个segment,只会存在于单个历史节点上。 为了防止某个历史节点宕机时,部分segment的不可用,可以根据资源的情况增加segment的副本数。
4.4.4 高可用
可以通过部署多个协调节点来达到协调节点高可用的目的,如果集群中存在多个Coordinator Node,则通过选举算法产生Leader,其他Follower作为备份。
4.5 索引服务(Indexing Service)
索引服务也可以产生Segment文件,相对于实时节点,索引节点主要包括以下优点:
1. 除了支持Pull的方式摄取数据,还支持Push的方式
2. 可以通过API的方式定义任务配置
3. 可以更灵活的使用系统资源
4. 可以控制segment副本数量的控制
5. 可以灵活的完成和segment数据文件相关的操作
6. 提供可扩展以及高可用的特性
4.5.1 主从结构的架构
Indexing Service是高可用、分布式、Master/Slave架构服务。主要由三类组件构成:负责运行索引任务(indexing task)的Peon,负责控制Peon的MiddleManager,负责任务分发给MiddleManager的Overlord;三者的关系可以解释为:Overlord是MiddleManager的Master,而MiddleManager又是Peon的Master。其中,Overlord和MiddleManager可以分布式部署,但是Peon和MiddleManager默认在同一台机器上,架构图如下:
4.5.2 统治节点(Overload)
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完成的所有成功或者失败的索引任务。
4.5.3 MiddleManager
MiddleManager负责接收Overlord分配的索引任务,同时创建新的进程用于启动Peon来执行索引任务,每一个MiddleManager可以运行多个Peon实例。
在运行MiddleManager实例的机器上,我们可以在${ java.io.tmpdir}目录下观察到以XXX_index_XXX开头的目录,每一个目录都对应一个Peon实例;同时restore.json文件中保存着当前所有运行着的索引任务信息,一方面用于记录任务状态,另一方面如果MiddleManager崩溃,可以利用该文件重启索引任务。
4.5.4 Peon
Peon是Indexing Service的最小工作单元,也是索引任务的具体执行者,所有当前正在运行的Peon任务都可以通过Overlord提供的web可视化界面进行访问