来源:https://fabxc.org/tsdb/
作者:Fabian Reinartz
适用人群:对于时序数据库底层设计有兴趣的开发人员,和对底层技术工程管理有兴趣的管理人员
此版本为翻后精简版,主要保留了原作者对于时序数据库底层读写加速方面的思考过程。


首先,Prometheus是一个从一开始就朝着适配“以Kubernetes为核心的云原生环境”而设计的系统。
而云环境的一个重要特征就是“多变”。
监控系统越多变,对监控系统本身的压力也越大。为了不拖Prometheus其他部件的后腿,TSDB(译:本文中指代prometheus专用的时序数据库)将会主要聚焦于增加多变环境中的底层存储性能。

Prometheus当前使用的的v2存储层已经展示出了其出色的性能,即使一个服务器每秒上报一百万的采样分布在数百万的采样串中。所有一切都只占用一个非常小的磁盘空间。
所以,现在新开发的TSDB的开发目标是首先不差于当前的存储系统,然后还要能支撑更高数量级的数据。

注意:我之前没有做过数据库,所以下面的说法可能是错的。

问题,问题,还有问题占用的空间。

首先让我们确认一下我们的问题。

时序数据(Time series data)。

我们有一个会收集以下数据的系统。

identifier -> (t0, v0), (t1, v1), (t2, v2), (t3, v3), ....

每个数据点包含一个时间戳和一个数据。时间戳可能是任意整数甚至浮点数,一串采样点中的时间戳应该是递增的,一般也应该带有采样点的标签信息。
标签信息包含一堆标签,也可以称之为维度。标签维度用来对一个采样串中的各个数据进行分类统计。每个“统计名称”加上一个独有的标签组会形成一个特定的采样串,而数值会跟随在这个采样串里。
数据串本身也可以被累加统计,比如下面的例子

requests_total{path="/status", method="GET", instance=”10.0.0.1:80”}
requests_total{path="/status", method="POST", instance=”10.0.0.3:80”}
requests_total{path="/", method="GET", instance=”10.0.0.2:80”}

统计名称也可以被称为是一个统计维度,作为参数之一进行处理。

{__name__="requests_total", path="/status", method="GET", instance=”10.0.0.1:80”}
{__name__="requests_total", path="/status", method="POST", instance=”10.0.0.3:80”}
{__name__="requests_total", path="/", method="GET", instance=”10.0.0.2:80”}

在最简单的情况下,统计的名称确定的情况下,查询将变得简单,直接取出所有采样串的信息即可。

{__name__="requests_total"}

在复杂的情况下,我们必须把查询所有符合要求的数据,举例,比如上面的查询条件变成(method!="GET”)或者(method=~"PUT|POST”)的时候。
此时,所有数据预先储存为total就不可行了。

横坐标和纵坐标(Vertical and Horizontal)

简单地一瞥,所有数据点都可以被认为存放在一个二维表中。横坐标对应时间而纵坐标对应不同的采样串。

series^   │   . . . . . . . . . . . . . . . . .   . . . . .   {__name__="request_total", method="GET"}│     . . . . . . . . . . . . . . . . . . . . . .   {__name__="request_total", method="POST"}│         . . . . . . .│       . . .     . . . . . . . . . . . . . . . .                  ... │     . . . . . . . . . . . . . . . . .   . . . .   │     . . . . . . . . . .   . . . . . . . . . . .   {__name__="errors_total", method="POST"}│           . . .   . . . . . . . . .   . . . . .   {__name__="errors_total", method="GET"}│         . . . . . . . . .       . . . . .│       . . .     . . . . . . . . . . . . . . . .                  ... │     . . . . . . . . . . . . . . . .   . . . . v<-------------------- time --------------------->

