VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

我建议您阅读 Alexander Valyalkin 2019 年末报告的文字记录“VictoriaMetrics 中的 Go 优化”

维多利亚计量公司 — 一个快速且可扩展的 DBMS,用于以时间序列的形式存储和处理数据(记录形成时间和与该时间对应的一组值,例如通过定期轮询传感器的状态或收集指标)。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

这是该报告的视频链接 - https://youtu.be/MZ5P21j_HLE

幻灯片

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

向我们介绍你自己。 我是亚历山大·瓦利亚金。 这里 我的 GitHub 帐户。 我对 Go 和性能优化充满热情。 我写了很多有用的和不太有用的库。 他们从以下任一开始 fast,或与 quick 字首。

我目前正在研究 VictoriaMetrics。 它是什么以及我在那里做什么? 我将在本次演讲中讨论这一点。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

报告概要如下:

  • 首先,我会告诉你什么是VictoriaMetrics。
  • 那我就告诉你什么是时间序列。
  • 然后我会告诉你时间序列数据库是如何工作的。
  • 接下来,我将向您介绍数据库架构:它由什么组成。
  • 然后让我们继续讨论 VictoriaMetrics 的优化。 这是对倒排索引的优化,也是对Go中bitset实现的优化。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

观众中有人知道 VictoriaMetrics 是什么吗? 哇,很多人已经知道了。 这是一个好消息。 对于那些不知道的人来说,这是一个时间序列数据库。 它基于ClickHouse架构,基于ClickHouse实现的一些细节。 例如,MergeTree、所有可用处理器核心上的并行计算以及通过处理放置在处理器高速缓存中的数据块来优化性能。

VictoriaMetrics 提供比其他时间序列数据库更好的数据压缩。

它可以垂直扩展 - 也就是说,您可以在一台计算机上添加更多处理器、更多 RAM。 VictoriaMetrics 将成功利用这些可用资源并提高线性生产力。

VictoriaMetrics还可以水平扩展——也就是说,你可以向VictoriaMetrics集群添加额外的节点,它的性能将几乎呈线性增长。

正如您所猜测的,VictoriaMetrics 是一个快速数据库,因为我无法编写其他数据库。 它是用 Go 编写的,所以我在这次聚会上谈论它。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

谁知道什么是时间序列? 他也认识很多人。 时间序列是一系列对 (timestamp, значение),其中这些对按时间排序。 该值是一个浮点数 – float64。

每个时间序列都由一个键唯一标识。 这个密钥由什么组成? 它由一组非空的键值对组成。

这是时间序列的示例。 这个系列的关键是一个对的列表: __name__="cpu_usage" 是指标的名称, instance="my-server" - 这是收集该指标的计算机, datacenter="us-east" - 这是该计算机所在的数据中心。

我们最终得到了一个由三个键值对组成的时间序列名称。 该键对应于一个对的列表 (timestamp, value). t1, t3, t3, ..., tN - 这些是时间戳, 10, 20, 12, ..., 15 — 相应的值。 这是给定时间给定行的 cpu 使用率。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

时间序列可以用在什么地方? 有人有什么主意吗?

  • 在 DevOps 中,您可以测量 CPU、RAM、网络、rps、错误数等。
  • 物联网——我们可以测量温度、压力、地理坐标和其他东西。
  • 还有金融——我们可以监控各种股票和货币的价格。
  • 此外,时间序列还可用于监控工厂的生产过程。 我们有用户使用 VictoriaMetrics 来监控机器人的风力涡轮机。
  • 时间序列对于从各种设备的传感器收集信息也很有用。 例如,对于发动机; 用于测量轮胎压力; 用于测量速度、距离; 用于测量汽油消耗量等
  • 时间序列还可用于监控飞机。 每架飞机都有一个黑匣子,用于收集飞机健康状况的各种参数的时间序列。 时间序列也用于航空航天工业。
  • 医疗保健是血压、脉搏等。

可能还有更多的应用程序我忘记了,但我希望您理解时间序列在现代世界中得到了积极的使用。 而且它们的使用量每年都在增长。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

为什么需要时间序列数据库? 为什么不能使用常规的关系数据库来存储时间序列?

