虽然TSDB被设计为只要有空间就可以存储原始全分辨率(resolution)的数据,但是对于广泛的时间范围或在许多标签组合之上的查询可能是相当痛苦的。这样的查询可能需要很长时间才能完成,或者在最糟糕的情况下,可能会导致内存不足的异常。从OpenTSDB 2.4开始,一组新的API允许存储和查询较低分辨率的数据以更快地回答这样的查询。本页将概述上卷和预聚合是什么,它们如何在TSDB中工作以及如何最好地使用它们。查看API部分的具体实现细节。
注意:
OpenTSDB本身不计算和存储上卷或预聚合数据。有多种计算结果的方法,但是根据规模和精度要求,它们都有好处和缺点。请参阅生成上卷和预聚合部分,讨论如何创建此数据。
示例数据
为了帮助描述较低分辨率的数据,我们来看一些完整的分辨率(也称为原始数据)的示例数据。第一个表格用快捷标识符定义时间序列。
序列ID | Metric | Tag1 | Tag2 | Tag3 |
---|---|---|---|---|
ts1 | system.if.bytes.out | host=web01 | colo=lga | interface=eth0 |
ts2 | system.if.bytes.out | host=web02 | colo=lga | interface=eth0 |
ts3 | system.if.bytes.out | host=web03 | colo=sjc | interface=eth0 |
ts4 | system.if.bytes.out | host=web04 | colo=sjc | interface=eth0 |
请注意,它们都具有相同metric和interface标记,但不同host和colo标签。
接下来以15分钟的时间间隔写出一些数据:
序列ID | 12:00 | 12:15 | 12:30 | 12:45 | 13:00 | 13:15 | 13:30 | 13: 45 |
---|---|---|---|---|---|---|---|---|
ts1 | 1 | 4 | -3 | 8 | 2 | -4 | 5 | 2 |
ts2 | 7 | 2 | 8 | -9 | 4 | 1 | 1 | |
ts3 | 9 | 3 | -2 | -1 | 6 | 3 | 8 | 2 |
ts4 | 2 | 5 | 2 | 8 | 5 | -4 | 7 |
请注意,有些数据点丢失。有了这些数据集,我们先看看上卷。
上卷
在OpenTSDB中,“上卷”被定义为随着时间的推移聚合汇总的单个时间序列。它也可以被称为“基于时间的聚合”。上卷帮助解决在广泛的时间跨度的问题。例如,如果每60秒写入一个数据点并查询一年的数据,则时间序列将返回超过525,000个单独的数据点。图表会有很多点并且很可能会相当凌乱。相反,可能需要查看较低分辨率的数据,例如,只需要8k左右1小时数据进行绘图。然后,可以识别异常并向下钻取更精细的分辨率数据。
如果您已经使用OpenTSDB查询数据,则您可能熟悉将每个时间序列聚合为更小或更低分辨率值的降采样器。上卷实质上是系统中存储的降采样的结果,并随意调用。每个上卷(或降采样器)需要两条信息:
- 间隔(Interval): 多少时间“滚动”到新的值。例如,一个小时(1h)的数据或一天(1d)的数据。
- 聚合函数:对基础值进行了什么算术运算才能得到新值。例如sum:累加所有的值或max:存储最大值。
警告
存储上卷时,最好避免平均值,中值或偏差等函数(average, median or deviation)。在进一步降采样或分组聚合时,这些值变得毫无意义。相反,总是存储总和(sum)和计数(count)要好得多,至少可以在查询时计算平均值。有关更多信息,请参阅下面的部分。
上卷数据点的时间戳应该捕捉到(snap to)上卷间隔的顶部。例如,如果汇总间隔是1h那么它包含1小时的数据,并应该快速(snap to)到小时的顶部。(因为所有的时间戳都是用Unix Epoch格式编写的,定义为UTC时区,所以这将是一个小时UTC时间的开始)。
上卷示例
对于前述给定的数据,使用1h的间隔存储sum和count。
序列ID | 12:00 | 13:00 |
---|---|---|
ts1 SUM | 10 | 5 |
ts1 COUNT | 4 | 4 |
ts2 SUM | 8 | 6 |
ts2 COUNT | 4 | 3 |
ts3 SUM | 9 | 19 |
ts3 COUNT | 4 | 4 |
ts4 SUM | 9 | 16 |
ts4 COUNT | 3 | 4 |
请注意,无论间隔“bucket”中的第一个数据点何时出现,所有时间戳都对齐到小时的顶部。另请注意,如果一个数据点在一段时间内不存在,则计数较低。
在一般情况下,在每个时间序列存储上卷时,目标应该是计算和存储MAX,MIN,SUM和COUNT。
平均上卷示例
当启用上卷并且请求使用OpenTSDB 的avg函数的降采样器时,TSD将扫描存储中SUM和COUNT值。然后在迭代数据的时候,它会精确地计算平均值。
COUNT和SUM值的时间戳必须匹配。但是,如果一个SUM的预期计数COUNT值缺失,则SUM将会被踢出结果。从上面的例子中,现在我们丢失了一个计数数据点ts2。
序列ID | 12:00 | 13:00 |
---|---|---|
ts1 SUM | 10 | 5 |
ts1 COUNT | 4 | 4 |
ts2 SUM | 8 | 6 |
ts2 COUNT | 4 |
所得到的一个2h降采样的avg查询应该是这样的:
序列ID | 12:00 |
---|---|
ts1 AVG | 1.875 |
ts2 AVG | 2 |
ts1: (10+5)/8=1.875
ts2: 8/4=2
预聚合
虽然上卷可以帮助处理较长时间的查询,但是如果度量具有较高的基数(即给定度量的唯一时间序列数量),仍然可能遇到小范围查询性能问题。在上面的例子中,我们有4台Web服务器。即使我们有10,000台服务器。获取网络流量(interface traffic)的总和或平均值可能会相当慢。如果用户经常拉取大量分组集合(或一些将它作为空间集合)的话,存储聚合和查询替代很有意义,这样会获取更少的数据。
与上卷不同,预聚合只需要一条额外的信息:
- 聚合函数:对基础值进行了什么算术运算才能得到新值。例如sum:累加所有的值或max:存储最大值。
在OpenTSDB中,预聚合与其他带有特殊标签的时间序列不同。默认标签key是_aggregate(可通过tsd.rollups.agg_tag_key配置)。然后用于生成数据的聚合函数以大写形式存储在标签值中。让我们看一个例子:
预聚合示例
考虑到顶部的示例,我们可能想要查看colo(数据中心)的总网络流量。在这种情况下,我们可以通过SUM和COUNT聚合,类似于上卷。结果将是四个伴随元数据的新的时间序列,如:
序列ID | Metric | Tag1 | Tag2 |
---|---|---|---|
ts1’ | system.if.bytes.out | colo=lga | _aggregate=SUM |
ts2’ | system.if.bytes.out | colo=lga | _aggregate=COUNT |
ts3’ | system.if.bytes.out | colo=sjc | _aggregate=SUM |
ts4’ | system.if.bytes.out | colo=sjc | _aggregate=COUNT |
请注意,这些时间序列已经丢弃了host和interface标签。这是因为,聚合过程中,取了host和interface的多个不同的值,已经包装到这个新的序列,如果再让他们作为标签,不会再有意义。另外请注意,我们在存储的数据中注入了新的标签_aggregate。现在查询可以通过指定一个_aggregate值来访问这些数据。
注意:
启用了上卷后,如果您计划使用预聚合,则可能需要通过让TSDB自动注入_aggregate=RAW来帮助区分预聚合中的原始数据。只需将tsd.rollups.tag_raw属性设置为true。
现在结果数据如下:
序列ID | 12:00 | 12:15 | 12:30 | 12:45 | 13:00 | 13:15 | 13:30 | 13:45 |
---|---|---|---|---|---|---|---|---|
ts1’ | 8 | 6 | 5 | -1 | 6 | -4 | 6 | 3 |
ts2’ | 2 | 2 | 2 | 2 | 2 | 1 | 2 | 2 |
ts3’ | 9 | 5 | 3 | 1 | 14 | 8 | 4 | 9 |
ts4’ | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
由于我们正在通过聚合(通过colo分组)来执行分组,所以我们为每个时间戳从原始数据集中获取一个值。在这种情况下,我们不采用降采样或者上卷。
聚合算法:
根据colo分组,因此ts1与ts2一组,ts3与ts4一组。
ts1’ => SUM: 1+7=8 4+2=6 …
ts2’ => COUNT: 2 2 …
ts3’ => SUM: 9 3+2=5 …
ts4’ => COUNT: 1 2 …
警告
和上卷一样,在编写预聚合时,最好避免平均值,中值或偏差等函数。只是存储总和和计数。
上卷 预聚合
虽然预聚合肯定有助于高基数的指标,但用户可能仍然希望查询访问广泛的时间跨度,但遇到缓慢的查询。谢天谢地,可以像原始数据一样进行上卷预聚合。只要生成预聚合,然后使用上面的信息将其上卷起来。
生成上卷和预聚合
目前,TSD不会为您生成上卷数据或预聚合数据。主要原因是意味着OpenTSDB处理大量的时间序列数据,所以单个TSD专注于尽可能快地将数据存储到存储中。
问题
由于TSD的(基本上)无状态特性,它们可能不具备用于执行预聚合的全套数据。例如,我们的样本ts1数据可能在写入TSD_A的同时ts2被写入TSD_B。不从存储中读取数据,也不能执行正确的分组。我们也不知道我们应该在什么时候进行预聚合。我们可以等待1分钟,然后预先聚合数据,但会错过那一分钟后发生的任何事情。或者我们可以等待一个小时,预先聚合的查询将不会有最后一个小时的数据。如果数据来得太晚,会发生什么?
此外,对于上卷,根据用户向TSD写入数据的方式,对于ts1来说,我们可能会收到TSD_A上12:15的数据点,但是数据在12:30到达了TSD_B,因此整个小时都不需要数据(neither has the data required for the full hour)。时间窗口限制也适用于上卷。
解决方案
使用上卷和预聚合需要一些分析和各种权衡之间的选择。由于一些OpenTSDB用户已经具备了计算这类数据的方式和手段,我们只需提供API来存储和查询。但是,这里有一些关于如何自己计算这些的提示。
批处理
其他时间序列数据库常用的一种方法是在延迟一段时间后从数据库中读取数据,计算出预聚合和上卷,然后写入数据。这是解决这个问题的最简单的方法,在小规模下运作良好。但是仍然有一些问题:
- 随着数据的增长,生成上卷的查询也会增长,以至于查询负载影响写入和用户查询性能。在HBase下启用数据Compaction时,OpenTSDB会遇到同样的问题。
- 同样随着数据的增长,更多的数据意味着批处理需要更长的时间,并且必须跨多个Worker共享,这可能是一个协调和排除故障的难题。
- 延迟或历史数据可能不会上卷,除非有一些跟踪手段以触发旧数据生成新批次。
改进批处理的一些方法包括:
- 从副本系统中读取数据,例如,如果设置了HBase副本,则可以让用户从Master系统查询从副本存储读取来聚合。
- 从其他关联存储读取。一个例子是将所有数据镜像到另一个存储(如HDFS),并针对该数据运行批处理作业。
在TSD上排队
某些数据库使用的另一个选项是将所有数据排列在进程中的内存中,并在经过配置的时间窗口后写入结果。但是由于TSD是无状态的,通常用户在其TSD之前放置一个负载均衡器,所以单个TSD可能无法得到整个上卷或预聚合的所有数据进行计算(如上所述)。为了使这种方法起作用,上游收集器必须将计算所需的所有数据发送到特定的TSD。这不是一个困难的任务,但面临的问题包括:
- 有足够的RAM或磁盘空间来为每个TSD本地化存储(spool吐)数据。
- 如果一个TSD进程死亡,您将要么丢失聚合的数据,要么必须从存储引导(bootstrapped)。
- 每当聚合计算发生时,原始数据的整体写入吞吐量就会受到影响。
- 仍然有延迟/历史数据问题。
- 由于TSDB是基于JVM的,将所有这些数据保存在RAM中,然后运行GC将会受到很多影响。(假脱机(spooling)到磁盘更好,但是你会遇到IO问题)
一般来说,在Writer上排队是一个坏主意。避免痛苦。
流式处理
处理上卷和预聚合的一种更好的方法是将数据发送到流处理系统,在那里它可以被近乎实时地处理并写入到TSD中。它类似于TSD上的排队选项,但使用无数的流处理框架之一(Storm,Flink,Spark等)来处理消息路由和内存中的存储。然后,您只需编写一些代码来计算聚合,并在窗口过去后将数据吐出。
这是许多下一代监控解决方案所使用的解决方案,例如雅虎!雅虎正在努力为需要大规模监控的其他人开放流处理系统,并将其整齐地插入到TSDB中。
虽然流处理更好,但您仍然遇到以下问题:
- 足够的资源让stream workers去完成他们的作业。
- 死掉的stream worker需要从存储引导。
- 延迟/历史数据必须处理。
共享
如果您有计算聚合的工作代码,请与OpenTSDB组共享。如果您的解决方案是开源的,我们可能会将其纳入OpenTSDB生态系统。
配置
对于Opentsdb 2.4,上卷配置由opentsdb.conf中的Key tsd.rollups.config引用。此键的值必须是引号转义的JSON字符串,不带换行符,或者最好是包含配置的JSON文件的路径。文件名必须如rollup_config.json一样以.json 结尾。
JSON配置应该如下所示:
{
"aggregationIds": {
"sum": 0,
"count": 1,
"min": 2,
"max": 3
},
"intervals": [{
"table": "tsdb",
"preAggregationTable": "tsdb-preagg",
"interval": "1m",
"rowSpan": "1h",
"defaultInterval": true
}, {
"table": "tsdb-rollup-1h",
"preAggregationTable": "tsdb-rollup-preagg-1h",
"interval": "1h",
"rowSpan": "1d"
}]
}
两个顶级的字段包括:
aggregationIds: 将OpenTSDB聚合函数名称映射到用于压缩存储的数字标识符的映射。
intervals: 包含表名和间隔定义的一个或多个间隔定义的列表。
aggregationIds
aggregation ids映射用于通过将每种类型的上卷数据预先加上数字ID来减少存储量,而不是拼出完整的汇总函数。例如,如果我们COUNT:为每个值的每个列(或压缩列)添加6个字节,我们可以使用一个ID保存。
ID必须是从0到127的整数。这意味着我们可以在每个间隔中存储多达128个不同的上卷。在map中只能为每个数值提供一个ID,并且只能为每个类型给定一个聚合函数。如果函数名称没有映射到OpenTSDB支持的聚合函数,则会在启动时引发异常。同样,至少要给定一个聚合,才能启动TSD。
警告
一旦开始写入数据,聚合ID将无法更改。如果更改映射,则可能会返回不正确的数据,或者查询和写入操作可能会失败。您可以随时添加函数,但不能改变映射。
intervals
每个间隔对象都定义了表格路由,以便上卷和预先聚合的数据写入并查询。有两种类型的间隔:
- 默认 - 这是默认的,原始数据,OpenTSDB表通过"defaultInterval":true定义。对于现有的安装部署,是tsdb表或tsd.storage.hbase.data_table定义的任意表。忽略间隔和跨度,默认为OpenTSDB 1小时行宽,并存储给定分辨率和时间戳的数据。每个TSD和配置一次只能配置一个默认值。
- 上卷间隔 - 任何设置为"defaultInterval":false或未设置默认间隔。这些是上卷表,其中值被捕捉到间隔边界
应该定义下列字段:
名称 | 数据类型 | Required | 描述 | 示例 |
---|---|---|---|---|
table | String | Required | 非预先汇总的数据的基础或上卷表。对于默认表,应该是tsdb或现有的原始数据被写入的表。对于上卷的数据,它必须与原始数据不同表。 | tsdb-rollup-1h |
preAggregationTable | String | Required | 应该写入预先聚合的数据和(可选)上卷数据的表。它可能与table值相同,为同一张表。 | tsdb-rollup-preagg-1h |
interval | String | Required | 格式为“ |
1h |
rowSpan | String | Required | 存储中每行的宽度。此值必须大于interval和interval中定义的数字。 例如,如果interval是1h,rowSpan是1天,则每行会有24个值。 | 1d |
defaultInterval | Boolean | Optional | 是否将配置的时间间隔作为原始未滚动数据的默认值。 | true |
在存储中,上卷的编写类似于原始数据,每行都有一个基准时间戳,每个数据点是相对于基准时间的偏移量。每个偏移量都是基准时间以外的增量,而不是实际的偏移量。例如,如果一行存储1天of 1小时的数据,则会有多达24个偏移量。偏移0将映射到午夜的行,偏移量5将映射到上午6点。因为上卷偏移量是以14位编码的,所以如果一行中存储的间隔太多以便适应14位,TSD启动时会引发错误。
警告
将数据写入TSD之后,不要更改上卷间隔的间隔宽度或行跨度。这将导致垃圾数据和可能失败的查询。