Prometheus拿到数据点后,周期性地写入到不同的采样串中。每个上报者称为一个target。
写操作是完全并行的。
一个直观的数据:单个Prometheus实例收集的数据可以包括上万的目标,每一个又有上万个不同的采样串。
在收集每秒上百万的数据的情况下,并发写是一个不可质疑的性能需求。每次只单独写一个数据到磁盘是不可接受的。所以,我们需要每次写入的数据是一个大块的数据。
这是一个机械磁盘的针头进行物理的移动时的并不意外的磁盘要素。
但是SSD并没有这个问题,它们可以实际上进行随机写入。
SSD的问题是无法单独进行位或者字节级别的修改,每次修改都是4KB或者其倍数。这就导致了写入16字节和写入4KB的消耗是相同的。
这种特性也被称为写放大(写膨胀),这个问题同时也会导致一个额外负担:不是简单的变慢,而是字面意思地在短时间内摧毁你的磁盘。
在这个问题上的更多信息,有专门的“为SSD编程”作为指导。
我们需要考虑的是:顺序并且大量的写入,无论对机械磁盘还是SSD都是最佳操作。
查询的模式和写入的模式完全不同,我们能查询一个独立的数据点,或者在不同的数据串中查询同一个时间,或者在一个数据串中查询大量数据。
在我们的2维数据计划中,查询并不是纯粹的沿着横轴或者纵轴,而是实际上会是两者组合的一个矩形。
记录规则(相当于视图)减轻了已知查询中的上述问题,但是解决不了即席查询(Ad Hoc-也就是实时查询计算)的问题,但是实时查询计算的性能也需要优化。
我们已经知道我们想要大量写入,但是唯一的“大量”机会是跨采样线的多点数据的写入。
当对一个时间段内的一个单体数据串进行查询时,每个时间串内的单个数据点的查找都会麻烦,同时也需要从随机的多个位置从磁盘读取。
每个查询都可能触及上百万个数据点,这在最快的SSD上都会变慢。
读取也会读出比请求的16字节更多的数据。SSD会读取一整页,机械磁盘会读取一个sector(可能是512字节),无论哪种情况,我们都在浪费读取效率。
所以,理想情况下,同一个数据串的数据期望存放在顺序的区间内,然后我们可以比较容易地在短时间内扫描过所有数据。
同时,这也让我们只需要记录数据的起点即可。
当然理想的写模式和理想的读模式之间的差距是很大的。
这也是TSDB要解决的问题。

当前解决方案(Current solution)

现在我们来看下普罗米修斯当前的存储,版本V2,和当前的问题的关系。
我们创建一个文件为每个时间串。里面包含本时间串中所有数据,顺序存储。
在每个文件后面新增数据会耗时困难,我们在内存中对每个采样串维护了1K的内存空间,在内存满后把它移动到硬盘。
这解决了大部分问题。
批量写入,顺序读取。
这也产生了很好的压缩格式,因为采样数据很可能并不是剧烈变化的。Facebook也采用了这样的模式,每16byte数据可以压缩为1.37byte。
V2 存储使用了多种压缩格式,包括facebook Gorilla的压缩模式。

   ┌──────────┬─────────┬─────────┬─────────┬─────────┐           series A└──────────┴─────────┴─────────┴─────────┴─────────┘┌──────────┬─────────┬─────────┬─────────┬─────────┐    series B└──────────┴─────────┴─────────┴─────────┴─────────┘ . . .┌──────────┬─────────┬─────────┬─────────┬─────────┬─────────┐   series XYZ└──────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ chunk 1    chunk 2   chunk 3     ...

在分块存储解决了几个问题之后,为每个采样串保持一个单独的文件导致了以下问题:

  • 我们对每个收集的时间串,需要多个文件。在管理了几百万的文件后,inode是可能耗尽的。此时没有很好的修复方法,并且迁移也很困难。
    即使能做到,也是不应该去做的。
    我们不应该因为一个程序的原因就去格式化磁盘。

  • 即使已经分块,数千的块同时需要写入磁盘持久化,仍然需要大量的性能,这种延迟会回过头来加剧数据在内存的积压。

  • 所有文件同时打开读写是不可能的,部分是因为99%的数据24小时后从来不进行查询。
    另外,如果查询过就需要打开,查找数据然后载入进内存,这回导致很高的查询延迟。
    数据块同时严重依赖缓冲,这也可能导致问题更严重。

  • 最终,旧数据必须被删除,至少是从前端容易访问的文件中删除。这也导致了删除也是一个大规模的写操作。另外,遍历上百万的文件并对其进行分析会消耗好几个小时。在它结束时,可能就立刻需要再运行一次。
    删除文件也可能导致新的写放大。

  • 数据块只保存在内存中,所以程序如果挂了,数据就会丢失。为了避免此问题,内存数据会周期性地写入到磁盘上,但是这个写入周期不可能太短,这个保持周期内的数据仍然可能丢失。恢复内存数据也会有耗时而且需要重启多个程序。

从现在设计中提炼的核心概念是数据块。最新的数据块应该被保存在内存中,这个设计也是合理的。
最新的数据总是最经常被查询。
每个采样串保持一个文件的设计是我们需要改变的。

串桶 (Series Churn)

在Prometheus系统中,我们使用名词“串桶”来描述一组过期的时间顺序采样串:指的是不再接受任何数据,并且用新的时间串来接受数据的情况。
举例,所有的串都通过一个微服务实例来暴露给访问者,他们都有一个“实例”标签来标明来源。
如果我们滚动更新了我们的微服务,然后所有的实例都升级了,那么“串桶”就出现了。
在更多变的环境中,这个事情每个小时都会发生。
集群系统可能持续的扩容或者升级。
随时可能产生上千个新的数据实例,并且其中产生大量的新的时间串。

series^│   . . . . . .│   . . . . . .│   . . . . . .│               . . . . . . .│               . . . . . . .│               . . . . . . .│                             . . . . . .│                             . . . . . .│                                         . . . . .│                                         . . . . .│                                         . . . . .v<-------------------- time --------------------->

