Prometheus 2 中的 TSDB 分析

Prometheus 2 中的 TSDB 分析

Prometheus 2 中的时间序列数据库(TSDB)是工程解决方案的一个很好的例子,它在数据积累速度、查询执行和资源效率方面比 Prometheus 2 中的 v1 存储有了重大改进。 我们在 Percona 监控和管理 (PMM) 中实施 Prometheus 2,我有机会了解 Prometheus 2 TSDB 的性能。 在这篇文章中,我将讨论这些观察的结果。

Prometheus 平均工作负载

对于那些习惯于处理通用数据库的人来说,典型的 Prometheus 工作负载非常有趣。 数据积累的速度趋于稳定:通常您监控的服务发送大致相同数量的指标,并且基础设施变化相对缓慢。
信息请求可能来自各种来源。 其中一些(例如警报)也力求稳定且可预测的值。 其他情况(例如用户请求)可能会导致突发,但大多数工作负载并非如此。

负载测试

在测试过程中,我重点关注积累数据的能力。 我使用以下脚本在 Linode 服务上部署了用 Go 2.3.2(作为 PMM 1.10.1 的一部分)编译的 Prometheus 1.14: 堆栈脚本。 对于最真实的负载生成,使用此 堆栈脚本 我启动了几个具有真实负载的 MySQL 节点(Sysbench TPC-C 测试),每个节点模拟 10 个 Linux/MySQL 节点。
以下所有测试均在具有 32 个虚拟核心和 20 GB 内存的 Linode 服务器上进行,运行 800 个负载模拟,监控 440 个 MySQL 实例。 或者,用 Prometheus 的话说,380 个目标,每秒 1,7 次抓取,每秒 XNUMX 万条记录,以及 XNUMX 万个活动时间序列。

设计

传统数据库(包括 Prometheus 1.x 使用的方法)的常用方法是 内存限制。 如果不足以处理负载,您将遇到高延迟,并且某些请求将失败。 Prometheus 2 中的内存使用可通过 key 配置 storage.tsdb.min-block-duration,它决定录制内容在刷新到磁盘之前将在内存中保留多长时间(默认为 2 小时)。 所需的内存量取决于添加到网络传入流中的时间序列、标签和抓取的数量。 在磁盘空间方面,Prometheus 的目标是每条记录(样本)使用 3 个字节。 另一方面,内存要求要高得多。

尽管可以配置块大小,但不建议手动配置,因此您被迫为 Prometheus 提供工作负载所需的尽可能多的内存。
如果没有足够的内存来支持传入的指标流,Prometheus 将出现内存不足或者 OOM 杀手将会获取内存。
当 Prometheus 内存不足时添加交换来延迟崩溃并没有真正的帮助,因为使用此功能会导致爆炸性的内存消耗。 我认为这与 Go、它的垃圾收集器以及它处理交换的方式有关。
另一个有趣的方法是将头块配置为在某个时间刷新到磁盘,而不是从进程开始时开始计数。

Prometheus 2 中的 TSDB 分析

从图中可以看出,每两个小时就会刷新一次磁盘。 如果将 min-block-duration 参数更改为一小时,那么这些重置将每小时发生一次,从半小时后开始。
如果您想在 Prometheus 安装中使用此图表和其他图表,您可以使用此 仪表板。 它是为 PMM 设计的,但只需稍作修改,即可适合任何 Prometheus 安装。
我们有一个称为头块的活动块,它存储在内存中; 具有较旧数据的块可通过 mmap()。 这消除了单独配置缓存的需要,但也意味着如果要查询早于头块可容纳的数据,则需要为操作系统缓存留出足够的空间。
这也意味着 Prometheus 虚拟内存消耗看起来会相当高,这并不是什么值得担心的事情。

Prometheus 2 中的 TSDB 分析

另一个有趣的设计点是WAL(预写日志)的使用。 从存储文档中可以看到,Prometheus 使用 WAL 来避免崩溃。 不幸的是,用于保证数据生存性的具体机制没有得到很好的记录。 Prometheus 版本 2.3.2 每 10 秒将 WAL 刷新到磁盘,并且此选项不可由用户配置。

压实