因为时间序列通常包含大量信息,在传统数据库中很难存储和处理。 因此,出现了专门的时间序列数据库。 这些基地有效储存积分 (timestamp, value) 使用给定的密钥。 它们提供了一个 API,用于通过键、单个键值对、多个键值对或正则表达式读取存储的数据。 例如,你想找到美国某个数据中心所有服务的CPU负载,那么你需要使用这个伪查询。

通常时间序列数据库提供专门的查询语言,因为时间序列 SQL 不太适合。 虽然有支持SQL的数据库,但是不太适合。 查询语言如 普罗姆QL, InfluxQL, , Q。 我希望有人至少听过其中一种语言。 很多人可能都听说过 PromQL。 这是普罗米修斯查询语言。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

以 VictoriaMetrics 为例,这就是现代时间序列数据库架构的样子。

它由两部分组成。 这是倒排索引的存储和时间序列值的存储。 这些存储库是分开的。

当新记录到达数据库时,我们首先访问倒排索引以查找给定集合的时间序列标识符 label=value 对于给定的指标。 我们找到这个标识符并将该值保存在数据存储中。

当有请求从 TSDB 检索数据时,我们首先会去倒排索引。 让我们得到一切 timeseries_ids 与该集合匹配的记录 label=value。 然后我们从数据仓库中获取所有必要的数据,索引为 timeseries_ids.

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

让我们看一个时间序列数据库如何处理传入的选择查询的示例。

  • 首先她得到了一切 timeseries_ids 来自包含给定对的倒排索引 label=value,或满足给定的正则表达式。
  • 然后,它以给定的时间间隔从数据存储中检索找到的数据点 timeseries_ids.
  • 之后,数据库根据用户的请求对这些数据点执行一些计算。 之后它返回答案。

在本次演讲中,我将向您介绍第一部分。 这是一个搜索 timeseries_ids 通过倒排索引。 你可以稍后看第二部分和第三部分 VictoriaMetrics 来源,或者等我准备其他报告:)

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

让我们继续讨论倒排索引。 许多人可能认为这很简单。 谁知道什么是倒排索引以及它是如何工作的? 唉,人已经不多了。 让我们尝试了解它是什么。

其实很简单。 它只是一个将键映射到值的字典。 什么是钥匙? 这对夫妇 label=value哪里 label и value - 这些是线条。 并且值是一个集合 timeseries_ids,其中包括给定的对 label=value.

倒排索引可以让你快速找到所有内容 timeseries_ids,其中给出了 label=value.

还可以让你快速找到 timeseries_ids 几对的时间序列 label=value,或情侣 label=regexp。 这是怎么发生的? 通过找到集合的交集 timeseries_ids 对于每对 label=value.

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

让我们看看倒排索引的各种实现。 让我们从最简单的简单实现开始。 她看起来像这样。

功能 getMetricIDs 获取字符串列表。 每行包含 label=value。 该函数返回一个列表 metricIDs.

怎么运行的? 这里我们有一个全局变量,叫做 invertedIndex。 这是一本普通词典(map),它将把字符串映射到整数切片。 该行包含 label=value.

函数实现:获取 metricIDs 为了第一 label=value,然后我们完成其他所有事情 label=value, 我们懂了 metricIDs 对于他们来说。 并调用该函数 intersectInts,这将在下面讨论。 该函数返回这些列表的交集。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

正如您所看到的,实现倒排索引并不是很复杂。 但这是一个幼稚的实现。 它有什么缺点? 这种简单实现的主要缺点是这样的倒排索引存储在 RAM 中。 重新启动应用程序后,我们会丢失该索引。 没有将该索引保存到磁盘。 这样的倒排索引不太可能适合数据库。

第二个缺点也与内存有关。 倒排索引必须适合 RAM。 如果它超过了 RAM 的大小,那么显然我们会得到 - 内存不足错误。 并且该程序将无法运行。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

这个问题可以使用现成的解决方案来解决,例如 级别数据库岩石数据库.

简而言之,我们需要一个能够让我们快速完成三个操作的数据库。

  • 第一个操作是录音 ключ-значение 到这个数据库。 她做得很快,其中 ключ-значение 是任意字符串。
  • 第二个操作是使用给定键快速搜索值。
  • 第三个操作是通过给定前缀快速搜索所有值。

