今天,我们很高兴宣布 RedisTimeSeries v1.0 正式发布 (GA)。RedisTimeSeries 是 Redis 开发的一个 Redis 模块,旨在增强您使用 Redis 管理时间序列数据的体验。我们在六个月前发布了 RedisTimeSeries 预览/测试版,感谢社区和客户在开发第一个 GA 版本时提供的宝贵反馈和建议。为了庆祝此次发布,我们进行了基准测试,结果表明 RedisTimeSeries 的每秒查询数为 125K,而 Redis 中的其他时间序列方法的每秒查询数则更低。 直接跳转到完整结果,或者花点时间先了解一下促使我们构建此新模块的原因。
许多 Redis 用户已经使用 Redis 处理时间序列数据将近十年,并且在使用过程中收获了喜悦和成功。正如我们稍后将解释的那样,这些开发人员正在使用 Redis 的通用本机数据结构。所以让我们先退一步,解释一下为什么我们决定构建一个具有专用时间序列数据结构的模块。
在下面的 DB-engines 趋势图中,您可以看到时间序列数据库最近在受欢迎程度方面获得了最大的增长。除了不断增长的数据量和自动驾驶汽车、算法交易、智能家居、在线零售等新的时间序列用例外,我们认为这种趋势还有两个主要的技术原因。
第一个原因是时间序列数据的查询模式和规模与现有数据库技术的设计目标不同。虽然大多数数据库旨在提供比写入更多的读取,但时间序列用例具有高吞吐量的大量数据摄取率,而读取查询数量较少。在根本原因分析用例中,读取是零星发生的,并且只涉及数据集的随机部分。在训练 AI 模型的用例中(例如,用于传感器数据的异常检测),读取通常会跨越数据集的较大一部分,但仍然比写入频率低得多。由于 Redis 具有 可扩展的架构,它能够以低延迟提供高写入吞吐量,因此 Redis 非常适合当前的时间序列查询模式。
这种趋势的第二个原因是,传统数据库技术中不具备处理时间序列所需的工具集。有效利用资源需要进行一些结构性更改,例如自动对历史时间序列数据进行降采样以及 双增量编码,以及用于直观查询和聚合时间序列数据的功能。传统数据库技术会在应用程序端引入大量工作,以解决保留、降采样和聚合等功能。
在 Redis,我们坚信实践出真知。对于我们的云产品(管理着运行在数千个 Redis Enterprise 集群上的超过 100 万个 Redis 数据库),我们会在内部 Redis 数据库中收集每个集群的指标。在旨在增强我们自身基础架构指标的内部项目中,我们亲身经历了将核心 Redis 数据结构用于时间序列用例的局限性和开发工作量。我们认为必须有更好、更高效的方法。除了上面提到的特定于工具集的功能之外,我们还希望开箱即用的二级索引,以便我们可以高效地查询时间序列集。
在 Redis 中,有两种方法可以使用时间序列,同时重用现有数据结构:有序集合和流。许多文章解释了如何使用 Redis 核心数据结构对时间序列进行建模。以下是一些我们将在稍后的基准测试中使用到的关键原则。
有序集合根据其分数存储值。对于时间序列数据,分数是观察事件的时间戳。该值重复时间戳,然后是分隔符和实际度量值,例如“<timestamp>:<measurement>”。这样做是为了使有序集合中的每个值都应该是唯一的。或者,该值保存唯一键的名称,该键存储一个哈希,其中可以为给定的时间戳保留更多数据或度量值。
这种方法的缺点
Redis 流是最近添加的数据结构(因此目前不太常用于时间序列),与有序集合相比,它占用的内存更少,并使用 Rax(基数树的独立实现)实现。总的来说,与有序集合相比,Redis 流提高了插入和读取的性能,但仍然缺少特定于时间序列的工具集,因为它被设计为一种通用数据结构。
这种方法的缺点
在 RedisTimeSeries 中,我们引入了一种新的数据类型,它使用固定大小的内存块来存储时间序列样本,并通过与 Redis 流相同的基数树实现进行索引。使用流,您可以创建 一个有限流,有效地限制消息的数量。在 RedisTimeSeries 中,您可以应用毫秒级的保留策略。这更适合时间序列用例,因为它们通常对给定时间窗口内的数据感兴趣,而不是固定数量的样本。
如果您想无限期地保留所有原始数据点,那么您的数据集将随着时间的推移线性增长。但是,如果您的用例允许您在更早的时间具有更粗粒度的数据,则可以应用降采样。这使您可以通过使用给定聚合函数对给定时间窗口的原始数据进行聚合来保留更少的历史数据点。 RedisTimeSeries 支持降采样,并支持以下聚合:avg、sum、min、max、range、count、first 和 last。
当使用 Redis 的核心数据结构时,您只能通过知道保存时间序列的精确键来检索时间序列。不幸的是,对于许多时间序列用例(例如根本原因分析或监控),您的应用程序将不知道它要查找的确切键。这些用例通常希望查询一组彼此相关的时间序列,以提取所需的见解。您可以使用核心 Redis 数据结构创建自己的二级索引来帮助您实现这一点,但这将带来很高的开发成本,并且需要您管理边缘情况以确保索引的正确性。
RedisTimeSeries 会根据您可以添加到每个时间序列的 `field value` 对(又称标签)为您执行此索引,并在查询时使用这些标签进行筛选(在我们的 文档 中提供了这些过滤器的完整列表)。以下是使用两个标签(sensor_id 和 area_id 分别为值 2 和 32)以及 60,000 毫秒的保留窗口创建时间序列的示例
TS.CREATE temperature RETENTION 60000 LABELS sensor_id 2 area_id 32
当您需要查询时间序列时,如果您只对给定时间间隔内的平均值感兴趣,那么流式传输所有原始数据点会很麻烦。RedisTimeSeries 遵循 Redis 的理念,只传输最低限度所需的数据,以确保最低延迟。以下是在 5,000 毫秒的时间段上使用 聚合函数进行聚合查询的示例
127.0.0.1:6379> 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"
RedisTimeSeries 与现有的时间序列工具进行了多种集成。其中一个集成是我们的 RedisTimeSeries 适配器,它适用于 Prometheus,可以将所有监控指标保存在 RedisTimeSeries 中,同时利用整个 Prometheus 生态系统。
此外,我们还为 Grafana 和 Telegraph 创建了直接集成。 此仓库 包含 RedisTimeSeries、其远程写入适配器、Prometheus 和 Grafana 的 docker-compose 设置。它还包含一组数据生成器和预建的 Grafana 仪表板。
为了展示我们新发布的 GA RedisTimeSeries 模块的强大功能,我们对其进行了基准测试,与三种常见的处理时间序列数据的技术进行了比较。我们使用客户端-服务器设置,在两台独立的机器上,以便比较有序集合、流和 RedisTimeSeries 在摄取、查询时间和内存消耗方面的性能。
具体来说,我们的设置包括
Redis Streams 允许您为给定时间戳添加消息中的多个字段值对。对于每个设备,我们收集了 10 个指标,这些指标在一个流消息中被建模为 10 个独立的字段。
对于有序集合,我们以两种不同的方式对数据进行建模。对于“每个设备一个有序集合”,我们将指标连接起来,并用冒号分隔它们,例如“<时间戳>:<指标 1>:<指标 2>: … :<指标 10>”。
当然,这会消耗更少的内存,但在读取时需要更多的 CPU 周期才能获取正确的指标。这也意味着更改每个设备的指标数量并不容易,这就是我们也对第二种有序集合方法进行基准测试的原因。在“每个指标一个有序集合”中,我们将每个指标保留在它自己的有序集合中,每个设备有 10 个有序集合。我们将值记录为“<时间戳>:<指标>”。
另一种替代方法是通过创建具有唯一密钥的哈希来规范化数据,以跟踪给定时间戳的给定设备的所有测量值。然后,此密钥将成为有序集合中的值。但是,在读取时间序列时需要访问许多哈希会带来巨大的成本,因此我们放弃了这种方法。
在 RedisTimeSeries 中,每个时间序列保存一个指标。我们选择这种设计是为了保持 Redis 的原则,即大量的小密钥比少量的大密钥更好。
需要注意的是,我们的基准测试没有利用 RedisTimeSeries 的开箱即用的二级索引功能。该模块在每个分片中保留一个部分二级索引,并且由于索引继承了它索引的密钥的相同哈希槽,因此它始终托管在同一个分片上。这种方法将使原生数据结构的设置变得更加复杂,因此为了简单起见,我们决定不将其包含在我们的基准测试中。此外,虽然 Redis 企业版可以使用 代理 将 TS.MGET 和 TS.MRANGE 等命令的请求分发到所有分片并聚合结果,但我们选择在基准测试中也不利用这种优势。
在基准测试的数据摄取部分,我们通过测量每秒可以摄取多少设备数据来比较这四种方法。我们的客户端有 8 个工作线程,每个线程有 50 个连接,以及每个请求 50 个命令的管道。
Redis Streams | RedisTimeSeries | 有序集合 每个设备 |
有序集合 每个指标 |
|
命令 | XADD | TS.MADD | ZADD | ZADD |
管道 | 50 | 50 | 50 | 50 |
每个请求的指标数 | 5000 | 5000 | 5000 | 500 |
密钥数量 | 4000 | 40000 | 4000 | 40000 |
表 1:每种方法的摄取细节
我们所有的摄取操作都以毫秒级延迟执行,尽管两者都使用相同的 Rax 数据结构,但 RedisTimeSeries 方法的吞吐量略高于 Redis Streams。
如您所见,使用有序集合的两种方法产生的吞吐量差异很大。这表明始终针对特定用例对方法进行原型设计的重要性。正如我们将在查询性能中看到的那样,每个设备一个有序集合可以提高写入吞吐量,但以查询性能为代价。对于您的用例,这是摄取、查询性能和灵活性的权衡(请记住我们之前提到的数据建模说明)。
我们在该基准测试中使用的读取查询查询了一个时间序列,并通过在每个时段内保留观察到的最大 CPU 百分比,将其聚合为一小时的时间段。我们在查询中考虑的时间范围正好是一小时,因此返回了一个最大值。对于 RedisTimeSeries,这是开箱即用的功能(如前所述)。
TS.RANGE cpu_usage_user{1340993056} 1451606390000 1451609990000 AGGREGATION max 3600000
对于 Redis Streams 和有序集合方法,我们创建了 以下 LUA 脚本。客户端再次有 8 个线程,每个线程有 50 个连接。由于我们执行的是相同的查询,因此只有一个分片被命中,并且在这四种情况下,该分片都达到了 100% 的 CPU 使用率。
在这里,您可以看到为给定用例提供专用数据结构以及随之运行的工具箱的真正力量。RedisTimeSeries 远远超过了其他所有方法,并且是唯一一个能够实现毫秒级响应时间的方案。
在 Redis Streams 和有序集合方法中,样本都保留为字符串,而在 RedisTimeSeries 中则保留为双精度浮点数。在这个特定数据集中,我们选择了一个 CPU 测量值,其四舍五入后的整数值介于 0-100 之间,因此作为字符串消耗 2 个字节的内存。但是,在 RedisTimeSeries 中,每个指标都有 64 位精度。
与两种有序集合方法相比,可以看出 RedisTimeSeries 显著降低了内存消耗。考虑到时间序列数据的无界性,这通常是评估的关键标准 - 需要在内存中保留的整个数据集的大小。Redis Streams 进一步降低了内存消耗,但如果需要更多位数以获得更高的精度,则其内存消耗将与 RedisTimeSeries 相同或更高。
在选择您的方法时,您需要了解您的时间序列用例的摄取率、查询工作负载、整个数据集的大小和内存占用。正如我们所看到的,在 Redis 中对时间序列数据进行建模有几种方法,每种方法都有不同的特点。RedisTimeSeries 提供了一种新方法,将时间视为一等公民,并提供如前所述的开箱即用的时间序列工具箱。它将高效的内存使用与非凡的查询性能相结合,并在摄取过程中开销很小。这使实时分析时间序列数据的愿望成为现实。
我们对我们在 RedisTimeSeries 的 1.0 GA 版本中取得的成就感到满意,但这仅仅是开始。我们希望听到您的反馈,以便将其纳入我们的路线图。与此同时,以下是我们接下来计划做的事情的清单。
我们坚信,所有具有时间序列用例的 Redis 用户都将从使用 RedisTimeSeries 中受益。如果您仍然不相信,并且想要亲自尝试一下,这里有一个 快速入门指南。