此图表示,随着时间变化,可能旧的串不会再更新了。
所以,即使整个基础架构的大小不变的,时间串的数量也会随着时间进步而修改。
在普罗米修斯服务器同时可以处理千万个时间串的同时,查询却可能是要处理10亿级别的数据。

当前解决方案 Current solution

当前的V2存储,有一个基于LevelDB的索引,为查询所有串服务。它允许查询关于一个label标签的所有串,但是缺少易用的组合多个标签的方法。
例如,查询所有名字为requests_total的标签对应的串很容易,但是查询所有实例名为A同时串名叫“requests_total”的,就会有性能问题。
我们等会再来看这个问题的原因然后看怎么解决。
这个问题是我们需要找到新的存储系统的最主要动力。
Prometheus需要提升其索引能力以帮助快速所有上亿的时间串。

资源消耗(Resource consumption)

资源消耗是Prometheus扩容时最重要的问题。
但是导致用户麻烦的并不会是绝对的资源紧缺。
实际上,Prometheus在有限的资源里做到了不可置信的工作。
但是问题是,环境资源是不可预测的,并且不是持续的。
V2存储架构创建采样数据的块,会导致内存消耗随着时间增长而增长。
数据写入磁盘后,内存空间可以释放。
最终,Prometheus的内存消耗量会达到一个稳定数值。
这种稳定会随着监控的设备和环境的变化而消失。新的采样串和串桶会增加内存,cpu,磁盘的消耗。
如果这种改变持续下去,它会再一次进入一个持续稳定的状态。但是又会明显高于一个更稳定的环境。
数据交换的周期经常是几个小时,并且很难确定最大的资源用量是多少。
对每个时间采样串保留一个文件同样会让单一查询变得容易。
当查询不在内存中的数据,文件被打开,相关数据被读入内存。如果数据量超过可用内存,Prometheus会触发OOM。
在查询结束后,载入的内存可以释放了,但是一般来说会在缓冲中保留一段时间,因为相同的查询可能马上还会到来。这可以提高查询的效率。
最后,我们观察了SSD的写放大情况,并且,Prometheus在这方面做了合并的处理.
最终,它在多个位置都可能由于小型的写入而导致写放大,但是至少消除了页内操作导致的写放大。
对于更大的Prometheus服务器,硬件的生命周期更短。
对于数据库程序而言,这样的大写入量是常见需求,但是我们需要注意是否可以迁移数据。

开始 Starting Over

现在我们有一个在我们的问题领域的好主意,知道了V2怎么解决问题,也知道V2的问题。
我们也看到了一些升级的思路,并且想要无缝实现。
大量的V2的问题可以通过特定的优化和重新设计来解决。
但是为了有趣,我重写了一个时间数据库。
从0开始。
关键的性能和资源使用率考虑,是选择的数据存储格式的直接结果。
我们必须找到一组正确的算法和磁盘格式,来实现一个性能有优势的存储层。
这就是我的解决方案的捷径,跳过那些头痛的的失败设计和修改。

V3-概要设计 (Macro Design)

存储的概要设计是什么样的?
长话短说,在使用树状数据时,所有问题都会揭示出来。
看一下下面这个图片

$ tree ./data
./data
├── b-000001
│   ├── chunks
│   │   ├── 000001
│   │   ├── 000002
│   │   └── 000003
│   ├── index
│   └── meta.json
├── b-000004
│   ├── chunks
│   │   └── 000001
│   ├── index
│   └── meta.json
├── b-000005
│   ├── chunks
│   │   └── 000001
│   ├── index
│   └── meta.json
└── b-000006├── meta.json└── wal├── 000001├── 000002└── 000003

在顶层,我们有按顺序排列的block,前缀是b-
每一个数据块,都含有一个文件,文件包含一个索引,和一个“库”目录,存放有更多的编号文件。
“库目录”含有库文件,库文件包含多个时间串的数据点。
在V2上,这导致了读取同时间段的串数据的代价非常低,
并且允许我们应用同样非常有效的压缩算法。
这个概念已经被证明工作很好,并且我们将坚持这个做法。
显然地,不再是一个时间串一个单独文件。
但是,作为替代的,少量的文件会保存大量的“库”来保存更大量的时间串
索引文件的存在不让人惊讶。
让我们先假设索引文件靠着大量的黑魔法来允许我们查找标签,可能的值,整个时间串,还有包含这些数据的库文件。
但是为什么这些目录包含这样的结构,索引,还有库?
并且,为什么最后一个目录含有一个wal目录?明白了这两个问题,
就解决了90%的我们的问题。

大量的小型数据库 Many Little Databases

我们把我们的平面维度,举例,时间,分割到不重叠的块中。
每个块扮演一个完全独立的数据库,里面包含所有的时间串。
因此,它有它自己的索引和库文件。