LevelDB 和 RocksDB - 这些数据库是由 Google 和 Facebook 开发的。 首先是 LevelDB。 然后 Facebook 的人采用了 LevelDB 并开始改进它,他们创造了 RocksDB。 现在Facebook内部几乎所有的内部数据库都运行在RocksDB上,包括已经转移到RocksDB和MySQL上的数据库。 他们给他起了个名字 我的摇滚.

倒排索引可以使用LevelDB来实现。 怎么做? 我们另存为密钥 label=value。 该值是该对存在的时间序列的标识符 label=value.

如果我们有许多具有给定对的时间序列 label=value,那么这个数据库中将会有很多行具有相同的键和不同的 timeseries_ids。 获取全部列表 timeseries_ids,以此开头 label=prefix,我们进行范围扫描,并针对该数据库进行了优化。 也就是说,我们选择所有以 label=prefix 并获得必要的 timeseries_ids.

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

下面是 Go 中的示例实现。 我们有一个倒排索引。 这就是LevelDB。

该功能与简单实现的功能相同。 它几乎逐行重复简单的实现。 唯一的一点是,而不是转向 map 我们访问倒排索引。 我们得到第一个的所有值 label=value。 然后我们遍历所有剩余的对 label=value 并获取它们对应的metricID集合。 然后我们找到交点。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

一切似乎都很好,但这个解决方案也有缺点。 VictoriaMetrics最初实现了基于LevelDB的倒排索引。 但最终我不得不放弃。

为什么? 因为 LevelDB 比简单的实现慢。 在一个简单的实现中,给定给定的键,我们立即检索整个切片 metricIDs。 这是一个非常快的操作 - 整个切片都可供使用。

在LevelDB中,每次调用函数时 GetValues 你需要遍历所有以 label=value。 并获取每行的值 timeseries_ids。 这样的 timeseries_ids 收集其中的一片 timeseries_ids。 显然,这比简单地通过键访问常规地图要慢得多。

第二个缺点是 LevelDB 是用 C 编写的。从 Go 调用 C 函数不是很快。 这需要数百纳秒。 这并不是很快,因为与用 go 编写的常规函数​​调用需要 1-5 纳秒相比,性能相差数十倍。 对于 VictoriaMetrics 来说,这是一个致命的缺陷:)

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

所以我编写了自己的倒排索引实现。 他打电话给她 合并集.

Mergeset 基于 MergeTree 数据结构。 这个数据结构借鉴自ClickHouse。 显然,mergeset应该针对快速搜索进行优化 timeseries_ids 根据给定的密钥。 合并集完全用 Go 编写。 你可以看到 GitHub 上的 VictoriaMetrics 源代码。 mergeset的实现在文件夹中 /lib/合并集。 您可以尝试弄清楚那里发生了什么。

合并集 API 与 LevelDB 和 RocksDB 非常相似。 也就是说,它允许您快速保存新记录并通过给定前缀快速选择记录。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

稍后我们会讨论mergeset的缺点。 现在我们来谈谈VictoriaMetrics在生产中实现倒排索引时出现了哪些问题。

他们为何出现?

第一个原因是高流失率。 翻译成俄语,这是时间序列的频繁变化。 这是一个时间序列结束、一个新序列开始或许多新时间序列开始的时间。 这种情况经常发生。

第二个原因是时间序列数量庞大。 最初,当监控越来越流行时,时间序列的数量很少。 例如,对于每台计算机,您需要监控 CPU、内存、网络和磁盘负载。 每台计算机 4 个时间序列。 假设您有 100 台计算机和 400 个时间序列。 这是很少的。

随着时间的推移,人们发现他们可以测量更精细的信息。 例如,不是测量整个处理器的负载,而是单独测量每个处理器内核的负载。 如果您有 40 个处理器核心,那么您就有 40 倍的时间序列来测量处理器负载。

但这还不是全部。 每个处理器核心可以有多种状态,例如空闲时的空闲状态。 并且还工作在用户空间、工作在内核空间等状态。 每个这样的状态也可以作为单独的时间序列来测量。 这还会使行数增加 7-8 倍。

从一个指标中,我们仅获得一台计算机的 40 x 8 = 320 个指标。 乘以 100,我们得到 32,而不是 000。