Prometheus TSDB 的设计类似于 LSM(日志结构化合并)存储:头块定期刷新到磁盘,而压缩机制将多个块组合在一起,以避免在查询期间扫描太多块。 在这里您可以看到我在一天负载后在测试系统上观察到的块数。

Prometheus 2 中的 TSDB 分析

如果您想了解有关商店的更多信息,可以检查 meta.json 文件,其中包含有关可用块及其形成方式的信息。

{
       "ulid": "01CPZDPD1D9R019JS87TPV5MPE",
       "minTime": 1536472800000,
       "maxTime": 1536494400000,
       "stats": {
               "numSamples": 8292128378,
               "numSeries": 1673622,
               "numChunks": 69528220
       },
       "compaction": {
               "level": 2,
               "sources": [
                       "01CPYRY9MS465Y5ETM3SXFBV7X",
                       "01CPYZT0WRJ1JB1P0DP80VY5KJ",
                       "01CPZ6NR4Q3PDP3E57HEH760XS"
               ],
               "parents": [
                       {
                               "ulid": "01CPYRY9MS465Y5ETM3SXFBV7X",
                               "minTime": 1536472800000,
                               "maxTime": 1536480000000
                       },
                       {
                               "ulid": "01CPYZT0WRJ1JB1P0DP80VY5KJ",
                               "minTime": 1536480000000,
                               "maxTime": 1536487200000
                       },
                       {
                               "ulid": "01CPZ6NR4Q3PDP3E57HEH760XS",
                               "minTime": 1536487200000,
                               "maxTime": 1536494400000
                       }
               ]
       },
       "version": 1
}

Prometheus 中的压缩与头块刷新到磁盘的时间相关。 此时,可以执行多次这样的操作。

Prometheus 2 中的 TSDB 分析

看来压缩不受任何限制,并且可能会在执行期间导致大量磁盘 I/O 峰值。

Prometheus 2 中的 TSDB 分析

CPU负载峰值

Prometheus 2 中的 TSDB 分析

当然,这对系统的速度造成了相当负面的影响,同时也对LSM存储提出了严峻的挑战:如何进行压缩以支持高请求率而不造成太大的开销?
压缩过程中内存的使用看起来也很有趣。

Prometheus 2 中的 TSDB 分析

我们可以看到,在压缩之后,大部分内存的状态如何从“缓存”更改为“空闲”:这意味着潜在有价值的信息已从其中删除。 好奇这里是否使用它 fadvice() 或其他一些最小化技术,或者是因为缓存已从压缩过程中被破坏的块中释放出来?

故障后恢复

从失败中恢复需要时间,这是有充分理由的。 对于每秒一百万条记录的传入流,考虑到 SSD 驱动器,我必须等待大约 25 分钟才能执行恢复。