t0            t1             t2             t3             now┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐│           │  │           │  │           │  │           │                 ┌────────────┐│           │  │           │  │           │  │  mutable  │ <─── write ──── ┤ Prometheus ││           │  │           │  │           │  │           │                 └────────────┘└───────────┘  └───────────┘  └───────────┘  └───────────┘                        ^└──────────────┴───────┬──────┴──────────────┘                              ││                                                  query│                                                    │merge ─────────────────────────────────────────────────┘

每个数据块都是不可修改的。
当日,我们必须可以增加新的时间串和采样到最新的块中。
再这个块中,所有新数据都被写入到一个内存数据库中。
这个内存数据库可以提供和永久存储一样的存储功能。
内存中数据结构能被更有效地更新。
为了阻止数据丢失,所有进入的数据都会先被写入到一个临时的写入日志。这些写入日志在wal目录中。它里面我们可以重建所有的内存中数据。
所有这些文件都有他们自己的串行格式,里面包含了:各种标志位,偏移,变量,校验码。解释起来很烦,用起来会感觉不错。
这个结构允许我们在输出查询时,一次输出到所有的时间相关的块上。
每个块上分别的数据,会在返回时进行合并。
这种横巷分割增加了以下能力。

  • 档查询一个时间段,我们能轻松地忽略所有此时间段以外的区域。它更会处理串桶,同时减少查看数据的次数。
  • 当完成一个块,我们能保存数据从内存数据库中,顺序地写入大量数据文件。
  • 我们避免了ssd上的写膨胀。
  • 我们保存了V2的优秀的就近属性,使其查询时保存在内存中。
  • 我们不在担心固定的的1K边界,我们可以采用任意大小和任意格式
  • 删除就数据的待解变得很小。我们只需要删除一个独立目录。记住:在旧的存储中,我们必须分析并且重写数百万个文件,这些会话费很多个小时。
    每个块都包含meta.json文件。它简单地包含人类可读的关于这个块的信息,这样便于理解和管理
内存映射 mmap

移动上百万的文件到少量的大型文件,允许我们同时打开这些文件。
这也导致了我们可以使用mmap,一个系统调用,允许我们透传一份内存数据到文件。
简单地说,你可以认为它就像交换区。只不过换出的同时,数据已经在硬盘上了,不需要再写入一次。
这就导致了我们可以对待所有数据库都当作内存数据库。
当我们访问数据库文件中的指定区域时,操作系统会把对应数据从磁盘上load上来。
这导致了所有内存数据都被操作系统关联上了持久的硬盘数据。
最终,这个决定可以提高整个系统的质量,因为这种持久化是跨越进程的。
查询数据可以更好滴在内存中集中,直到内存压力导致页面被释放。
只有机器有未使用的内存,Prometheus就可以缓冲更多的数据库。当其他程序需要的时候,操作系统又能通过mmap的过期来换出内存。
因此,查询能够轻易滴导致进程OOM,因为查询可能需要超出内存容量的数据。
内存缓存大小此时变得适应系统的需要。
从我这里理解,这就是很多数据库工作的方式,并且,只要磁盘格式允许,这就是理想的方式。除非有东西干扰了操作系统管理进程的方式。
到这里,我们只需要做很少的事情了。

平整 Compaction

存储会周期性地切出一个新的快,然后把之前的内容写入硬盘,此时之前的内容已经是完全记录完毕的。
只有在块数据被成功的持久化存储之后,wal(write ahead log文件)这个用来做数据恢复的文件才会被删除。
我们的注意力在保持每个块不太大,默认是两个小时的数据,以避免在内存中累加太多的数据。
当我们查询多个block的数据时,我们不得不合并他们的结果到一个大结果中。
这个合并当然会带来成本,并且一周长的查询也不应该得到80片需要合并的大块数据。
为了达到这两个目标,我们引入了平整功能。
平整描述的是将多个块写入到一个更大的块里的过程。
它还可能修改已有的数据,比如,丢弃已删除的数据,或者重新组织数据以提高查询效率


t0             t1            t2             t3             t4             now┌────────────┐  ┌──────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐│ 1          │  │ 2        │  │ 3         │  │ 4         │  │ 5 mutable │    before└────────────┘  └──────────┘  └───────────┘  └───────────┘  └───────────┘┌─────────────────────────────────────────┐  ┌───────────┐  ┌───────────┐│ 1              compacted                │  │ 4         │  │ 5 mutable │    after (option A)└─────────────────────────────────────────┘  └───────────┘  └───────────┘┌──────────────────────────┐  ┌──────────────────────────┐  ┌───────────┐│ 1       compacted        │  │ 3      compacted         │  │ 5 mutable │    after (option B)└──────────────────────────┘  └──────────────────────────┘  └───────────┘