然后 Kubernetes 出现了。 而且情况变得更糟,因为 Kubernetes 可以托管许多不同的服务。 Kubernetes 中的每个服务都由许多 Pod 组成。 而这一切都需要监控。 此外,我们会不断部署您的服务的新版本。 对于每个新版本,必须创建新的时间序列。 结果,时间序列的数量呈指数级增长,我们面临着大量时间序列的问题,这就是所谓的高基数。 与其他时间序列数据库相比,VictoriaMetrics 成功地应对了这一问题。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

让我们仔细看看高流失率。 是什么导致生产中的高流失率? 因为标签和标记的某些含义是不断变化的。

以 Kubernetes 为例,它有这样的概念 deployment,即当您的应用程序推出新版本时。 由于某种原因,Kubernetes 开发人员决定将部署 ID 添加到标签中。

这导致了什么? 此外,随着每次新的部署,所有旧的时间序列都会被中断,取而代之的是新的时间序列以新的标签值开始 deployment_id。 这样的行可能有数十万甚至数百万。

所有这一切的重要之处在于,时间序列的总数在增长,但当前活动和接收数据的时间序列的数量保持不变。 这种状态称为高流失率。

高流失率的主要问题是确保在一定时间间隔内给定标签集的所有时间序列的搜索速度恒定。 通常这是最后一小时或最后一天的时间间隔。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

如何解决这个问题呢? 这是第一个选项。 这是随着时间的推移将倒排索引分成独立的部分。 也就是说,经过一些时间间隔,我们完成对当前倒排索引的处理。 并创建一个新的倒排索引。 又一个时间间隔过去了,我们创造了一个又一个。

当从这些倒排索引中采样时,我们找到一组落在给定区间内的倒排索引。 因此,我们从那里选择时间序列的 id。

这节省了资源,因为我们不必查看不在给定间隔内的部分。 也就是说,通常,如果我们选择最后一小时的数据,那么对于之前的时间间隔,我们会跳过查询。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

还有另一种选择可以解决这个问题。 这是为了每天存储当天发生的时间序列 ID 的单独列表。

与之前的解决方案相比,该解决方案的优点是我们不会重复不会随时间消失的时间序列信息。 它们始终存在并且不会改变。

缺点是这样的方案实现起来比较困难,调试起来也比较困难。 VictoriaMetrics 选择了这个解决方案。 历史上就是这样发生的。 与前一个解决方案相比,该解决方案也表现良好。 因为这个解决方案没有被实现,因为有必要在每个分区中复制不改变的时间序列的数据,即不随时间消失的时间序列。 VictoriaMetrics主要针对磁盘空间消耗进行了优化,之前的实现使磁盘空间消耗变得更糟。 但这种实现更适合最大限度地减少磁盘空间消耗,因此选择了它。

我不得不和她战斗。 困难在于,在这个实现中你仍然需要选择一个更大的数字 timeseries_ids 对于数据而言,倒排索引是时间分区的。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

我们是如何解决这个问题的? 我们用一种原始的方式解决了这个问题——在每个倒排索引条目中存储多个时间序列标识符而不是一个标识符。 也就是说,我们有一把钥匙 label=value,它出现在每个时间序列中。 现在我们保存了几个 timeseries_ids 在一个条目中。

这是一个例子。 以前我们有 N 个条目,但现在我们有一个条目,其前缀与所有其他条目相同。 对于上一个条目,该值包含所有时间序列 ID。

这使得倒排索引的扫描速度提高了 10 倍。 它允许我们减少缓存的内存消耗,因为现在我们存储字符串 label=value 只在缓存中一起出现过N次。 如果您在标签和标签中存储长行,那么这条线可能会很大,Kubernetes 喜欢将其推到那里。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

加快倒排索引搜索速度的另一个选择是分片。 创建多个倒排索引而不是一个,并按键在它们之间分片数据。 这是一套 key=value 蒸汽。 也就是说,我们得到了几个独立的倒排索引,我们可以在多个处理器上并行查询它们。 以前的实现仅允许在单处理器模式下运行,即仅在一个内核上扫描数据。 该解决方案允许您同时扫描多个核心上的数据,就像 ClickHouse 喜欢做的那样。 这就是我们计划实施的。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

现在让我们回到我们的羊——交集函数 timeseries_ids。 让我们考虑一下可能有哪些实现。 这个功能可以让你找到 timeseries_ids 对于给定的集合 label=value.

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

