系统设计
OpenTSDB 的优势在于数据的写入和存储能力,得益于底层依赖的HBase 所提供的能力。劣势在于数据查询和分析的能力上的不足,虽然在查询上已经做了很多的优化,但是不是所有的查询场景都能适用。可以说,OpenTSDB 在TagValue 过滤查询优化,是这次要对比的几个时序数据库中,优化的最差的。在GroupBy 和Downsampling 的查询上,也未提供Pre-aggregation 和Auto-rollup 的支持。不过在功能丰富程度上,OpenTSDB 的API 是支持最丰富的,这也让OpenTSDB 的API 成为了一个标杆。
数据模型
OpenTSDB 采用按指标建模的方式,一个数据点会包含以下组成部分:
metric:时序数据指标的名称,例如sys.cpu.user ,stock.quote 等。
timestamp:秒级或毫秒级的Unix 时间戳,代表该时间点的具体时间。
tags:一个或多个标签,也就是描述主体的不同的维度。Tag 由TagKey 和TagValue 组成,TagKey 就是维度,TagValue 就是该维度的值。
value:该指标的值,目前只支持数值类型的值。
存储模型
OpenTSDB 底层存储的优化思想,可以参考这篇文章 ,简单总结就是以下这几个关键的优化思路:
对数据的优化:为Metric 、TagKey 和TagValue 分配UniqueID ,建立原始值与UniqueID 的索引,数据表存储Metric 、TagKey 和TagValue 对应的UniqueID 而不是原始值。
对KeyValue 数的优化:如果对HBase 底层存储模型十分了解的话,就知道行中的每一列在存储时对应一个KeyValue ,减少行数和列数,能极大的节省存储空间以及提升查询效率。
对查询的优化:利用HBase 的Server Side Filter 来优化多维查询,利用Pre-aggregation 和Rollup 来优化GroupBy 和降精度查询。
UIDTable
接下来看一下OpenTSDB 在HBase 上的几个关键的表结构的设计,首先是tsdb-uid 表,结构如下:
Metric、TagKey 和TagValue 都会被分配一个相同的固定长度的UniqueID ,默认是三个字节。tsdb-uid 表使用两个ColumnFamily ,存储了Metric 、TagKey 和TagValue 与UniqueID 的映射和反向映射,总共是6 个Map 的数据。从图中的例子可以解读出:
TagKey 为’host’,对应的UniqueID 为'001'
TagValue 为’static’,对应的UniqueId 为'001'
Metric 为’proc.loadavg.1m’,对应的UniqueID 为'052'
为每一个Metric 、TagKey 和TagValue 都分配UniqueID 的好处,一是大大降低了存储空间和传输数据量,每个值都只需要3 个字节就可以表示,这个压缩率是很客观的;二是采用固定长度的字节,可以很方便的从row key 中解析出所需要的值,并且能够大大减少Java 堆内的内存占用(bytes 相比String 能节省很多的内存占用) ,降低GC 的压力。
不过采用固定字节的UID 编码后,对于UID 的个数是有上限要求的,3 个字节最多只允许有16777216 个不同的值,不过在大部分场景下都是够用的。当然这个长度是可以调整的,不过不支持动态更改。
DataTable
第二张关键的表是数据表,结构如下:
该表中,同一个小时内的数据会存储在同一行,行中的每一列代表一个数据点。如果是秒级精度,那一行最多会有3600 个点,如果是毫秒级精度,那一行最多会有3600000 个点。这张表设计的精妙之处在于row key 和qualifier (列名)的设计,以及对整行数据的compaction 策略。row key 格式为:
< metric >< timestamp >< tagk1 >< tagv1 >< tagk2 > tagv2 > ... < tagkn >< tagvn >
其中metric 、tagk 和tagv 都是用uid 来表示,由于uid 固定字节长度的特性,所以在解析row key 的时候,可以很方便的通过字节偏移来提取对应的值。Qualifier 的取值为数据点的时间戳在这个小时的时间偏差,例如如果你是秒级精度数据,第30 秒的数据对应的时间偏差就是30 ,所以列名取值就是30 。列名采用时间偏差值的好处,主要在于能大大节省存储空间,秒级精度的数据只要占用2 个字节,毫秒精度的数据只要占用4 个字节,而若存储完整时间戳则要6 个字节。整行数据写入后,OpenTSDB 还会采取compaction 的策略,将一行内的所有列合并成一列,这样做的主要目的是减少KeyValue 数目。
查询优化
HBase 仅提供简单的查询操作,包括单行查询和范围查询。单行查询必须提供完整的RowKey ,范围查询必须提供RowKey 的范围,扫描获得该范围下的所有数据。通常来说,单行查询的速度是很快的,而范围查询则是取决于扫描范围的大小,扫描个几千几万行问题不大,但是若扫描个十万上百万行,那读取的延迟就会高很多。
OpenTSDB 提供丰富的查询功能,支持任意TagKey 上的过滤,支持GroupBy 以及降精度。TagKey 的过滤属于查询的一部分,GroupBy 和降精度属于对查询后的结果的计算部分。在查询条件中,主要的参数会包括:metric 名称、tag key 过滤条件以及时间范围。上面一章中指出,数据表的rowkey 的格式为:
< metric >< timestamp >< tagk1 >< tagv1 >< tagk2 > tagv2 > ... < tagkn >< tagvn >
从查询的参数上可以看到,metric 名称和时间范围确定的话,我们至少能确定row key 的一个扫描范围。但是这个扫描范围,会把包含相同metric 名称和时间范围内的所有的tag key 的组合全部查询出来,如果你的tag key 的组合有很多,那你的扫描范围是不可控的,可能会很大,这样查询的效率基本是不能接受的。
Server side filter
HBase 提供了丰富和可扩展的filter ,filter 的工作原理是在server 端扫描得到数据后,先经过filter 的过滤后再将结果返回给客户端。Server side filter 的优化策略无法减少扫描的数据量,但是可以大大减少传输的数据量。OpenTSDB 会将某些条件的tag key filter 转换为底层HBase 的server side filter ,不过该优化带来的效果有限,因为影响查询最关键的因素还是底层范围扫描的效率而不是传输的效率。
减少范围查询内扫描的数据量
要想真正提高查询效率,还是得从根本上减少范围扫描的数据量。注意这里不是减小查询的范围,而是减少该范围内扫描的数据量。这里用到了HBase 一个很关键的filter ,即FuzzyRowFilter ,FuzzyRowFilter 能够根据指定的条件,在执行范围扫描时,动态的跳过一定数据量。但不是所有OpenTSDB 提供的查询条件都能够应用该优化,需要符合一定的条件,具体要符合哪些条件就不在这里说明了,有兴趣的可以去了解下FuzzyRowFilter 的原理。
范围查询优化成单行查询
这个优化相比上一条,更加的极端。优化思路非常好理解,如果我能够知道要查询的所有数据对应的row key ,那就不需要范围扫描了,而是单行查询就行了。这里也不是所有OpenTSDB 提供的查询条件都能够应用该优化,同样需要符合一定的条件。单行查询要求给定确定的row key ,而数据表中row key 的组成部分包括metric 名称、timestamp 以及tags ,metric 名称和timestamp 是能够确定的,如果tags 也能够确定,那我们就能拼出完整的row key 。所以很简单,如果要能够应用此优化,你必须提供所有tag key 对应的tag value 才行。
总结
以上就是OpenTSDB 对HBase 查询的一些优化措施,但是除了查询,对查询后的数据还需要进行GroupBy 和降精度。GroupBy 和降精度的计算开销也是非常可观的,取决于查询后的结果的数量级。对GroupBy 和降精度的计算的优化,几乎所有的时序数据库都采用了同样的优化措施,那就是pre-aggregation 和auto-rollup 。思路就是预先进行计算,而不是查询后计算。不过OpenTSDB 在已发布的最新版本中,还未支持pre-aggregation 和rollup 。而在开发中的2.4 版本中,也只提供了半吊子的方案,它只提供了一个新的接口支持将pre-aggregation 和rollup 的结果进行写入,但是对数据的pre-aggregation 和rollup 的计算还需要用户自己在外层实现。