level=info ts=2018-09-13T13:38:14.09650965Z caller=main.go:222 msg="Starting Prometheus" version="(version=2.3.2, branch=v2.3.2, revision=71af5e29e815795e9dd14742ee7725682fa14b7b)"
level=info ts=2018-09-13T13:38:14.096599879Z caller=main.go:223 build_context="(go=go1.10.1, user=Jenkins, date=20180725-08:58:13OURCE)"
level=info ts=2018-09-13T13:38:14.096624109Z caller=main.go:224 host_details="(Linux 4.15.0-32-generic #35-Ubuntu SMP Fri Aug 10 17:58:07 UTC 2018 x86_64 1bee9e9b78cf (none))"
level=info ts=2018-09-13T13:38:14.096641396Z caller=main.go:225 fd_limits="(soft=1048576, hard=1048576)"
level=info ts=2018-09-13T13:38:14.097715256Z caller=web.go:415 component=web msg="Start listening for connections" address=:9090
level=info ts=2018-09-13T13:38:14.097400393Z caller=main.go:533 msg="Starting TSDB ..."
level=info ts=2018-09-13T13:38:14.098718401Z caller=repair.go:39 component=tsdb msg="found healthy block" mint=1536530400000 maxt=1536537600000 ulid=01CQ0FW3ME8Q5W2AN5F9CB7R0R
level=info ts=2018-09-13T13:38:14.100315658Z caller=web.go:467 component=web msg="router prefix" prefix=/prometheus
level=info ts=2018-09-13T13:38:14.101793727Z caller=repair.go:39 component=tsdb msg="found healthy block" mint=1536732000000 maxt=1536753600000 ulid=01CQ78486TNX5QZTBF049PQHSM
level=info ts=2018-09-13T13:38:14.102267346Z caller=repair.go:39 component=tsdb msg="found healthy block" mint=1536537600000 maxt=1536732000000 ulid=01CQ78DE7HSQK0C0F5AZ46YGF0
level=info ts=2018-09-13T13:38:14.102660295Z caller=repair.go:39 component=tsdb msg="found healthy block" mint=1536775200000 maxt=1536782400000 ulid=01CQ7SAT4RM21Y0PT5GNSS146Q
level=info ts=2018-09-13T13:38:14.103075885Z caller=repair.go:39 component=tsdb msg="found healthy block" mint=1536753600000 maxt=1536775200000 ulid=01CQ7SV8WJ3C2W5S3RTAHC2GHB
level=error ts=2018-09-13T14:05:18.208469169Z caller=wal.go:275 component=tsdb msg="WAL corruption detected; truncating" err="unexpected CRC32 checksum d0465484, want 0" file=/opt/prometheus/data/.prom2-data/wal/007357 pos=15504363
level=info ts=2018-09-13T14:05:19.471459777Z caller=main.go:543 msg="TSDB started"
level=info ts=2018-09-13T14:05:19.471604598Z caller=main.go:603 msg="Loading configuration file" filename=/etc/prometheus.yml
level=info ts=2018-09-13T14:05:19.499156711Z caller=main.go:629 msg="Completed loading of configuration file" filename=/etc/prometheus.yml
level=info ts=2018-09-13T14:05:19.499228186Z caller=main.go:502 msg="Server is ready to receive web requests."

恢复过程的主要问题是内存消耗较高。 尽管在正常情况下服务器可以在相同内存量下稳定工作,但如果崩溃则可能会因OOM而无法恢复。 我找到的唯一解决方案是禁用数据收集,启动服务器,让它恢复并在启用收集的情况下重新启动。

热身

预热期间要记住的另一个行为是启动后低性能和高资源消耗之间的关系。 在某些(但不是全部)启动过程中,我观察到 CPU 和内存负载严重。

Prometheus 2 中的 TSDB 分析

Prometheus 2 中的 TSDB 分析

内存使用量的差距表明 Prometheus 无法从一开始就配置所有集合,并且一些信息丢失。
我还没有弄清楚CPU和内存负载高的确切原因。 我怀疑这是由于在头部块中以高频率创建了新的时间序列。

CPU负载激增

除了会产生相当高的 I/O 负载的压缩之外,我还注意到 CPU 负载每两分钟就会出现严重的峰值。 当输入流量较高时,突发时间较长,并且似乎是由 Go 的垃圾收集器引起的,至少有一些核心已满载。

Prometheus 2 中的 TSDB 分析

Prometheus 2 中的 TSDB 分析

这些跳跃并不是那么微不足道。 看来,当这些情况发生时,Prometheus 的内部入口点和指标将变得不可用,从而导致同一时间段内的数据缺口。

Prometheus 2 中的 TSDB 分析

您还可以注意到 Prometheus 导出器关闭一秒钟。

Prometheus 2 中的 TSDB 分析

我们可以注意到与垃圾收集 (GC) 的相关性。

Prometheus 2 中的 TSDB 分析

结论

Prometheus 2 中的 TSDB 速度很快,能够使用相当普通的硬件处理数百万个时间序列,同时每秒处理数千条记录。 CPU 和磁盘 I/O 利用率也令人印象深刻。 我的示例显示每个使用的核心每秒最多可处理 200 个指标。

为了计划扩展,您需要记住足够的内存量,并且这必须是真实内存。 我观察到传入流每秒每 5 条记录使用的内存量约为 100 GB,加上操作系统缓存,占用的内存约为 000 GB。

当然,要抑制 CPU 和磁盘 I/O 峰值还有很多工作要做,考虑到 TSDB Prometheus 2 与 InnoDB、TokuDB、RocksDB、WiredTiger 相比还很年轻,这并不奇怪,但它们都有相似的地方生命周期早期的问题。

来源: habr.com

添加评论