第一个选项是一个幼稚的实现。 两个嵌套循环。 这里我们得到函数输入 intersectInts 两片—— a и b。 在输出处,它应该向我们返回这些切片的交集。

一个幼稚的实现看起来像这样。 我们迭代切片中的所有值 a,在这个循环中我们遍历 slice 的所有值 b。 我们将它们进行比较。 如果它们匹配,那么我们就找到了交集。 并将其保存在 result.

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

有什么缺点? 二次复杂度是其主要缺点。 例如,如果您的尺寸是切片 a и b 一次一百万,那么这个函数永远不会给你返回答案。 因为它需要进行一万亿次迭代,即使对于现代计算机来说这也是很多次了。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

第二种实现是基于map的。 我们创建地图。 我们将切片中的所有值放入此映射中 a。 然后我们在一个单独的循环中遍历切片 b。 我们检查这个值是否来自切片 b 在地图中。 如果存在,则将其添加到结果中。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

有什么好处? 优点是只有线性复杂度。 也就是说,对于较大的切片,该函数执行速度会更快。 对于百万大小的切片,此函数将执行 2 万次迭代,而不是前一个函数的万亿次迭代。

缺点是该函数需要更多内存来创建该地图。

第二个缺点是散列的巨大开销。 这个缺点不是很明显。 对于我们来说,这也不是很明显,所以最初在 VictoriaMetrics 中,交叉点的实现是通过地图来实现的。 但随后分析表明,主处理器时间花费在写入映射并检查该映射中是否存在值。

为什么CPU时间会浪费在这些地方呢? 因为 Go 对这些行执行了哈希操作。 也就是说,它计算键的哈希值,然后在 HashMap 中的给定索引处访问它。 哈希计算操作在几十纳秒内完成。 这对于 VictoriaMetrics 来说很慢。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

我决定实现一个专门针对这种情况优化的位集。 这就是两个切片的交集现在的样子。 这里我们创建一个位集。 我们将第一个切片中的元素添加到其中。 然后我们检查第二个切片中是否存在这些元素。 并将它们添加到结果中。 也就是说,它与前面的例子几乎没有什么不同。 这里唯一的事情是我们用自定义函数替换了对地图的访问 add и has.

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

乍一看,如果之前使用标准地图,然后调用一些其他函数,那么这似乎应该运行得更慢,但分析表明,在 VictoriaMetrics 的情况下,这个东西的运行速度比标准地图快 10 倍。

此外,与映射实现相比,它使用的内存要少得多。 因为我们在这里存储位而不是八字节值。

这种实现的缺点是它不是那么明显,不是微不足道的。

许多人可能没有注意到的另一个缺点是,这种实现在某些情况下可能无法正常工作。 也就是说,它针对特定情况(即 VictoriaMetrics 时间序列 id 的交集情况)进行了优化。 这并不意味着它适合所有情况。 如果使用不当,我们不会得到性能提升,而是出现内存不足错误和性能下降。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

让我们考虑一下这个结构的实现。 如果您想查看,它位于 VictoriaMetrics 源的文件夹中 库/uint64set。 它专门针对 VictoriaMetrics 案例进行了优化,其中 timeseries_id 是一个 64 位值,其中前 32 位基本不变,只有最后 32 位发生变化。

该数据结构不存储在磁盘上,它只在内存中运行。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

这是它的 API。 这不是很复杂。 该 API 是专门针对使用 VictoriaMetrics 的特定示例而定制的。 也就是说,这里没有多余的功能。 以下是 VictoriaMetrics 明确使用的函数。

有功能 add,这增加了新的值。 有一个功能 has,它检查新值。 并且有一个功能 del,这会删除值。 有一个辅助函数 len,它返回集合的大小。 功能 clone 克隆很多。 及功能 appendto 将此集合转换为切片 timeseries_ids.

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

这就是这个数据结构的实现的样子。 集合有两个元素:

  • ItemsCount 是一个辅助字段,用于快速返回集合中的元素数量。 没有这个辅助字段也是可以的,但必须在此处添加它,因为 VictoriaMetrics 经常在其算法中查询位集长度。

  • 第二个字段是 buckets。 这是结构的切片 bucket32。 每个结构存储 hi 场地。 这些是高 32 位。 还有两片—— b16his и buckets из bucket16 结构。

16 位结构第二部分的前 64 位存储在这里。 这里存储每个字节的低 16 位的位集。

