暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Apache Druid架构原理分析

定安说技术 2021-10-04
2037

一、简介

  • 由MetaData公司于2012年开源,现由Apache进行孵化的

  • 开源的分布式实时数据分析存储系统

  • 快速实现数据查询与分析,高可用、高扩展能力


二、特性

  • 列式存储格式   查询时只需加载需要的列

  • 可伸缩的分布式系统 集群部署(几十到上百个节点),每秒百万数据写入,万亿级数据规模,查询延时压秒到几秒之间

  • 并行处理 查询可被转发到多个节点并行处理

  • 数据实时或批量写入 支持实时和批量两种写入方式,被写入的数据立即可被查询

  • 可自愈、自平衡、易操作

  • 云原生、高容错架构  数据写入后会保存在深度存储(hdfs、amazon s3),几点宕机可从深度存储恢复数据

  • 高效的索引 RoaringBitMap,快速过滤/搜索

  • 数据按时间分区 时间字段作为第一分区字段,也可指定其他字段作为分区字段

  • 近似和精确的聚合算法 近似的count-distinct、ranking、histograms、quantiles,特殊场景也支持精确count-distinct、ranking

  • 数据预聚合 支持数据实时写入时按维度列聚合存储,节省空间,提升查询性能


三、应用场景

适用场景:

  • 写多改少的场景

  • 大多数查询是聚合操作,group by,sum

  • 查询延迟在100ms~几秒之间

  • 数据包含时间字段

  • 单表查询

  • 数据包含高基数列(uid,url)


不适用场景

  • 低延迟更新

  • 离线计算,延迟不重要的场景

  • 多表join场景


四、架构设计

1、实时数据摄入: 实时数据首先会写入MiddleManager,MiddleManager会先将每行的数据加入到1个map中,等达到一定的行数或者大小限制时,MiddleManager就会将内存中的map 持久化到磁盘中,MiddleManager 会按照segmentGranularity将一定时间段内的小文件merge为一个大文件,生成Segment,然后将Segment上传到Deep Storage(HDFS,S3)中,Coordinator知道有Segment生成后,会通知相应的Historical Node下载对应的Segment,并负责该Segment的查询。


2、离线数据摄入: 离线摄入的过程比较简单,就是直接通过MR Job 生成Segment,剩下的逻辑和实时摄入相同


3、用户查询过程:  用户的查询都是直接发送到Broker Node,Broker Node会将查询分发到MiddleManager节点和Historical节点,然后将结果合并后返回给用户


各节点的的主要职责:

  • Coordinator: Coordinator 节点主要负责Segment的管理。Coordinator 节点会通知Historical节点加载新Segment,删除旧Segment,复制Segment,以及Segment间的复杂均衡。Coordinator 节点依赖ZK确定Historical的存活和集群Segment的分布。

  • Realtime Node: 实时节点主要负责数据的实时摄入,实时数据的查询,将实时数据转为Segment,将Segment Hand off 给Historical 节点。

  • Broker: Broker 节点是Druid查询的入口,主要负责查询的分发和Merge。之外,Broker还会对不可变的Segment的查询结果进行LRU缓存。

  • Historical: Historical 节点是整个Druid集群的骨干,主要负责加载不可变的segment,并负责Segment的查询(注意,Segment必须加载到Historical 的内存中才可以提供查询)。Historical 节点是无状态的,所以可以轻易的横向扩展和快速恢复。Historical 节点load和un-load segment是依赖ZK的,但是即使ZK挂掉,Historical依然可以对已经加载的Segment提供查询,只是不能再load 新segment,drop旧segment。

  • Zookeeper: Druid依赖ZK实现服务发现,数据拓扑的感知,以及Coordinator的选主。

  • Metadata Storage: Metadata storage(Mysql) 主要用来存储 Segment和配置的元数据。当有新Segment生成时,就会将Segment的元信息写入metadata store, Coordinator 节点会监控Metadata store 从而知道何时load新Segment,何时drop旧Segment。注意,查询时不会涉及Metadata store。

  • Deep Storage: Deep storage (S3 and HDFS)是作为Segment的永久备份,查询时同样不会涉及Deep storage。


Column


Druid中的列主要分为3类:时间列,维度列,指标列。Druid在数据摄入和查询时都依赖时间列,这也是合理的,因为多维分析一般都带有时间维度。维度和指标是OLAP系统中常见的概念,维度主要是事件的属性,在查询时一般用来filtering 和 group by,指标是用来聚合和计算的,一般是数值类型,如count,sum,min,max等。


Druid中的维度列支持String,Long,Float,不过只有String类型支持倒排索引;指标列支持Long,Float,Complex, 其中Complex指标包含HyperUnique,Cardinality,Histogram,Sketch等复杂指标。强类型的好处是可以更好的对每1列进行编码和压缩, 也可以保证数据索引的高效性和查询性能。


