时间序列
您可以使用Redis Stack在Redis Enterprise中管理时间序列数据。
功能
- 按开始时间和结束时间查询
- 按标签集查询
- 任何时间段的聚合查询(最小值、最大值、平均值、总和、范围、计数、第一个、最后一个)
- 可配置的最大保留期限
- 压缩/汇总 - 自动更新的聚合时间序列
- 标签索引 - 每个键都有标签,允许通过标签进行查询
内存模型
时间序列是内存块的链表。 每个块都有预定义的样本大小。 每个样本是时间和128位值的元组, 64位用于时间戳,64位用于值。
时间序列功能
Redis Stack 提供了一种新的数据类型,它使用固定大小的内存块来存储时间序列样本,并通过与 Redis 流相同的 Radix Tree 实现进行索引。使用流,您可以创建一个有上限的流,有效地限制消息的数量。对于时间序列,您可以以毫秒为单位应用保留策略。这对于时间序列用例来说更好,因为它们通常对给定时间窗口内的数据感兴趣,而不是固定数量的样本。
下采样/压缩
下采样前 | 下采样后 |
---|---|
![]() |
![]() |
如果你想无限期地保留所有的原始数据点,你的数据集会随着时间的推移线性增长。然而,如果你的使用场景允许你在时间上保留较不精细的数据,可以应用降采样。这允许你通过使用给定的聚合函数对给定时间窗口内的原始数据进行聚合,从而保留较少的历史数据点。时间序列支持降采样,并提供以下聚合函数:avg、sum、min、max、range、count、first和last。
二级索引
在使用Redis的核心数据结构时,你只能通过知道确切的时间序列键来检索时间序列。不幸的是,对于许多时间序列用例(如根本原因分析或监控),你的应用程序不会知道它正在寻找的确切键。这些用例通常希望查询一组在几个维度上相互关联的时间序列,以提取你需要的洞察。你可以使用核心Redis数据结构创建自己的二级索引来帮助解决这个问题,但这会带来高昂的开发成本,并且需要你管理边缘情况以确保索引的正确性。
Redis 根据称为 labels 的 field value
对为您进行此索引。您可以为每个时间序列添加标签,并在查询时使用它们进行 filter。
以下是一个创建时间序列的示例,包含两个标签(sensor_id 和 area_id 是字段,其值分别为 2 和 32),并且保留窗口为 60,000 毫秒:
TS.CREATE temperature RETENTION 60000 LABELS sensor_id 2 area_id 32
读取时的聚合
当您需要查询时间序列时,如果您只对给定时间间隔内的平均值感兴趣,那么流式传输所有原始数据点会很麻烦。时间序列仅传输所需的最少数据,以确保最低延迟。
这是一个在5,000毫秒时间桶上进行聚合的示例:
127.0.0.1:12543> TS.RANGE temperature:3:32 1548149180000 1548149210000 AGGREGATION avg 5000
1) 1) (integer) 1548149180000
2) "26.199999999999999"
2) 1) (integer) 1548149185000
2) "27.399999999999999"
3) 1) (integer) 1548149190000
2) "24.800000000000001"
4) 1) (integer) 1548149195000
2) "23.199999999999999"
5) 1) (integer) 1548149200000
2) "25.199999999999999"
6) 1) (integer) 1548149205000
2) "28"
7) 1) (integer) 1548149210000
2) "20"
集成
Redis Stack 提供了与现有时间序列工具的多种集成。其中一个集成是我们的 RedisTimeSeries 适配器,用于 Prometheus,它将所有监控指标保留在时间序列中,同时利用整个 Prometheus 生态系统。

此外,我们还为Grafana创建了直接集成。此仓库包含RedisTimeSeries、其远程写入适配器、Prometheus和Grafana的docker-compose设置。它还附带了一组数据生成器和预构建的Grafana仪表板。
使用Redis的时间序列建模方法
数据建模方法
Redis流允许您在给定时间戳的消息中添加多个字段值对。对于每个设备,我们收集了10个指标,这些指标被建模为单个流消息中的10个独立字段。