Bucket64 由一个数组组成 uint64。 使用这些常数计算长度。 合而为一 bucket16 最大可存储 2^16=65536 少量。 如果将其除以 8,则为 8 KB。 如果再除以 8,就是 1000 uint64 意义。 那是 Bucket16 – 这是我们的 8 KB 结构。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

让我们看看这个结构中添加新值的方法之一是如何实现的。

一切都始于 uint64 含义。 我们计算高 32 位,我们计算低 32 位。 让我们回顾一下一切 buckets。 我们将每个桶中的前 32 位与添加的值进行比较。 如果它们匹配,那么我们调用该函数 add 在结构b32中 buckets。 并在那里添加低 32 位。 如果它回来了 true,那么这意味着我们在那里添加了这样的值而我们没有这样的值。 如果返回的话 false,那么这样的意义就已经存在了。 然后我们增加结构中的元素数量。

如果我们没有找到您需要的人 bucket 具有所需的高值,然后我们调用该函数 addAlloc,这将产生一个新的 bucket,将其添加到桶结构中。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

这是函数的实现 b32.add。 它与之前的实现类似。 我们计算最高有效 16 位,最低有效 16 位。

然后我们遍历所有高 16 位。 我们找到匹配项。 如果存在匹配,我们将调用 add 方法,我们将在下一页中考虑该方法 bucket16.

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

而且这里是最低级别,应该尽可能优化。 我们计算为 uint64 切片位中的 id 值以及 bitmask。 这是给定 64 位值的掩码,可用于检查该位是否存在或设置它。 我们检查该位是否已设置并设置它,然后返回存在。 这是我们的实现,与传统地图相比,它使我们能够将时间序列相交 id 的操作速度加快 10 倍。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

除了这个优化之外,VictoriaMetrics还有很多其他的优化。 大多数这些优化都是出于某种原因而添加的,但是是在对生产中的代码进行分析之后添加的。

这是优化的主要规则 - 不要假设这里存在瓶颈而添加优化,因为结果可能不会存在瓶颈。 优化通常会降低代码的质量。 因此,只有在分析之后并且最好在生产中才值得优化,以便这是真实的数据。 如果有人感兴趣,您可以查看 VictoriaMetrics 源代码并探索其中的其他优化。

VictoriaMetrics 中的 Go 优化。 亚历山大·瓦亚尔金

我有一个关于位集的问题。 与 C++ 矢量 bool 实现非常相似,优化了位集。 您是从那里实施的吗?

不,不是从那里开始的。 在实现这个位集时,我以这些 ids 时间序列的结构知识为指导,这些时间序列在 VictoriaMetrics 中使用。 并且它们的结构是这样的:高32位基本不变。 低 32 位可能会发生变化。 位越低,改变的频率就越高。 因此,该实现专门针对该数据结构进行了优化。 据我所知,C++ 实现针对一般情况进行了优化。 如果针对一般情况进行优化,这意味着它对于特定情况来说并不是最佳的。

我还建议您观看阿列克谢·米洛维德的报道。 大约一个月前,他谈到了 ClickHouse 中针对特定专业的优化。 他只是说,在一般情况下,C++ 实现或其他一些实现经过专门设计,可以在医院中正常运行。 它的性能可能比像我们这样的特定于知识的实现更差,我们知道前 32 位大部分是不变的。

我还有第二个问题。 与 InfluxDB 的根本区别是什么?

有许多根本性的差异。 在性能和内存消耗方面,InfluxDB 在测试中显示,当您拥有大量(例如数百万)高基数时间序列时,内存消耗会增加 10 倍。 例如,VictoriaMetrics 每百万活动行消耗 1 GB,而 InfluxDB 消耗 10 GB。 这是一个很大的区别。

第二个根本区别是 InfluxDB 有奇怪的查询语言——Flux 和 InfluxQL。 与相比,它们处理时间序列不太方便 普罗姆QL,由 VictoriaMetrics 支持。 PromQL 是 Prometheus 的一种查询语言。

还有一个区别是 InfluxDB 有一个稍微奇怪的数据模型,其中每一行可以存储具有不同标签集的多个字段。 这些行进一步分为各种表。 这些额外的复杂性使该数据库的后续工作变得复杂。 很难支持和理解。

