系统设计
系统设计
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 的计算还需要用户自己在外层实现。