dot 未来之快即将在您所在的城市举办活动。

在 Redis 发布会上加入我们

RedisTimeSeries 版本 1.2 已发布!

当您思考时,“生活就是一个时间序列”。 

所以,即使RedisTimeSeries 模块——它简化了 Redis 在物联网、应用程序监控和遥测等时间序列用例中的使用——自 2019 年 6 月才正式上市,我们已经看到许多有趣的用例也不足为奇。例如,纽约时报 在它的内部Photon 项目中使用了 RedisTimeSeries,亚马逊网络服务物联网 Greengrass项目也是如此(查看 GitHub 代码库此处)。

但我们不会躺在功劳簿上休息。相反,我们宣布了模块一个主要新版本(RedisTimeSeries 1.2)的正式上市,它首次亮相一系列强大的新功能。但鉴于广泛的兴趣,我们对于 RedisTimeSeries 1.2 的主要目标是不降低性能的情况下增加可用性。由于 Redis 是一个内存中数据库,在 DRAM 中存储大量时间序列数据会变得昂贵。所以在版本 1.2 中,我们添加了压缩,在大多数情况下,它将内存需求最多降低 90%——理论上,它可以将你的时间序列数据压缩最多 98%!我们让我们的 API 更加一致且直观,并删除了冗余响应数据,以进一步提高性能。同时,API 更改和压缩将读取性能提高最多 70%将所有内容集合起来,RedisTimeSeries 的新版本可以在不影响性能的情况下为用户节省用于基础设施成本的大笔资金。

(查看我们在RedisTimeSeries 1.2 基准测试上的关联博客文章了解更多有关数据集中时间序列的数量、压缩和时间序列的样本数量与摄取吞吐量无关的信息。)

但让我们从压缩的工作原理开始,因为它非常酷。

大猩猩压缩

RedisTimeSeries 中的时间序列由一个链表组成,该链表包含固定数量样本的块。样本是时间戳的元组,表示特定时间点的测量值。时间表示为时间戳,而值表示为浮点数(双精度浮点格式)。这些块本身在基数树中索引。您可以在此处阅读有关内部原理的更多信息。 

未压缩时,时间戳和值每个样本使用 8 个字节(或总共 16 个字节/128 位)。对于时间戳,我们使用双重差值压缩,而对于值,我们使用XOR(“异或”)压缩。这两种方法都基于Facebook Gorilla 论文,记录在源代码中,但我们将在下面解释其工作原理。

时间戳的双重差分编码

在许多时间序列用例中,样品以固定间隔收集。想象一下我们每 5 秒从温度传感器收集测量值。以毫秒为单位,连续两个样本之间的差值将为 5,000。但是,由于间隔保持不变,因此两个差值之间的差值——双重差值——将为 0。

偶尔数据会在第 6 秒或第 4 秒到达,但这通常是例外。与其以 64 位存储此 ΔΔ(双重差值),可变长度编码会根据以下伪代码使用:

如果 ΔΔ为零,则存储单个“0”位
否则,如果ΔΔ在 [-63, 64] 之间,则存储“10”,后跟值(7 位)
否则,如果ΔΔ在 [-512,511] 之间,则存储“110”,后跟值(10 位)
否则如果 ΔΔ在 [-4096,4095] 之间,则存储“1110”,后跟值(13 位)
否则如果 ΔΔ在 [-32768,32767] 之间,以紧随其后的‘11110’ 存储值 (16 位)
否则以紧随其后的‘11111’存储使用 64 位的 D

值的异或压缩

值压缩的基础是假设连续值之间的差值通常很小,并且是逐渐而非突然发生的。此外,浮点数天生浪费,包含许多可被消除的重复 0。因此,当对两个连续值进行异或时,结果中只会存在几个有意义的位。为简单起见,下面的示例使用单精度 double - RedisTimeSeries 使用双精度 double

可以看出,前两个相等数字之间的异或操作显然为 0。但有意义的异或值周围还经常有一些前导和尾随 0。值异或差的变长编码会移除前导和尾随 0。 

还有一个控制位,可以进一步减少样本消耗的位数。如果有意义的位块位于前一个值有意义的位块内,即以至少和前一个值一样多的前导 0 和尾随 0 结尾,那么可以使用前一个样本的前导和尾随 0 的数量:

如果 XOR为 0(数值相等)
存储单个‘0’位 
否则 
计算XOR中的前导和尾随 0 的数量, 
存储位‘1’紧随其后存储
如果有意义位块位于前一个有意义位块内, 
存储控制位`0`
否则 存储控制位`1`, 
存储在接下来的 5 位中前导 0 的数量, 
在接下来的 6 位存储XOR值的有效位数量。 
最后存储XOR值的有意义的位。

内存消耗结果

那么这项技术可以给你带来多少内存减少?毫不奇怪,实际减少取决于你的用例,但在我们的基准数据集里,我们注意到了 94% 的减少。根据 Facebook 的第六页大猩猩论文,每个样本平均占用 1.37 字节,而之前是 16 字节。这导致90% 的内存减少,适用于最常见的用例。

当双差为 0 且异或也为 0 时达到理论上的极限。在这种情况下,每个样本使用 2 位而不是 128 位,导致内存减少 98.4%!最糟糕的情况是,一个样本占用 145 位:69 位用于时间戳,76 位用于值。(在这种极端情况下,双差必须大于 MAX_32,并且浮点值将极其复杂且不同。因为我们不想排除极端用例,所以当您创建时间序列时,您仍然可以使用“未压缩”选项。)

API 增强

时间序列用例主要是仅追加的,这允许优化,例如上述压缩。但通过这种压缩方法,插入一个比最后一个样本更早的时间戳的样本,不仅会引入更复杂的逻辑并占用更多 CPU 周期,而且还需要更复杂的内存管理。

这就是为什么在 RedisTimeSeries 1.2 中,我们使 API 更加严格,不再允许客户端重新编写时间序列的最后一个样本。这可能听起来有局限性,但大多数更改都是直观的,实际上增强了开发人员体验。

因此,对降采样时间序列的聚合样本的自动计算现在发生在源时间序列中。只有当聚合时间窗口已过,表示聚合的样本才会被写入降采样时间序列。这意味着写入降采样时间序列的频率降低,进一步提高了摄取性能。

利用降采样,您可以通过聚合位于过去更长时间的样本,从而减少内存消耗。但是,您还可以为固定期间建模计数器,例如每小时的网站页面浏览次数。您可以使用 RedisTimeSeries 轻松构建这些计数器。在源时间序列中,您可以使用 TS.ADD 在具有您希望计数器递增的值的样本中添加一个样本,以及一个降采样规则,并通过求和在固定窗口大小上进行聚合:

redis:6379> TS.CREATE ts RETENTION 20000
OK
redis:6379> TS.CREATE counter
OK
redis:6379> TS.CREATERULE ts counter AGGREGATION sum 5000
OK
redis:6379> TS.ADD ts * 5
(integer) 1580394077750
redis:6379> TS.ADD ts * 2
(integer) 1580394079257
redis:6379> TS.ADD ts * 3
(integer) 1580394085716
redis:6379> TS.RANGE counter - +
1) 1) (integer) 1580394075000
   2) "7"
redis:6379> TS.ADD ts * 1
(integer) 1580394095233
redis:6379> TS.RANGE counter - +
1) 1) (integer) 1580394075000
   2) "7"
2) 1) (integer) 1580394085000
   2) "3"
redis:6379> TS.RANGE ts - +
1) 1) (integer) 1580394077750
   2) "5"
2) 1) (integer) 1580394079257
   2) "2"
3) 1) (integer) 1580394085716
   2) "3"
4) 1) (integer) 1580394095233
   2) "1"
redis:6379>

在两种情况下,降采样序列都将具有您正在寻找的确切计数器。

RedisTimeSeries 下一步是什么?

添加 Gorilla 压缩和 API 增强已大大减少了内存消耗。(有关此工作原理的更多信息,请参阅有关 RedisTimeSeries 1.2 基准测试的配套博客文章。)

但是我们还没有完成。展望未来,我们计划使用 RediSearch 中使用的全新低级 API,以替换使用有序集合的 RedisTimeSeries 专有二级索引。这将使用丰富且经过验证的查询语言,使用与时间序列关联的标签使用户能够对全文本搜索查询进行查询。我们还致力于通过引入 TS.REVRANGETS.MREVRANGE 命令来完善我们的 API,使其与其他 Redis 数据结构一致。

大多数 Redis 模块致力于极为出色且快速地解决一个特定问题——将它们结合在一起能够实现众多有趣的新用例。我们当前正在探索将 RedisTimeSeries 和RedisAI结合起来,以执行实时异常检测和无监督学习。最后,我们正在努力公开一个用于RedisGears的低级 API,此 API 将允许您以最有效的方式执行跨时间序列聚合。