在 VictoriaMetrics 中,一切都变得更加简单。 在那里,每个时间序列都是一个键值。 该值是一组点 - (timestamp, value),关键是集合 label=value。 字段和测量之间没有分离。 它允许你选择任何数据,然后组合、加、减、乘、除,这与 InfluxDB 不同,据我所知,InfluxDB 仍然没有实现不同行之间的计算。 即使实现了,也很困难,你必须写很多代码。

我有一个澄清的问题。 我是否正确理解您提到的某种问题,即该倒排索引不适合内存,因此存在分区?

首先,我展示了标准 Go 映射上倒排索引的简单实现。 此实现不适合数据库,因为此倒排索引不会保存到磁盘,并且数据库必须保存到磁盘,以便该数据在重新启动时仍然可用。 在此实现中,当您重新启动应用程序时,您的倒排索引将消失。 您将无法访问所有数据,因为您将无法找到它。

你好! 感谢您的报告! 我叫帕维尔。 我来自野莓。 我有几个问题要问你。 问题一。 您是否认为,如果您在构建应用程序架构时选择了不同的原则并随着时间的推移对数据进行分区,那么您也许能够在搜索时交叉数据,仅基于一个分区包含一个分区的数据这一事实一段时间,即在一个时间间隔内,你就不用担心你的棋子分散的情况不同了? 问题 2 - 既然您正在使用位集和其他所有内容实现类似的算法,那么也许您尝试过使用处理器指令? 也许你尝试过这样的优化?

我马上回答第二个问题。 我们还没到那一步。 但如果有必要,我们会到达那里。 第一个问题是什么?

您讨论了两种情况。 他们说他们选择了第二个,其实现更复杂。 他们不喜欢第一个,即数据按时间分区。

是的。 在第一种情况下,索引的总容量会更大,因为在每个分区中,我们必须存储持续通过所有这些分区的时间序列的重复数据。 如果您的时间序列流失率很小,即不断使用相同的序列,那么在第一种情况下,与第二种情况相比,我们会损失更多的磁盘空间占用量。

所以 - 是的,时间分区是一个不错的选择。 普罗米修斯使用它。 但普罗米修斯还有另一个缺点。 合并这些数据时,需要在内存中保留所有标签和时间序列的元信息。 因此,如果它合并的数据块很大,那么合并过程中内存消耗会增加很多,这与VictoriaMetrics不同。 合并时,VictoriaMetrics 根本不消耗内存;无论合并的数据块有多大,仅消耗几千字节。

您使用的算法使用内存。 它标记包含值的时间序列标签。 通过这种方式,您可以检查一个数据数组和另一个数据数组中是否存在配对。 并且您了解是否发生了相交。 通常,数据库实现游标和迭代器来存储其当前内容并运行排序的数据,因为这些操作非常复杂。

为什么我们不使用游标来遍历数据呢?

是。

我们将排序的行存储在 LevelDB 或合并集中。 我们可以移动光标并找到交点。 我们为什么不使用它呢? 因为它很慢。 因为游标意味着你需要为每一行调用一个函数。 一次函数调用需要 5 纳秒。 如果你有 100 行,那么事实证明我们只花了半秒的时间来调用该函数。

有这样的事,是的。 我的最后一个问题。 这个问题听起来可能有点奇怪。 为什么无法在数据到达时读取所有必要的聚合并将它们保存为所需的形式? 为什么要在一些系统(如 VictoriaMetrics、ClickHouse 等)中保存大量数据,然后在它们上花费大量时间?

我将举一个例子以使其更清楚。 我们来说说小型玩具车速表是如何工作的? 它记录您行驶的距离,始终将其添加到一个值,然后添加第二个值。 并分裂。 并获得平均速度。 你也可以做同样的事情。 即时添加所有必要的事实。

好吧,我明白这个问题了。 你的例子有它的一席之地。 如果您知道需要什么聚合,那么这是最好的实现。 但问题是,人们在 ClickHouse 中保存了这些指标、一些数据,但他们还不知道将来如何聚合和过滤它们,所以他们必须保存所有原始数据。 但是如果你知道你需要计算平均值,那么为什么不计算它而不是在那里存储一堆原始值呢? 但这只有在您确切知道自己需要什么的情况下才能实现。