对于有序集合,我们以两种不同的方式对数据进行了建模。对于“每个设备的有序集合”,我们将指标连接起来,并用冒号分隔,例如“
。

当然,这会消耗较少的内存,但在读取时需要更多的CPU周期来获取正确的指标。这也意味着更改每个设备的指标数量并不简单,这就是为什么我们还对第二种排序集方法进行了基准测试。在“每个指标的排序集”中,我们将每个指标保留在自己的排序集中,每个设备有10个排序集。我们以“
的格式记录值。

另一种替代方法是通过创建一个带有唯一键的哈希来规范化数据,以跟踪给定时间戳下给定设备的所有测量值。然后,这个键将成为排序集中的值。然而,在读取时间序列时,必须访问许多哈希,这将在读取时带来巨大的成本,因此我们放弃了这种方法。
每个时间序列都包含一个单一的指标。我们选择这种设计是为了维护Redis的原则,即大量的小键比少量的大键更好。

我们的基准测试没有利用时间序列的开箱即用的二级索引功能。Redis 在每个分片中保留了一个部分的二级索引,由于索引继承了它所索引的键的相同哈希槽,因此它始终托管在同一个分片上。这种方法会使原生数据结构的设置更加复杂,为了简化起见,我们决定不在基准测试中包含它。此外,虽然 Redis Enterprise 可以使用代理将像TS.MGET和TS.MRANGE这样的命令请求分发到所有分片并聚合结果,但我们选择在基准测试中也不利用这一优势。
数据摄取
在我们的基准测试的数据摄取部分,我们通过测量每秒可以摄取多少设备的数据来比较了四种方法。我们的客户端有8个工作线程,每个线程有50个连接,每个请求有50个命令的管道。
每种方法的摄取详情:
Redis 流 | 时间序列 | 每个设备的排序集合 |
每个指标的排序集合 |
|
---|---|---|---|---|
命令 | XADD | TS.MADD | ZADD | ZADD |
管道 | 50 | 50 | 50 | 50 |
每次请求的指标 | 5000 | 5000 | 5000 | 500 |
# 键 | 4000 | 40000 | 4000 | 40000 |

我们所有的数据摄取操作都在亚毫秒级的延迟下执行。尽管两者都使用了相同的Rax数据结构,但时间序列方法的吞吐量略高于Redis流。
每种方法都会产生不同的结果,这显示了针对特定用例进行原型设计的价值。正如我们在查询性能上看到的,每个设备的排序集提高了写入吞吐量,但以查询性能为代价。这是在数据摄取、查询性能和灵活性之间的一种权衡(记住之前的数据建模评论)。
读取性能
我们在本次基准测试中使用的读取查询查询了一个单一的时间序列,并通过保持每个时间桶中观察到的最大CPU百分比来将其聚合为一小时的时间桶。我们在查询中考虑的时间范围正好是一小时,因此返回了一个单一的最大值。对于时间序列来说,这是开箱即用的功能。
127.0.0.1:12543> TS.RANGE cpu_usage_user{1340993056} 1451606390000 1451609990000 AGGREGATION max 3600000
对于Redis流和有序集合的方法,我们创建了以下LUA脚本。客户端再次有8个线程和每个线程50个连接。由于我们执行了相同的查询,只有一个分片被击中,在所有四种情况下,这个分片的CPU都达到了100%。

在这里,您可以看到为特定用例配备专用数据结构的真正威力,以及与之配套的工具箱。使用时间序列超越了所有其他方法,并且是唯一能够实现亚毫秒级响应时间的方法。
内存利用率
对于Redis流和有序集合方法,样本都存储为字符串,而时间序列将它们存储为双精度浮点数。在这个特定的数据集中,我们选择了一个CPU测量值,其值为0-100之间的整数,因此作为字符串存储时占用两个字节的内存。使用时间序列时,每个指标具有64位精度。
与排序集方法相比,时间序列可以显著减少内存消耗。鉴于时间序列数据的无界性质,这通常是一个关键的评估标准——需要保留在内存中的整体数据集大小。Redis流进一步减少了内存消耗,但在需要更高精度的更多位数时,其内存消耗将与时间序列相等或更高。