在上面的例子里,我们可能有1,2,3,4个块
Block1,2,3,可以被平整,然后块编号变成了1,4
也可以变成平整为1,3
所有数据都还在,但是数据编号变少了,block变少了。
最大的作用是在查询的时候需要合并的数据源变少了。

持久化 Retention

我们发现,删除旧数据在V2 存储上,是一个缓慢而痛苦的过程,并且对CPU,内存,磁盘,造成很大压力
怎么在块式设计的基础上删除旧数据?
简单,删除包含这些块的目录即可,目录可通过是否在我们的存储时间区域内来确定。
在下面的例子里,block1 可以删除,2则不会,因为2是存储区域的下沿。

                      |┌────────────┐  ┌────┼─────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐│ 1          │  │ 2  |     │  │ 3         │  │ 4         │  │ 5         │   . . .└────────────┘  └────┼─────┘  └───────────┘  └───────────┘  └───────────┘||retention boundary

更旧的数据更有可能在持续的平整中变成更大的数据块。
一个块大小的上限必须设置出来,一阻止块大小变得像整个数据库一样大,然后干掉我们原来的分块设计。
同时,这个设计也限制了在整个磁盘上的部分在时间窗内并且部分在时间窗外的块的数量
2号块就是一个例子。
当设置最大块大小为百分之十的保留时间时,我们的总的保留block2的消耗也控制在10%
(译注:这里的逻辑我也没看懂)
总结下来,保留数据的删除,从非常复杂,变成了非常容易。

如果你看到了这里,并且有一定数据库基础,你可能会问:这是新技术吗? 不是新技术,但是是好技术。
在内存中批量处理数据,在wal中跟踪,然后周期性写入磁盘是现在普遍存在的技术。
调研以上技术的好处在于我们发现这些技术的优势基本无视数据量而存在。
也可以参考主流的开源例子,像LevelDB等这样的实例。
更重要的事情是,要避免重新发明一个更差的轮子,探索一个已经被证明的方法,或者同时做上面两件事情。
不走寻常路,加你的独有配料,是一个不太好的设想。

索引 The Index

最开始的调研存储进步的动力是串桶的问题导致的。
块为基础的层级关系,减少了总的查询时需要访问的串的数量。
假设我们的索引搜索是复杂度为O(n ^ 2),那么我们相当于大量地减少到了O(n ^ 2),所以提高了总的性能。
但是,等等,糟糕
算法基础课程从我脑子里一闪而过,理论上,这并没有解决问题,如果事情之前很糟糕,之后也会很糟糕。
在实践中,大部分我们的查询已经惊人地快了。
但是,查询少数几个块的长时间的信息仍然会很慢。
我们的最开始的想法是,在梳理了我们从一开始做的所有工作后,结论是,为了解决这个问题,我们需要一个更好用的“倒排索引”。
“倒排索引”给我们一种更快地基于其内容查询数据的能力。
简单地说,我能查询到所有含有lable app=nginx的串,但是不需要遍历所有时间串并且确认其中是否 包含这个label
因为,所有的串都有一个唯一的ID,通过这个ID可以直接找到它们。在当前情况下,这个ID就是我们的正排索引。

举例:如果串ID10,29,9都包含nginx,那么nginx的倒排索引就会包含10,29,9,这样我们就能快速定位到那些串包含这些label了。即使我们有2百亿个串,我们也可以很快找到我们的数据。

简单地说,如果n是我们的串的总数量,m是查询结果大小,那么使用这套倒排索引的查询复杂度是O(m)。 查询的规模与查询结果相关,而不再与总的需要搜索的数据总量相关。

简单地说,我们可以假设我们能持续地按时间获得倒排索引
实际上,这就是V2已经有的倒排索引了。并且,只需要很少资源 就可以服务几百万的串了。
眼尖的观察者已经注意到了,在最差的情况下,一个lable可能存在于所有的串中,然后又会把复杂度变成O(n)
这是可预期的行为,并且可以接受。如果你查询所有数据,当天它会消耗更多的时间。
真正的问题发生在,我们要处理复杂的查询的时候。

组合标签 Combining Labels

一个标签和上百万个时间串相关是常见现象。想想一个横向扩展的 foo 微服务,带有数百个实例,并且每个实例力有上千个数据。
每个单独的串会有一个labe app=foo,当然,一个人不会想要查询所有这些时间串,而智慧想要查询某一些时间串。
比如,我可能想要知道我的服务实例收到了多少请求。那么查询会写成 name=“requests_total” AND app=“foo”.
要找出所有标签相关的时间串,我们需要一个反向索引来标记这些时间串。
结果set会比全部浏览一次信息产生的请求要少得多。
每个查询列,最差是O(n),那么两个条件的查询,最差结果是O(n ^ 2)
其他类似的查询也应花费相同的时间花费
当增加新的label信息时,期望查询时间会变成O(n ^ 3),O(n ^ 4),O(n ^ 5)等等。
很多的手段可以被用于缩小这个运行时间,比如修改执行顺序。
越复杂,越多关于数据的总结信息和关联信息会需要。
这会引入很大的复杂度。并且不会降低我们的算法最差时间。
这是V2存储最终的情况,幸好,看起来一个小改动就可以在此基础上获得巨大的提升。
如果我们假设我们的ID在我们的反向索引里是排序过的呢?