Segment

前面提到过,Druid中会按时间段生成不可变的带倒排索引的列式文件,这个文件就称之为Segment,Segment是Druid中数据存储、复制、均衡、以及计算的基本单元, Segment由dataSource_beginTime_endTime_version_shardNumber唯一标识,1个segment一般包含5–10 million行记录,大小一般在300~700mb。


Segment的存储格式


Druid segment的存储格式如上图所示,包含3部分:

  • version文件

  • meta文件

  • 数据文件


其中meta文件主要包含每1列的文件名和文件的偏移量。(注,druid为了减少文件描述符,将1个segment的所有列都合并到1个大的smoosh中,由于druid访问segment文件的时候采用MMap的方式,所以单个smoosh文件的大小不能超过2G,如果超过2G,就会写到下一个smoosh文件)。


在smoosh文件中,数据是按列存储中,包含时间列,维度列和指标列,其中每1列会包含2部分:ColumnDescriptor和binary数据。其中ColumnDescriptor主要保存每1列的数据类型和Serde的方式。


smoosh文件中还有index.drd文件和metadata.drd文件,其中index.drd主要包含该segment有哪些列,哪些维度,该Segment的时间范围以及使用哪种bitmap;metadata.drd主要包含是否需要聚合,指标的聚合函数,查询粒度,时间戳字段的配置等。


指标列的存储格式


指标列的存储格式如上图所示:

  • version

  • value个数

  • 每个block的value的个数(druid对Long和Float类型会按block进行压缩,block的大小是64K)

  • 压缩类型 (druid目前主要有LZ4和LZF两种压缩算法)

  • 编码类型 (druid对Long类型支持差分编码和Table编码两种方式,Table编码就是将long值映射到int,当指标列的基数小于256时,druid会选择Table编码,否则会选择差分编码)

  • 编码的header (以差分编码为例,header中会记录版本号,base value,每个value用几个bit表示)

  • 每个block的header (主要记录版本号,是否允许反向查找,value的数量,列名长度和列名)

  • 每1列具体的值


Long型指标

Druid中对Long型指标会先进行编码,然后按block进行压缩。编码算法包含差分编码和table编码,压缩算法包含LZ4和LZF。

Float型指标

Druid对于Float类型的指标不会进行编码,只会按block进行压缩。

Complex型指标

Druid对于HyperUnique,Cardinality,Histogram,Sketch等复杂指标不会进行编码和压缩处理,每种复杂指标的Serde方式由每种指标自己的ComplexMetricSerde实现类实现。


String维度的存储格式



String维度的存储格式如上图所示,前面提到过,时间列,维度列,指标列由两部分组成:ColumnDescriptor和binary数据。String维度的binary数据主要由3部分组成:dict,字典编码后的id数组,用于倒排索引的bitmap。


以上图中的D2维度列为例,总共有4行,前3行的值是meituan,第4行的值是dianing。Druid中dict的实现十分简单,就是一个hashmap。图中dict的内容就是将meituan编码为0,dianping编码为1。Id数组的内容就是用编码后的ID替换掉原始值,所以就是[1,1,1,0]。第3部分的倒排索引就是用bitmap表示某个值是否出现在某行中,如果出现了,bitmap对应的位置就会置为1,如图:meituan在前3行中都有出现,所以倒排索引1:[1,1,1,0]就表示meituan在前3行中出现。


显然,倒排索引的大小是列的基数*总的行数,如果没有处理的话结果必然会很大。不过好在如果维度列如果基数很高的话,bitmap就会比较稀疏,而稀疏的bitmap可以进行高效的压缩。


Segment生成过程

1. Add Row to Map

2. Begin persist to disk

3. Write version file

4. Merge and write dimension dict

5. Write time column

6. Write metric column

7. Write dimension column

8. Write index.drd

9. Merge and write bitmaps

10. Write metadata.drd


Segment load过程


  • Read version

  • Load segment to MappedByteBuffer

  • Get column offset from meta

  • Deserialize each column from ByteBuffer


Segment Query过程

Druid查询的最小单位是Segment,Segment在查询之前必须先load到内存,load过程如上一步所述。如果没有索引的话,我们的查询过程就只能Scan的,遇到符合条件的行选择出来,但是所有查询都进行全表Scan肯定是不可行的,所以我们需要索引来快速过滤不需要的行。Druid的Segmenet查询过程如下:

1. 构造1个Cursor进行迭代

2. 查询之前构造出Fliter

3. 根据Index匹配Fliter,得到满足条件的Row的Offset

4. 根据每列的ColumnSelector去指定Row读取需要的列


Druid的编码和压缩

前面已经提到了,Druid对Long型的指标进行了差分编码和Table编码,Long型和Float型的指标进行了LZ4或者LZF压缩




文章转载自定安说技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论