顺便说一句,用于存储时间序列的数据库支持聚合计数。 例如,普罗米修斯支持 记录规则。 也就是说,如果您知道需要什么单位,就可以做到这一点。 VictoriaMetrics 还没有这个功能,但它通常先于 Prometheus,您可以在其中的重新编码规则中执行此操作。

例如,在我之前的工作中,我需要计算过去一小时内滑动窗口中的事件数。 问题是我必须在 Go 中进行自定义实现,即用于计数这个东西的服务。 这项服务最终并非微不足道,因为它很难计算。 如果您需要以固定时间间隔对某些聚合进行计数,那么实现可能会很简单。 如果你想统计滑动窗口中的事件,那么它并不像看起来那么简单。 我认为这在ClickHouse或时间序列数据库中还没有实现,因为它很难实现。

还有一个问题。 我们只是在谈论平均,我记得曾经有过带有碳后端的石墨这样的东西。 而且他知道如何对旧数据进行稀疏化,即每分钟留下一个点,每小时留下一个点等等。原则上,如果我们需要原始数据,相对来说,一个月,其他一切都可以,这是相当方便的。被稀疏化。 但 Prometheus 和 VictoriaMetrics 不支持此功能。 有计划支持吗? 如果没有,为什么不呢?

谢谢你的提问。 我们的用户定期询问这个问题。 他们询问我们何时添加对下采样的支持。 这里有几个问题。 首先,每个用户都明白 downsampling 有些不同:有人想要得到给定区间内的任意点,有人想要最大值、最小值、平均值。 如果许多系统将数据写入您的数据库,那么您就无法将所有数据集中在一起。 每个系统可能需要不同的细化。 而这很难实施。

第二件事是,VictoriaMetrics 与 ClickHouse 一样,针对处理大量原始数据进行了优化,因此如果系统中有许多核心,它可以在不到一秒的时间内处理 50 亿行数据。 在 VictoriaMetrics 中扫描时间序列点 – 每核每秒 000 个点。 而且这种性能可以扩展到现有的内核。 也就是说,例如,如果您有 000 个核心,则每秒将扫描 20 亿个点。 而 VictoriaMetrics 和 ClickHouse 的这一特性减少了对下采样的需求。

另一个特点是 VictoriaMetrics 有效地压缩了这些数据。 生产中平均压缩为每点 0,4 到 0,8 字节。 每个点都是一个时间戳+值。 并且平均被压缩到不到XNUMX个字节。

谢尔盖. 我有个问题。 最小记录时间量是多少?

一毫秒。 我们最近与其他时间序列数据库开发人员进行了对话。 他们的最小时间片是一秒。 例如,在 Graphite 中,它也是一秒。 在 OpenTSDB 中也是一秒。 InfluxDB 具有纳秒级精度。 在 VictoriaMetrics 中它是一毫秒,因为在 Prometheus 中它是一毫秒。 而VictoriaMetrics最初是作为Prometheus的远程存储而开发的。 但现在它可以保存其他系统的数据。

与我交谈的人说他们具有秒到秒的精度 - 这对他们来说已经足够了,因为这取决于时间序列数据库中存储的数据类型。 如果这是 DevOps 数据或来自基础设施的数据,您以每分钟 30 秒的间隔收集数据,那么秒精度就足够了,您不需要任何其他东西。 如果您从高频交易系统收集这些数据,那么您需要纳秒级的精度。

VictoriaMetrics中的毫秒精度也适用于DevOps案例,并且可以适用于我在报告开头提到的大多数案例。 它唯一可能不适合的是高频交易系统。

谢谢你! 还有一个问题。 PromQL 中的兼容性是什么?

完全向后兼容。 VictoriaMetrics 完全支持 PromQL。 此外,它还在 PromQL 中添加了额外的高级功能,称为 MetricsQL。 YouTube 上有一个关于此扩展功能的讨论。 我在春天在圣彼得堡举行的监测聚会上发表了讲话。

电报频道 维多利亚计量公司.

只有注册用户才能参与调查。 登录拜托

是什么阻止您改用 VictoriaMetrics 作为 Prometheus 的长期存储? (写在评论里,我会把它添加到投票中))

  • 71,4%我不使用普罗米修斯5

  • 28,6%不知道 VictoriaMetrics2

7 位用户投票。 12 名用户弃权。

来源: habr.com

添加评论