__name__="requests_total"   ->   [ 9999, 1000, 1001, 2000000, 2000001, 2000002, 2000003 ]app="foo"              ->   [ 1, 3, 10, 11, 12, 100, 311, 320, 1000, 1001, 10002 ]intersection   =>   [ 1000, 1001 ]

假设上面的例子。两个lable的并集实际上很小。
我们能找到这个并集通过设置查询从表格的开始地方开始查询,然后假设所有数据都收从小到大排列。
当数据相等,就把数据加入结果中。
总体来说,我们可以把总的查询时间从O(2n) ,然后简写为O(n)。
处理超过两个查询条件的方法和上述流程类似。
然后最差期望时间就从O( n ^ k)变成了O(k * n).
这是一个显著的进步。
我这里描述的是一个简化版本的“全文搜索引擎”的实际应用。
每个时间串都被当作是一个文档。每个标签都被当作一个“单词”
我们能忽略大量额外的搜索引擎需要的数据,比如词位置和词频率。
“相近的搜索”的存在也会影响我们提高实际处理效率,我们可以对请求输入进行一点猜测。
不令人惊讶地,还有大量的压缩技术可以用在反向索引中。
但是因为我们文档相对都比较小,而 单词都比较大,所以压缩就显得没那么重要了。
举例,一个实际存在的数据库,有4百4十万的时间串,12个标签组,每个标签组里有5000个标签。
在我们的初始存储中个,我们坚持使用非压缩的数据,使用较少简单办法跳过无效数据。
保持ID有序听起来简单,实际上也不难。实际上,V2存储计算了ID的hash,导致了反向索引不那么好建立有序的查找。
另外一个程序用来在数据删除和更新的时候修改索引。
典型的,最简单的方法是简单低重计算和重写它们,但是同时保持数据库又可以查询。
V3存储就把这一点做的很好,对于每个块有一个不可变的索引,只有在整合时可能修改,
只有修改了的块,它们肯定在内存里,才会触发索引的修改。

性能测试 Benchmarking

