存储原理:
prometheus按照block块的方式来存储数据,每2小时为一个时间单位,首先会存储到内存中,当到达2小时后,会自动写入磁盘中。为防止程序异常而导致数据丢失,采用了WAL机制,即2小时内记录的数据存储在内存中的同时,还会记录一份日志,存储在block下的wal目录中。当程序再次启动时,会将wal目录中的数据写入对应的block中,从而达到恢复数据的效果。
当删除数据时,删除条目会记录在tombstones 中,而不是立刻删除。
每个block都是一个独立的数据库:
prometheus采用的存储方式称为“时间分片”,每个block都是一个独立的数据库。优势是可以提高查询效率,查哪个时间段的数据,只需要打开对应的block即可,无需打开多余数据。
example:
./data
├── 01BKGV7JBM69T2G1BGBGM6KB12
│ └── meta.json
├── 01BKGTZQ1SYQJTR4PB43C8PD98
│ ├── chunks
│ │ └── 000001
│ ├── tombstones
│ ├── index
│ └── meta.json
├── chunks_head
│ └── 000001
└── wal # 预写日志
├── 000000002
└── checkpoint.00000001
└── 00000000
01BKGV7JBM69T2G1BGBGM6KB12:block ID,这类命名的目录是一个完整的block
meta.json:是这个block的元信息
chunks:目录下存储每一个Block中的所有的Chunk,目录下每个文件都是一个chunk数据单元
Index:文件是该Chunk的索引文件
tombstones:数据删除记录文件,记录的是删除信息
wal:保存了内存里最近2小时的数据,用于重启后恢复最近两小时里内存的数据
chunks_head:磁盘内存映射头块
checkpoint:checkpoint机制会将wal 清理过后的数据做过滤写成新的段,
然后checkpoint文件被命名为创建 checkpoint的最后一个段号checkpoint.X
meta.json:
// example
{
"ulid": "01EM6Q6A1YPX4G9TEB20J22B2R",
"minTime": 1602237600000,
"maxTime": 1602244800000,
"stats": {
"numSamples": 553673232,
"numSeries": 1346066,
"numChunks": 4440437
},
"compaction": {
"level": 1,
"sources": [
"01EM65SHSX4VARXBBHBF0M0FDS",
"01EM6GAJSYWSQQRDY782EA5ZPN"
]
},
"version": 1
}
// 内容解析
version:告诉我们如何解析元文件。
minTime,maxTime:是块中存在的所有块中的绝对最小和最大时间戳。
stats:告诉块中存在的Series、Samples和Chunks的数量。
compaction:讲述区块的历史。
* level:告诉这个块已经到了多少代。
* sources:告诉这个块是从哪些块创建的(即合并形成这个块的块)。如果它是从 Head 块创建的,
则sources设置为自身(01EM6Q6A1YPX4G9TEB20J22B2R在这种情况下)。
chunks_head文件夹中的chunks:文件的最大大小保持在 128MiB
// example
┌──────────────────────────────┐
│ magic(0x0130BC91) <4 byte> │
├──────────────────────────────┤
│ version(1) <1 byte> │
├──────────────────────────────┤
│ padding(0) <3 byte> │
├──────────────────────────────┤
│ ┌──────────────────────────┐ │
│ │ Chunk 1 │ │
│ ├──────────────────────────┤ │
│ │ ... │ │
│ ├──────────────────────────┤ │
│ │ Chunk N │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘
// 内容解析
magic:将此文件标识为块文件
version:告诉我们如何解析这个文件
padding:适用于任何未来的标题
Chunk 1 - Chunk N:是块列表。
// 单个块的格式:
┌─────────────────────┬───────────────────────┬───────────────────────┬───────────────────┬───────────────┬──────────────┬────────────────┐
| series ref <8 byte> | mint <8 byte, uint64> | maxt <8 byte, uint64> | encoding <1 byte> | len | data │ CRC32 <4 byte> │
└─────────────────────┴───────────────────────┴───────────────────────┴───────────────────┴───────────────┴──────────────┴────────────────┘
series ref:它是用于访问内存中series的series ID
mint和maxt:块的样本中看到的最小和最大时间戳
encoding:是用于压缩块的编码
len:是从这里开始的字节数data,是压缩块的实际字节数。
CRC32:是上述chunk内容的校验和,用于校验数据的完整性。
// 块如何被读取
series ref 为 8 个字节。前4个字节告诉文件块所在的文件号,最后4个字节告诉文件中块开始的偏移量
如果块在文件中00093并且series ref从文件中的字节偏移开始1234:
那么该块的引用将是(93 << 32) | 1234(左移位,然后按位或)。
block文件夹中的chunks:该chunks目录包含一系列编号的文件,每个文件的上限为 512MiB。接下来分析此目录中单个文件的格式
单个 chunk 的时间跨度默认是 2 小时,Prometheus 后台会有合并操作,把时间相邻的 block 合到一起
// example
┌──────────────────────────────┐
│ magic(0x85BD40DD) <4 byte> │
├──────────────────────────────┤
│ version(1) <1 byte> │
├──────────────────────────────┤
│ padding(0) <3 byte> │
├──────────────────────────────┤
│ ┌──────────────────────────┐ │
│ │ Chunk 1 │ │
│ ├──────────────────────────┤ │
│ │ ... │ │
│ ├──────────────────────────┤ │
│ │ Chunk N │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘
// 内容解析
magic:将此文件标识为块文件
version:告诉我们如何解析这个文件
padding:适用于任何未来的标题
Chunk 1 - Chunk N:是块列表。
// 单个块的格式:
┌───────────────┬───────────────────┬──────────────┬────────────────┐
│ len │ encoding <1 byte> │ data │ CRC32 <4 byte> │
└───────────────┴───────────────────┴──────────────┴────────────────┘
// 作用:这里面存的是时序数据,文件中的块由 uint64 从index文件中引用,
// uint64 由文件内偏移量(低 4 个字节)和段序列号(高 4 个字节)组成
// 即:index中的数据条目有一个64bit的引用记录,其中四个字节存数据在哪个文件(段文件序列号),
// 另外四个字节存文件内偏移量,这样就能找到每个记录对应的chunk数据在哪个文件的哪个位置
// 这里的chunk和上面的head chuank相比,少了series ref, mint和maxt,为什么不需要呢
// 因为series ref, mint和maxt的信息在index文件里面有,我们就是根据index文件里的
// series ref, mint和maxt来查找chunk里的数据的,因此这里不需要存
// 原因解析:
// https://ganeshvernekar.com/blog/prometheus-tsdb-persistent-block-and-its-index/
index:
┌────────────────────────────┬─────────────────────┐
│ 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:告诉我们如何解析这个文件
TOC:该索引的入口点,它代表索引目录。
// TOC
┌─────────────────────────────────────────┐
│ ref(symbols) <8b> │ -> Symbol Table
├─────────────────────────────────────────┤
│ ref(series) <8b> │ -> Series
├─────────────────────────────────────────┤
│ ref(label indices start) <8b> │ -> Label Index 1
├─────────────────────────────────────────┤
│ ref(label offset table) <8b> │ -> Label Offset Table
├─────────────────────────────────────────┤
│ ref(postings start) <8b> │ -> Postings 1
├─────────────────────────────────────────┤
│ ref(postings offset table) <8b> │ -> Postings Offset Table
├─────────────────────────────────────────┤
│ CRC32 <4b> │
└─────────────────────────────────────────┘
// 作用解析
它告诉我们索引的各个组成部分到底从哪里开始(文件中的字节偏移量)。
我已经在上面的索引格式中标记了每个参考指向的内容。
下一个组件的起点也告诉我们各个组件的终点在哪里。
如果任何参考文献是0,则表明索引中不存在相应的部分,因此应在阅读时跳过。
由于TOC是固定大小,因此文件的最后 52 个字节可以作为TOC.
正如您将在接下来的部分中注意到的那样,每个组件都有自己的校验和,即CRC32检查底层数据的完整性。
// Symbol Table
// 此部分包含已删除重复字符串的排序列表,这些字符串可在此块中所有系列的标签对中找到。
// 例如,如果系列是{a="y", x="b"},那么符号就是"a", "b", "x", "y"
┌────────────────────┬─────────────────────┐
│ len <4b> │ #symbols <4b> │
├────────────────────┴─────────────────────┤
│ ┌──────────────────────┬───────────────┐ │
│ │ len(str_1) │ str_1 │ │
│ ├──────────────────────┴───────────────┤ │
│ │ . . . │ │
│ ├──────────────────────┬───────────────┤ │
│ │ len(str_n) │ str_n │ │
│ └──────────────────────┴───────────────┘ │
├──────────────────────────────────────────┤
│ CRC32 <4b> │
└──────────────────────────────────────────┘
len:这部分的占用字节数
symbols:这部分存储的符号数
len(str_n) │ str_n :是一个符号的长度和内容
作用:
索引中的其他部分可以为任何字符串引用此符号表,从而显着减小索引大小。
符号在文件中开始的字节偏移量(即 的开头len(str_i))形成了相应符号的引用,
该符号可以在其他地方使用,而不是实际的字符串。
当您需要实际字符串时,可以使用偏移量从该表中获取它。
// Series
┌───────────────────────────────────────┐
│ ┌───────────────────────────────────┐ │
│ │ series_1 │ │
│ ├───────────────────────────────────┤ │
│ │ . . . │ │
│ ├───────────────────────────────────┤ │
│ │ series_n │ │
│ └───────────────────────────────────┘ │
└───────────────────────────────────────┘
每个系列条目都是 16 字节对齐的,这意味着系列开始的字节偏移量可以被 16 整除。
因此,我们将系列的 ID 设置为offset/16偏移量指向系列条目开始的位置。
此 ID 用于引用该系列,并且每当您想要访问该系列时,您都可以通过执行 获取索引中的位置ID*16。
每个series条目都包含系列的标签集和对属于该系列的所有块的引用:
┌──────────────────────────────────────────────────────┐
│ len │
├──────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────┐ │
│ │ labels count │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ ref(l_i.name) │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ ref(l_i.value) │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ ... │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ chunks count │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ c_0.mint │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ c_0.maxt - c_0.mint │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ ref(c_0.data) │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ c_i.mint - c_i-1.maxt │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ c_i.maxt - c_i.mint │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ ref(c_i.data) - ref(c_i-1.data) │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ ... │ │
│ └──────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────┤
│ CRC32 <4b> │
└──────────────────────────────────────────────────────┘
含义解析:
labels count:这个series数据里有多少个label对
ref(l_i.name) 和ref(l_i.value) :我们不存储实际的字符串本身,而是使用符号表中的符号引用,
利用这个引用去查符号表就可以
chunks count:这个series对应的时序数据由多少个chunk块来存放
mint,maxt,ref:这三个就是前面说的chunk相比head chunk少的三个数据就是存放在这里,
在查询series的时候,会根据index中这个series的chunk列表中每个chunk的
mint,maxt,ref,然后到chunk文件去查。这里ref是八个字节,里面四个字节
记录了数据在哪个chunk文件,四个字节记录了文件在那个chunk文件里的偏移量
// 在索引中保存mintandmaxt允许查询跳过查询时间范围不需要的块
// 上面在记录mint,maxt的时候,你可以看到除了第一个mint记录的是完整的时间戳,
// 后面的其他mint,maxt记录的全是相对上一个数据的时间增量,以节省记录的空间
// 即第一个mint是varint,后面的全是uvarint,因为增量肯定是正数,使用uvarint,
// 可以节省很多前缀0
// Label Offset Table和Label Index i
这两个不再使用了;它们是为向后兼容而编写的,但不会从最新的 Prometheus 版本中读取
// Postings Offset Table和Postings i
// Postings 1- N存储了Postings列表,Postings Offset Table记录这些条目的偏移量。
// Postings是一个series ID,在index文件的上下文中,它是series条目在文件中开始的偏移量除16,
// 因为它是 16 字节对齐的。
一个Postings的结构
┌────────────────────┬────────────────────┐
│ len <4b> │ #entries <4b> │
├────────────────────┴────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ ref(series_1) <4b> │ │
│ ├─────────────────────────────────────┤ │
│ │ ... │ │
│ ├─────────────────────────────────────┤ │
│ │ ref(series_n) <4b> │ │
│ └─────────────────────────────────────┘ │
├─────────────────────────────────────────┤
│ CRC32 <4b> │
└─────────────────────────────────────────┘
entries是下面series列表的数量
ref(series_1) 是series ID,也是series ref,也就是引用
// 具体Postings如何与Postings Offset Table一起配合记录,
// 看下面讲完Postings Offset Table后的实例
// Postings Offset Table
┌─────────────────────┬──────────────────────┐
│ len <4b> │ #entries <4b> │
├─────────────────────┴──────────────────────┤
│ ┌────────────────────────────────────────┐ │
│ │ n = 2 <1b> │ │
│ ├──────────────────────┬─────────────────┤ │
│ │ len(name) │ name │ │
│ ├──────────────────────┼─────────────────┤ │
│ │ len(value) │ value │ │
│ ├──────────────────────┴─────────────────┤ │
│ │ offset │ │
│ └────────────────────────────────────────┘ │
│ . . . │
├────────────────────────────────────────────┤
│ CRC32 <4b> │
└────────────────────────────────────────────┘
// Postings Offset Table和Postings联合作用
// 举例:
series: {a="b", x="y1"} with series ID 120, {a="b", x="y2"} with series ID 145
Postings list
a="b" [120,145]
x="y1" [120]
x="y2" [145]
Postings Offset Table:
a="b" offset 1
x="y1" offset 2
x="y2" offset 3
查询series: {a="b", x="y1"} 时,查Postings Offset Table,得到a="b", x="y1"分别在
Postings list中的第一和第二,拿出Postings list中第一和第二这两个label-pair对应的series,
然后求合集,就得到这两个label-pair的公共series,也就是哪些series里同时有这两个标签
// Postings Offset Table作用
这里存储了很多的label-pair对的名字和值,以及offset,也就是他们的posting位置,
有了这些就能知道一个label-pari对在哪个posting中,然后posting中记录了label-pari对
对应的series的内容,然后series里面的ref又能找到chunk里面的时序数据,这样就对应起来了