1 简介
Druid是针对时间序列数据提供低延时的数据写入以及快速交互式查询的分布式OLAP数据库。
分布式OLAP数据库:
(1)ES-明细数据检索(OLAP聚合分析支持不好)
(2)Kylin-预计算+kv存储(预计算无法做到低延时)
(3)Presto-可直接读HDFS文件的查询引擎
注意:如果需要使用到join,一般来说会在之前进行一步流处理,将数据处理成一个宽表,再进行Druid端计算。
Druid的核心架构,结合了数据仓库,时间序列数据库,日志搜索系统。其中主要的特点有:
<1> 列式存储格式
Druid使用列式存储,在特定查询中只需要指定需要的列,这样能够给速度带来很大的提升;另外,每列都针对其特定数据类型进行了优化存储,支持更快浏览和聚合。
<2> 可扩展的分布式系统
<3> 大量并行计算
<4> 支持实时或者批量的数据摄入
<5> ‘自愈’,自动平衡,容易维护
<6> 云原生的容错架构,不会丢失数据
一旦Druid摄入了数据,就会产生副本存储起来
<7> 支持索引快速过滤
Druid使用CONCISE或Roaring压缩的bitmap索引来创建索引,以支持快速过滤和跨多列搜索。
<8> 基于时间的分区
Druid首先按时间对数据进行分区,然后可以根据其他字段进行分区。 这意味着基于时间的查询将仅访问与查询时间范围匹配的分区。
<9> 近似算法
Druid包括用于近似计数区别,近似排名以及近似直方图和分位数计算的算法。 这些算法提供有限的内存使用量,通常比精确计算要快得多。 对于精度比速度更重要的情况,Druid还提供了精确的计数区别和精确的排名。
<10> 摄取时自动汇总
Druid可选地(需要预先配置)在摄取时支持数据汇总,这种汇总会部分地预先聚合您的数据,并可以节省大量成本并提高性能。
2 基本概念
1.数据
按照列的不同类型,可以将数据分为以下三类:
(1) 时间序列(Timestamp):
Druid既是内存数据库,又是时间序列数据库,Druid中所有查询以及索引过程都和时间维度息息相关。Druid底层使用绝对毫秒数保存时间戳,默认使用ISO-8601格式展示时间(形如:yyyy-MM-ddThh:mm:sss.SSSZ,其中“Z”代表零时区,中国所在的东八区可表示为+08:00)。
(2)维度列(Dimensions),Druid的维度概念和OLAP中一致,一条记录中的字符类型(String)数据可看作是维度列,维度列被用于过滤筛选(filter)、分组(group)数据。如图3.1中page、Username、Gender、City这四列。
(3)度量列(Metrics),Druid的度量概念也与OLAP中一致,一条记录中的数值(Numeric)类型数据可看作是度量列,度量列被用于聚合(aggregation)和计算(computation)操作。如图3.1中的Characters Added、Characters Removed这两列。
2.roll up
Druid会在摄入数据的时候,对原始数据进行聚合操作。该过程称为上卷(roll up)。Roll up可以看做是在选择的列上进行的第一级聚合操作,以减少sgements的数量。
以下面数据为例:
{"timestamp":"2018-01-01T01:01:35Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2","packets":20,"bytes":9024}
{"timestamp":"2018-01-01T01:01:51Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2","packets":255,"bytes":21133}
{"timestamp":"2018-01-01T01:01:59Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2","packets":11,"bytes":5780}
{"timestamp":"2018-01-01T01:02:14Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2","packets":38,"bytes":6289}
{"timestamp":"2018-01-01T01:02:29Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2","packets":377,"bytes":359971}
{"timestamp":"2018-01-01T01:03:29Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2","packets":49,"bytes":10204}
{"timestamp":"2018-01-02T21:33:14Z","srcIP":"7.7.7.7", "dstIP":"8.8.8.8","packets":38,"bytes":6289}
{"timestamp":"2018-01-02T21:33:45Z","srcIP":"7.7.7.7", "dstIP":"8.8.8.8","packets":123,"bytes":93999}
{"timestamp":"2018-01-02T21:35:45Z","srcIP":"7.7.7.7", "dstIP":"8.8.8.8","packets":12,"bytes":2818}
rollup-index.json
{
"type" : "index",
"spec" : {
"dataSchema" : {
"dataSource" : "rollup-tutorial",
"parser" : {
"type" : "string",
"parseSpec" : {
"format" : "json",
"dimensionsSpec" : {
"dimensions" : [
"srcIP",
"dstIP"
]
},
"timestampSpec": {
"column": "timestamp",
"format": "iso"
}
}
},
"metricsSpec" : [
{ "type" : "count", "name" : "count" },
{ "type" : "longSum", "name" : "packets", "fieldName" : "packets" },
{ "type" : "longSum", "name" : "bytes", "fieldName" : "bytes" }
],
"granularitySpec" : {
"type" : "uniform",
"segmentGranularity" : "week",
"queryGranularity" : "minute",
"intervals" : ["2018-01-01/2018-01-03"],
"rollup" : true
}
},
"ioConfig" : {
"type" : "index",
"firehose" : {
"type" : "local",
"baseDir" : "quickstart/tutorial",
"filter" : "rollup-data.json"
},
"appendToExisting" : false
},
"tuningConfig" : {
"type" : "index",
"maxRowsPerSegment" : 5000000,
"maxRowsInMemory" : 25000
}
}
}
其中维度列是srcIP和dstIP,时间序列时timestamp,度量列有count,packets和bytes。
其中"queryGranularity" : "minute"
发现01:01内的数据被聚合了,按照时间序列和维度列,即{timestamp,srcIP,dstIP}进行第一步聚合,对应的指标列进行sum操作。
总结,roll-up对什么范围内的数据进行第一步的聚合,是由"queryGranularity" : "minute"来决定的。
queryGranularity:默认为None,允许查询的时间粒度,单位与segmentGranularity相同,如果为None那么允许以任意时间粒度进行查询。
注意:当设定roll-up为true时,会带来信息量的丢失,因为roll-up的粒度会变成最小的数据可视化粒度,即毫秒级别的原始数据,如果按照分钟粒度进行roll-up,那么入库之后我们能够查看数据的最小粒度即为分钟级别。
在界面中,点击如下按钮可以显示上面的json
3.Sharding和Segment
Druid是时间序列数据库,也存在分片(Sharding)的概念。Druid对原始数据按照时间维度进行分片,每一个分片称为段(Segment)。
Segment是Druid中最基本的数据存储单元,采用列式(columnar)存储某一个时间间隔(interval)内某一个数据源(dataSource)的部分数据所对应的所有维度值、度量值、时间维度以及索引。
注意:
Segment是按照time进行分区,默认的,一个Segment文件是每隔一个time interval创建的,而这个time interval是在granularitySpec中的segmentGranularity来配置的。
为了能够让Druid在查询下良好运行,segment文件的大小推荐在300mb~700mb中。
如果大小超过这个区间,考虑改变这个time interval的单位(换成更小的单位)或者重新对数据分区,在TunningConfig中增加partitionsSpec,调整targetPartitionSize(此参数的最佳起点是500万行)。
Druid将不同时间范围内的数据存储在不同的Segment数据快中,这就是所谓的数据横向切割,这种设计为Druid带来的显而易见的优点:按时间范围查询数据时,仅需要访问对应时间段内的这些Segment数据块,而不需要进行全表数据范围查询,大大提高效率。
将行式数据转换为列式存储结构:按需加载,提高查询速度。有3种类型的数据结构:
(1)timestamp列,long数组
(2)指标列,int数组或者float数组
(3)维度列,支持过滤和分组。使用压缩的BitMap索引
3.1 Segment数据结构
Timestamp列和Metric列比较简单,他们在实现中被存储为通过lz4压缩的整型或浮点数组。当一个查询需要访问某列数据时,只需要解压缩这些列,然后读取出这些列的数据,然后执行预定义的聚合操作。对于查询过程中不需要的列,Druid会直接跳过对这些列数据的访问。
维度列Dimension列由于要实现filter和group by,实现有所不同,dimension列包含以下三种数据结构:
(1)一个字典:将值从字符串(所有的dimension列的数值都被当做字符串处理)映射到整数id
(2)一个值的列表(正排数据):把列中的数值通过字典编码以后,将数字id存储在列表里面
(3)一个bitmap倒排索引:对于列里面的每个不同的值,都对应存储为一个bitmap,用来表示哪一行数据包含该值
3.2 为什么我们需要这三种数据结构呢?
1.通过字典将字符串映射成数字id,数字id通常比字符串更小,因此可以被更紧凑的存储。
2.第三点中的bitmap,通常被称为倒排索引,用于支持快速的过滤操作(bitmap对AND和OR操作的速度非常快)。
3.最后,第二点中的值列表将用于支持group by和topN查询,而普通的查询只需要根据filter出来的行去来聚合相应的metirc就可以了,而不需要访问上述2中的值列表。
下面以上面表格中的page列数据为例,来演示一下这三种数据结构,如下所示:
1.编码了列值的字典:
{
'Justin Bieber': 0,
'Ke$ha': 1
}
2.列值数据(正排):
[0, 0, 1, 1](第一个和第二个0代表第一,二行数据编码,即上述字典中的'Justin BieBer',第三个和第四个代表第三四行数据,即上述字典中的'Ke$ha')
3. Bitmaps:字典中的每个值对应一个bitmap
value='Justin Bieber': [1, 1, 0, 0]
value='Ke$ha': [0, 0, 1, 1]
注意:bitmap跟其他两种数据结构不同,其他两种数据结构都是跟随数据量的增长而线性增长的(最差情况下),而 bitmap的大小=数据的总行数 * 列中值的种类数 (也就是字典的size)。这就意味着如果值得的种类很多,那么bitmap中为1的数量将会非常稀疏,这种情况下bitmap有可能被大幅压缩。Druid针对这种情况,采用了特殊的压缩算法,比如roaring bitmap压缩算法。
倒排索引相关文档:http://hbasefly.com/2018/06/19/timeseries-database-8/?jqdajo=dz6ta
疑问:
公司的Druid设置的按小时进行分区,但是每个小时时间段生成的Segment文件数目却不是一份,另外发现有些文件的size为0,是因为这部分数据还没有push到磁盘上。