我针对性开发的存储的时候使用的数据是一个4百万个属性时间串的基于真实世界的数据。
以下迭代测试是为了指明存储的瓶颈和高并发时的死锁。
当我们前面设计的概念都实现了的时候,目标存储的性能应该可以在mac笔记本上支持2千万级别的数据,同时这个主机上还应该可以运行其他的网页服务器和其他计算。
这听起来很不错,但是也意味着我们会暂停在这个水平,不会再做根本性的重新设计了。
对于一个第一个印象,这个水平够了。
在达成了最初的设计目标的20倍性能后,现在是时候把它集成到Prometheus服务器里了,增加所有实际运行时需要的代码,并使其适配实际的环境。
我们实际上没有一个完全针对Prometheus的性能测试。本来应该有A/B测试来对不同版本的Prometheus测试的(但是现在有了
我们的工具允许我们创建一个性能测试场景,这个场景甚至也可以部署在kubernetes的云集群里。
这不是最适合我们性能测试的场景,但是是最和我们的用户类似的场景。
我们部署了带有v2存储的1.5.2服务器和带有v3存储的2.0服务器
每个Prometheus服务器都跑在ssd上。
平行扩展的微服务统计部署在负载上。
同时kub集群自己也是被监控着的。
这个设置被另外一个prometheus监控着,监控这些Prometheus server的健康状态和性能。
为了模拟数据串,微服务周期性地扩容和缩容,去掉旧容器,部署新容器,产生新的串。
查询负载是参考一堆典型的查询模拟出的。每组Prometheus server跑一组查询。
总之,扩容和查询的数量都是明显超出当今Prometheus会真的使用的产品的情况的。
举例,我们有60%的微服务实例子只会存活15分钟。
而现实世界这样的短期容器每天应该只会有1到5次。
这保证了v3存储有能力支撑数年后的数据规模变化。
从结果而言,1.5和2.0的存储性能差距比环境导致 的结果差异大很多。
总的来说,我们抓了十多万个性能取样点。
在这些设置跑了 一段时间后,我们看了下数据。我们估计的是12个小时后各自版本会进入一个稳定期。
另外,本图里的Y轴相对我们实际在prom中看到的,截断了一点。

Prom服务器消耗的存储

内存使用量是最麻烦的资源,因为数据量越来越难以预估,所以内存使用量甚至可能导致程序挂掉。

很明显,查询服务会消耗更多的内存。这是由于查询引擎导致的,而这是下一步优化的目标。

整体而言,prom2的内存消耗降低了3到4倍。

在6小时后,1.5prom有一个陡增,这可能是由于我们每6小时一次数据长期化保存。

因为删除动作消耗很大,所以资源消耗增长了,以至于在图表中能明显看出趋势。

Cpu利用率 每秒使用的核数

类似的性能表现也体现在cpu上。但是查询导致的消耗差异更大。

平均11万个请求点每秒会消耗0.5个核。我们的新存储在查询上多消耗的cpu相比旧版本也要好很多。

整体而言,新版本带来3到10倍的cpu资源减少。

磁盘写数量,每秒

目前最大和最意外的优化来自于磁盘。
很明显的,1.5很容易写穿ssd。
在第一个chunk创造后,旧版本就迎来写入需求的攀升,然后在第一次删除后数据又攀升了一次。
惊讶的是查询也会导致磁盘写入的性能产生差异(译注:可能查询导致了page buffer的更新变得更慢了)
Prometheus 2.0 几乎只写每秒1M到WAL里。
写入的毛刺和数据整理合并有关。
整体节省存储97-99%

磁盘消耗,GB

写入速度也和写入总量相关。
因我们用了同样的压缩算法,所以单个数据串的数据存下来本身应该是一样的。
在实际应用中本应一样。但是我们这里模拟的是一个重复创建新churn的场景。
我们可以看见,Prometheus 1.5 更快地耗尽了存储资源。
Prometheus 2.0 在单个数据串上消耗的资源更少。
我们能很好地看到空间是跟随WAL线性增长的,并且会因为合并而下降。
至于查询为何会导致写入的数据量不同,这还需要另外调查。

以上已经让我们感觉存储达标了。下一个重要问题是:查询延迟。

新的索引应该能改进我们的查询复杂性。
因为我们比较的数据是没有多大差别的。

查询复杂度主要体现在rate函数和聚合功能里

百分之99的查询延迟(按秒计算)

在这些数据上,我们达到了预期
在Prometheus 1.5上,查询延迟会随着时间推移,新数据串常见而增加。
只有当旧数据删除的时候,延迟会减少。
相比而言,Prometheus 2.0 保持了一开始的速度。
对于以上数据的采集方法这里要加一个注意:

测试时发起的查询是在一定的随机范围的,可能计算重也可能轻,可能触及多或者少的数据。
这个测试查询和现实世界的查询的常态时不一致的,但是也没有必要一致。
(译注:这表示现实使用1.5时,并不一定会感知到巨大的延迟增加

统一的,这些查询没有特意选择冷数据或者热数据,所以我们反而可以认为所有数据均匀地存在于内存中。

尽管如此,我们仍然有自信地说,整体查询性能变得更能适应更大规模的数据,并且在测试中有大约4倍的性能提升。

在一个更稳定的环境里,我们可以假设查询时间会更多地被消费在查询引擎自己上,所以存储的提升会明显减小。

每秒可以吃下的采样数量

最后,我们来快速看一下我们接受信息的速度。
我们可以看到,两个v3服务器有同样的接受速度。
在几个小时后后,它变得不太稳定。由于高压力下无响应状态。但是2.0没有这个问题,或者,至少和1 不是同一个额问题
整个1.5的Prometheus服务器都遭受了重大性能下跌。虽然实际上还有更多资源可用。
高压制造数据桶导致了大量的数据无法收集。
但是,你现在实际实际上能处理的数据量会有多大?
我不知道。并且,它也是一个很容易衡量和优化的目标。
它并不是一个在具体的版本之外讨论时很有意义的数据。
有大量的因素影响着Prometheus接受数据的能力。
最大接受能力测出的极限值,是在故意适配其场景和故意不进行查询和数据合并的情况下测试出的。
初略地说,随着数据增加,消耗的资源也增加。基础的测试证明了这个思路。
我们可以基于这些信息轻松推算出可能发生的事情:
我们的性能设置模拟一个更高的动态环境压力,比现实世界可能出现的更大。
测试结果告诉我们,我们已经达到了自己最初计划的设计目标,并且是运行在没有特意优化的云服务器上。

最终,是否可用还是有用户来决定,而非性能测试。

备注:现在写这篇文章的时候,Prometheus 1.6 正在开发中,新版本可以配置内存最大使用量,并且更稳定,而且资源消耗没有增加。
我没有重复对上述问题在1.6上测试,因为1.6并没有针对性解决2.0解决的问题,特别是大量数据串的问题。

结论 Conclusion

Prom正在被设计成一个可以支撑大基数的串和输出和独立取样的软件。
这个目标仍然有一定挑战,但是新设计的存储为我们的未来开了个好头。
感谢所有被我对SSD,浮点数,还有串行数据的唠唠叨叨烦过的人。

(完。)

[渣翻]从零开始写一个时序数据库相关推荐

  1. 从零开始写一个武侠冒险游戏-3-地图生成

    2019独角兽企业重金招聘Python工程师标准>>> 从零开始写一个武侠冒险游戏-3-地图生成 概述 前面两章我们设计了角色的状态, 绘制出了角色, 并且赋予角色动作, 现在是时候 ...

  2. 如何搭建python框架_从零开始:写一个简单的Python框架

    原标题:从零开始:写一个简单的Python框架 Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发. 你为什么想搭建一个Web框架?我想有下面几个原因: 有一个 ...

  3. mysql c测试程序_Linux平台下从零开始写一个C语言访问MySQL的测试程序

    Linux 平台下从零开始写一个 C 语言访问 MySQL 的测试程序 2010-8-20 Hu Dennis Chengdu 前置条件: (1) Linux 已经安装好 mysql 数据库: (2) ...

  4. 从零开始写一个武侠冒险游戏-6-用GPU提升性能(1)

    从零开始写一个武侠冒险游戏-6-用GPU提升性能(1) ----把帧动画的实现放在GPU上 作者:FreeBlues 修订记录 2016.06.19 初稿完成. 2016.08.05 增加对 XCod ...

  5. python提取数据库nosql_用 Python 写一个 NoSQL 数据库

    本文译自 What is a NoSQL Database? Learn By Writing One In Python. 完整的示例代码已经放到了 GitHub 上, 请 点击这里, 这仅是一个极 ...

  6. 从零开始写一个武侠冒险游戏-8-用GPU提升性能(3)

    从零开始写一个武侠冒险游戏-8-用GPU提升性能(3) ----解决因绘制雷达图导致的帧速下降问题 作者:FreeBlues 修订记录 2016.06.23 初稿完成. 2016.08.07 增加对 ...

  7. python编写数据库连接工具_详解使用Python写一个向数据库填充数据的小工具(推荐)...

    一. 背景 公司又要做一个新项目,是一个合作型项目,我们公司出web展示服务,合作伙伴线下提供展示数据. 而且本次项目是数据统计展示为主要功能,并没有研发对应的数据接入接口,所有展示数据源均来自数据库 ...

  8. 自己写一个微型数据库_“最国际化的微型机构:”两名伦敦训练营的毕业生如何建造了一个远程…...

    自己写一个微型数据库 by Rebecca Radding 由丽贝卡·拉丁(Rebecca Radding) "最国际化的微型机构:"两名伦敦训练营毕业生如何与加沙的软件工程师建立 ...

  9. dotnet 从零开始写一个人工智能 从一个神经元开始

    现在小伙伴说的人工智能都是弱智能,可以基于神经网络来做.而神经网络是有多层网络,每一层网络都有多个神经元.那么最简单的神经网络就是只有一层,而这一层只有一个神经元,也就是整个神经网络只是有一个神经元. ...

最新文章

  1. java matlab 矩阵_如何在MATLAB中将函数应用于矩阵的每一行/列?
  2. python操作MYSQL数据库(2018-9-27)
  3. 小学计算机室教室的简报,高新区第三小学开展“信息技术与教育教学融合创新发展”培训...
  4. linux磁盘符变化autofs,Linux基础教程学习笔记之Autofs自动挂载
  5. php定义数据表类,phpwind中的数据库操作类
  6. 关于体育的python毕业设计_Python实例13:体育竞技分析
  7. Android 之数据传递小结
  8. vSAN其实很简单-如何榨干vSAN的最后的空间- Part2(转)
  9. silverlight数据绑定
  10. 编码 GBK 的不可映射字符
  11. Reading Thinking in Java #3
  12. SpringBoot整合QueryDSL
  13. Apollo OpenDRIVE和ASAM OpenDRIVE的区别
  14. DO Global亮相DMEXCO 2018,发布全新智能DSP
  15. android多悬浮窗口播放器,Android实现悬浮播放器
  16. 按钮 蓝底白字 html,为什么ChemDraw Professional 15颜色设置总是蓝底白字?
  17. Cython基础使用
  18. 老扎克伯格的四位儿女全是人生赢家,到底是怎么教的?
  19. 2021年材料员-通用基础(材料员)考试试题及材料员-通用基础(材料员)作业模拟考试
  20. 计算机学院新生篮球赛名字,计算机学院新生篮球赛圆满结束,获奖队伍公布!...

热门文章

  1. 从程序员到项目经理(一)
  2. 西门子Simotion运动控制
  3. python的内置数据结构_Python基础知识2-内置数据结构(上)
  4. c语言大数运算知乎,为什么知乎上大多数人不推荐C语言入门?
  5. 鸿蒙如何用JS开发智能手表App
  6. 关于魔趣刷机(含root)步骤
  7. mysql 的事件_一文总结MySQL数据库事件--定时任务实现方式
  8. Vue2知识点 - RT
  9. python小球游戏代码
  10. arcgis 空间交集 计算_ArcGIS叠置分析之相交分析