Prometheus 原理分析 —— 深入TSDB存储
聊聊 Prometheus 的时序数据库 TSDB ~
一些概念
- 时序数据库 : 专门用来存储随时间变化数据的数据库,如股票价格、传感器数据等
- 时间序列 : 某个变量随时间变化的所有历史
- 样本 : 历史中该变量的瞬时值
关系如图所示 :
Prometheus 中的时序数据库
要了解Prometheus 中的时序数据库,首先得看一下官方架构图。如图所示:
Prometheus 中时序数据存储结构
Prometheus 中时间序列是按照时间戳和值的序列顺序存放的,每条time-series通过指标名称(metrics name)和一组标签集(labelset)命名。如下所示,可以将时间序列理解为一个以时间为Y轴的数字矩阵:
|
|
上图中的每一个点称为一个样本,样本由两部分组成,源码内容如下(release-2.20版本) :
|
|
- Metric 为metricName和label内容(labelset)
- Point 为当前样本时间和浮点数据值
有人很可能会好奇Prometheus中存储的数据不应该为四部分数据么?(指标、标签、时间戳、样本值),这是因为很多书籍方便给大家的一些基础认知,实际上Prometheus在底层数据存储的时候做了一些处理,把指标这一部分转化成了标签进行处理(具体原因作者还需要找一些认证材料,后续更新),源码如下(release-2.20版本):
|
|
|
|
TSDB 数据写入
磁盘部分
要了解TSDB的数据写入,首先得了解TSDB的目录结构
|
|
默认Prometheus的所有监控数据都会放在 ./data 目录下面,里面存放了 chunks_head、wal、block(01BKGV7JBM69T2G1BGBGM6KB12等) 三种类型的数据。
-
block : 每个 block 会存储 2 小时时间窗口内所有 series 指标数据,每个 block 文件名都会使用 github.com/oklog/ulid 这个库生成不重复的文件名。一个块包含4个部分(meta.json 文件、index 文件、tombstones 文件、chunks 文件夹)。
-
meta.json (块的元数据)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
{ // 虽然目录名称设置为 ULID,但只有 as 中存在的那个meta.json才是ulid有效 ID,目录名称可以是任何名称。 "ulid": "01BKGV7JBM69T2G1BGBGM6KB12", // minTime和maxTime块中存在的所有块之间的绝对最小和最大时间戳。 "minTime": 1602237600000, "maxTime": 1602244800000, // stats包含块中存在的时间序列、样本和块的数量的信息。 "stats": { "numSamples": 553673232, "numSeries": 1346066, "numChunks": 4440437 }, // compaction讲述该区块的压缩历史。level告诉我们这个区块已经经历了多少次压缩。sources告诉这个块是从哪些块创建的(即,哪些块被合并形成这个块)。如果它是从 Head 块创建的,则将其sources设置为其自身(01BKGV7JBM69T2G1BGBGM6KB12在本例中)。 "compaction": { "level": 1, "sources": [ "01EM65SHSX4VARXBBHBF0M0FDS", "01EM6GAJSYWSQQRDY782EA5ZPN" ] }, "version": 1 // 告诉我们如何解析元文件。 }
-
chunks (包含原始块,没有任何有关块的元数据)
chunks目录包含一系列类似于 WAL/checkpoint/head 块的编号文件。每个文件的上限为 512MiB。这是该目录中单个文件的格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
┌──────────────────────────────┐ │ magic(0x85BD40DD) <4 byte> │ ├──────────────────────────────┤ │ version(1) <1 byte> │ ├──────────────────────────────┤ │ padding(0) <3 byte> │ ├──────────────────────────────┤ │ ┌──────────────────────────┐ │ │ │ Chunk 1 │ │ │ ├──────────────────────────┤ │ │ │ ... │ │ │ ├──────────────────────────┤ │ │ │ Chunk N │ │ │ └──────────────────────────┘ │ └──────────────────────────────┘
它看起来与内存映射头块文件非常相似。该magic数字将该文件标识为块文件。version告诉我们如何解析这个文件。padding用于任何未来的标头。接下来是块列表。 以下是单个块的格式:
1 2 3
┌───────────────┬───────────────────┬──────────────┬────────────────┐ │ len <uvarint> │ encoding <1 byte> │ data <bytes> │ CRC32 <4 byte> │ └───────────────┴───────────────────┴──────────────┴────────────────┘
它看起来再次类似于磁盘上的内存映射头块,只是它缺少series ref,mint和maxt。我们需要 Head 块的这些附加信息来在启动期间重新创建内存中索引。但对于块来说,我们在 中拥有这些附加信息index,因为索引是它最终所属的地方,因此我们在这里不需要它。 引用的长度是 8 个字节。前 4 个字节告诉该块所在的文件号,最后 4 个字节告诉该块在文件中的起始偏移量(即 的第一个字节)len。如果该块位于文件中00093并且该len块的起始于文件中的字节偏移量1234,则该块的引用将为(92 « 32) | 1234(左移位,然后按位或)。文件名使用基于 1 的索引,而块引用使用基于 0 的索引。因此在计算块引用时00093被转换为92
-
index
索引包含查询该块数据所需的所有内容。它不与任何其他块或外部实体共享任何数据,这使得可以在没有任何依赖性的情况下读取/查询块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
┌────────────────────────────┬─────────────────────┐ │ magic(0xBAAAD700) <4b> │ version(1) <1 byte> │ ├────────────────────────────┴─────────────────────┤ │ ┌──────────────────────────────────────────────┐ │ │ │ Symbol Table │ │ │ ├──────────────────────────────────────────────┤ │ │ │ Series │ │ │ ├──────────────────────────────────────────────┤ │ │ │ Label Index 1 │ │ │ ├──────────────────────────────────────────────┤ │ │ │ ... │ │ │ ├──────────────────────────────────────────────┤ │ │ │ Label Index N │ │ │ ├──────────────────────────────────────────────┤ │ │ │ Postings 1 │ │ │ ├──────────────────────────────────────────────┤ │ │ │ ... │ │ │ ├──────────────────────────────────────────────┤ │ │ │ Postings N │ │ │ ├──────────────────────────────────────────────┤ │ │ │ Label Offset Table │ │ │ ├──────────────────────────────────────────────┤ │ │ │ Postings Offset Table │ │ │ ├──────────────────────────────────────────────┤ │ │ │ TOC │ │ │ └──────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────┘
与其他文件一样,该magic数字标识该文件为索引文件。version告诉我们如何解析这个文件。
-
tombstones
墓碑是删除标记,即它们告诉我们在读取过程中要忽略哪个系列的什么时间范围。这是在写入存储删除请求的块之后创建和修改的块中唯一的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13
┌────────────────────────────┬─────────────────────┐ │ magic(0x0130BA30) <4b> │ version(1) <1 byte> │ ├────────────────────────────┴─────────────────────┤ │ ┌──────────────────────────────────────────────┐ │ │ │ Tombstone 1 │ │ │ ├──────────────────────────────────────────────┤ │ │ │ ... │ │ │ ├──────────────────────────────────────────────┤ │ │ │ Tombstone N │ │ │ ├──────────────────────────────────────────────┤ │ │ │ CRC<4b> │ │ │ └──────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────┘
-
-
chunks_head : 这个文件夹里面也包含了多个 chunks ,当内存的
head block
写不下了会将数据存放在这个文件夹下面,并保留对文件的引用。 -
wal : 该文件夹里面存放的数据是当前正在写入的数据,里面包含多个数据段文件,一个文件默认最大 128M,Prometheus 会至少保留3个文件,对于高负载的机器会至少保留2小时的数据。wal 文件夹里面的数据是没有压缩过的,所以会比 block 里面的数据略大一些。
内存部分
- head block : v2.19之前,最近 2 小时的指标数据存储在内存中,v2.19 引入 head block,最近的指标数据存储在内存中,当内存存满时将数据刷入到磁盘中,并通过一个引用关联刷到磁盘的数据。
参考
https://prometheus.io/ https://ganeshvernekar.com/blog https://blog.csdn.net/m0_51450908/category_11542276.html https://cloud.tencent.com/developer/article/1983747 https://www.luozhiyun.com/archives/725 https://juejin.cn/post/7001273893780471845