何时(不)使用 varbit chunks

2016年5月8日作者 Björn “Beorn” Rabenstein

Prometheus 服务器的嵌入式时序数据库(TSDB)将每个时间序列的原始样本数据组织成大小恒定为 1024 字节的数据块(chunks)。除了原始样本数据外,一个数据块还包含一些元数据,这允许为每个数据块选择不同的编码方式。最根本的区别是编码版本。你可以通过命令行标志 -storage.local.chunk-encoding-version 来为新创建的数据块选择版本。到目前为止,只支持两个版本:版本 0 用于原始的增量(delta)编码,版本 1 用于改进的双增量(double-delta)编码。在 0.18.0 版本中,我们添加了版本 2,它是双增量编码的另一种变体。我们称之为 varbit 编码,因为它在数据块内为每个样本使用了可变的位宽。虽然版本 1 在几乎所有方面都优于版本 0,但在版本 1 和版本 2 之间存在真正的权衡。这篇博客文章将帮助你做出决定。版本 1 仍然是默认编码,所以如果你在阅读本文后想尝试版本 2,你必须通过命令行标志明确选择它。来回切换没有坏处,但请注意,现有的数据块一旦创建,其编码版本就不会改变。不过,这些数据块会根据配置的保留时间逐渐被淘汰,并被使用命令行标志中指定的编码的新数据块所取代。

什么是 varbit 编码?

从一开始,我们就将分块样本存储设计为易于添加新的编码方式。当 Facebook 发表了一篇关于其内存时序数据库 Gorilla 的论文时,我们对 Gorilla 和 Prometheus 独立开发的方案之间的一些相似之处感到 intrigued。然而,也存在许多根本性的差异,我们对此进行了详细研究,想知道是否能从 Gorilla 中获得一些灵感来改进 Prometheus。

在一个难得有空的周末,我决定试一试。在一场编程狂欢中,我实现了后来(经过大量测试和调试后)成为 varbit 编码的东西。

在未来的一篇博客文章中,我将描述该编码的技术细节。目前,你只需要了解一些特性,以便在新的 varbit 编码和传统的双增量编码之间做出选择。(从现在开始,我将后者简称为“双增量编码”,但请注意,varbit 编码也使用了双增量,只是方式不同。)

varbit 编码有什么优势?

简而言之:它提供了更好的压缩率。对于真实世界的数据集,双增量编码每个样本大约需要 3.3 字节,而 varbit 编码在 SoundCloud 的一个典型大型生产服务器上,每个样本可以低至 1.28 字节。这使得空间效率提高了近三倍(甚至比 Gorilla 报告的每个样本 1.37 字节还要好一些——但请对此持保留态度,因为 SoundCloud 的典型数据集可能与 Facebook 的典型数据集不同)。

现在想想这意味着什么:内存中的样本数量增加了三倍,磁盘上的样本数量增加了三倍,磁盘操作仅为原来的三分之一。由于磁盘操作目前是数据采集速度的瓶颈,这也将使采集速度提高三倍。事实上,最近报道的每秒 80 万样本的新采集记录只有通过 varbit chunks 才能实现——当然,还需要 SSD。使用机械硬盘时,瓶颈会更早出现,因此 3 倍的增益就显得更为重要。

这一切听起来好得令人难以置信……

那么代价是什么呢?

一方面,varbit 编码更为复杂。因此,编码和解码值的计算成本有所增加,这从根本上影响了所有写入或读取样本数据的操作。幸运的是,这只是对通常只占操作总成本一小部分的内容成比例地增加。

varbit 编码的另一个特性可能更为重要:varbit chunks 中的样本只能顺序访问,而双增量编码的 chunks 中的样本可以通过索引随机访问。由于 Prometheus 中的写入是仅追加(append-only)的,不同的访问模式只影响样本数据的读取。实际影响在很大程度上取决于原始 PromQL 查询的性质。

一个相对无害的情况是检索一个时间间隔内的所有样本。这发生在评估范围选择器或渲染分辨率与抓取频率相似的仪表盘时。Prometheus 存储引擎需要找到该时间间隔的起点。使用双增量 chunks,它可以进行二分查找,而对于 varbit chunk,则必须顺序扫描。然而,一旦找到起点,该间隔内所有剩余的样本无论如何都需要顺序解码,使用 varbit 编码的成本仅略高一些。

当从一个 chunk 中检索少量不相邻的样本,或者在所谓的即时查询中直接检索单个样本时,权衡就不同了。存储引擎可能需要迭代大量样本才能找到要返回的少数样本。幸运的是,即时查询最常见的来源是规则评估,它引用了每个相关时间序列中的最新样本。并非完全巧合的是,我最近改进了对时间序列最新样本的检索方式。基本上,添加到时间序列的最后一个样本现在会被缓存。一个只需要时间序列最新样本的查询甚至不会再触及 chunk 层,因此 chunk 编码在这种情况下是无关紧要的。

即使即时查询引用的是过去的样本,因而必须触及 chunk 层,查询的其他部分,如索引查找,很可能仍将主导总查询时间。但确实存在一些现实生活中的查询,其中 varbit chunks 所需的顺序访问模式会变得非常重要。

对于 varbit chunks,最坏情况的查询是什么?

varbit chunks 的最坏情况是,你需要从一个非常长的时间序列的*每个* chunk 的中间位置只取一个样本。不幸的是,这有一个真实的用例。假设一个时间序列压缩得足够好,使得每个 chunk 大约能持续八小时。这相当于一天大约三个 chunk,或者一个月大约 100 个 chunk。如果你有一个仪表盘,显示该时间序列过去一个月的数据,分辨率为 100 个数据点,那么仪表盘将执行一个查询,从 100 个不同的 chunk 中各检索一个样本。即使在这种情况下,不同 chunk 编码之间的差异也会被查询执行时间的其他部分所主导。根据情况,我猜测使用双增量编码的查询可能需要 50 毫秒,而使用 varbit 编码则需要 100 毫秒。

然而,如果你的仪表盘查询不仅涉及单个时间序列,而是对数千个时间序列进行聚合,那么需要访问的 chunk 数量会相应成倍增加,顺序扫描的开销将变得主导。 (我们不鼓励此类查询,通常建议对这类频繁使用的查询(例如在仪表盘中)使用记录规则。)但使用双增量编码时,查询时间可能仍然可以接受,比如说大约一秒。切换到 varbit 编码后,相同的查询可能会持续数十秒,这显然不是你希望在仪表盘上看到的。

经验法则是什么?

简单来说:如果你的磁盘容量和磁盘操作性能都没有限制,那就不用担心,继续使用默认的经典双增量编码。

但是,如果你希望有更长的数据保留时间,或者你目前受限于磁盘操作性能,我邀请你尝试一下新的 varbit 编码。用 -storage.local.chunk-encoding-version=2 启动你的 Prometheus 服务器,并等待一段时间,直到你有足够多的使用 varbit 编码的新 chunks 来评估效果。如果你发现有些查询变得慢到无法接受,检查一下是否可以使用记录规则来加速它们。很可能,即使使用旧的双增量编码,这些查询也能从中获益匪浅。

如果你对 varbit 编码的幕后工作原理感兴趣,请关注不久的将来发布的另一篇博客文章。