Prometheus 的监控数据以指标(metric)的形式保存在内置的时间序列数据库(TSDB)当中。
1、指标名称和标签(metric names, labels)
每一条时间序列由指标名称(Metrics Name)以及一组标签labels(键值KV对)唯一标识。
TIPS: 改变标签中的K或者V值(包括添加或删除),都会创建新的时间序列。
2、样本Samples
在时间序列中的每一个点称为一个样本(sample),样本由以下三部分组成:
- 指标(metric):指标名称Metric names和标签labels;
- 时间戳(timestamp):一个精确到毫秒的时间戳;
- 样本值(value): 一个 folat64 的浮点型数据表示当前样本的值。
如下就是三个样本数据:
<--------------------------- metric ------------------------><-----timestamp-----><–value–>
<—metric name–><--------------labels--------------><------timestamp------><–value–>
http_request_total{status=“200”, method=“GET”}@1434417560938 => 94355
http_request_total{status=“200”, method=“GET”}@1434417561287 => 94334
http_request_total{status=“200”, method=“POST”}@1434417561287 => 93656
当我们根据时间顺序,把样本数据统一放在一起,就可以形成一条监控曲线,如下:
3、Series
这样的特定的metric、timestamp、value构成的时间序列(TimeSeries) 在Prometheus中被称作Series。
4、总结:
Metric names 指标名称
labels 指标标签,与指标名称一同构成唯一标识项
Samples 一个时间点的 Metric names,labels,样本值(value),
Series 由很多相关的samples组成的时间序列
时间序列 (Time Series) 指的是某个指标随时间变化的所有历史数据,而样本 (Sample) 指的是历史数据中该变量的瞬时值。
当我们有很多的Series,就可以用下面的示意图表示了,图上每一个点都是一个Sample。
存储机制:
Prometheus将最近的数据保存在内存中,这样查询最近的数据会变得非常快,然后通过一个compactor定时将数据打包到磁盘。数据在内存中最少保留2个小时(storage.tsdb.min-block-duration。为了防止程序崩溃导致数据丢失,实现了WAL(write-ahead-log)机制,启动时会以写入日志(WAL)的方式来实现重播,从而恢复数据。
Block:
一个Block就是一个独立的小型数据库,其保存了一段时间内所有查询所用到的信息。包括标签/索引/符号表数据等等。Block的实质就是将一段时间里的内存数据组织成文件形式保存下来。
最近的Block一般是存储了2小时的数据,而较为久远的Block则会通过compactor进行合并,一个Block可能存储了若干小时的信息。值得注意的是,合并操作只是减少了索引的大小(尤其是符号表的合并),而本身数据(chunks)的大小并没有任何改变。
Block还可以理解,每两个小时数据切一个块,就是一个Block,chunk这个概念是哪里来的?
在Prometheus内部,chunk是用来存储压缩后的样本sample数据的最小单位。chunk的大小1KB,每个chunk可以存储最多120个样本,当chunk存满120个样本或超过了chunkRange的时间范围(默认情况为 2h),就会写到新的chunk里。
每一个Series的样本数据,由多个chunk来进行存储,按照上面所说的存满120个或者超过chunkRange时间,就会写到新的里面,chunk存储的是经过压缩后的sample数据
每个 block 实际上就是一个小型数据库,内部存储着该时间窗口内的所有时序数据,因此它需要拥有自己的 index 和 chunks。除了最新的、正在接收新鲜数据的 block 之外,其它 blocks 都是不可变的。由于新数据的写入都在内存中,数据的写效率较高:
为了防止数据丢失,所有新采集的数据都会被写入到 WAL 日志中,在系统恢复时能快速地将其中的数据恢复到内存中。在查询时,我们需要将查询发送到不同的 block 中,再将结果聚合。
按时间将数据分片赋予了存储引擎新的能力:
- 当查询某个时间范围内的数据,我们可以直接忽略在时间范围外的 blocks
- 写完一个 block 后,我们可以将轻易地其持久化到磁盘中,因为只涉及到少量几个文件的写入
- 新的数据,也是最常被查询的数据会处在内存中,提高查询效率 (第二代同样支持)
- 每个 chunk 不再是固定的 1KB 大小,我们可以选择任意合适的大小,选择合适的压缩方式
- 删除超过留存时间的数据变得异常简单,直接删除整个文件夹即可
将所有时序数据按时间切割成许多 blocks,当新写满的 block 持久化到磁盘后,相应的 WAL 文件也会被清除。写入数据时,我们希望每个 block 不要太大,比如 2 小时左右,来避免在内存中积累过多的数据。读取数据时,若查询涉及到多个时间段,就需要对许多个 block 分别执行查询,然后再合并结果。假如需要查询一周的数据,那么这个查询将涉及到 80 多个 blocks,降低数据读取的效率。
找一个block的mata.json文件:
"ulid":"01EXTEH5JA3QCQB0PXHAPP999D",
// maxTime - maxTime =>162h
"minTime":1610964800000,
"maxTime":1611548000000
......
"compaction":{
"level": 5,
"sources: [
31个01EX......
]
},
"parents: [
{
"ulid": 01EXTEH5JA3QCQB1PXHAPP999D
...
}
{
"ulid": 01EXTEH6JA3QCQB1PXHAPP999D
...
}
{
"ulid": 01EXTEH5JA31CQB1PXHAPP999D
...
}
]
从中我们可以看到,该Block是由31个原始Block经历5次压缩而来。最后一次压缩的三个Block ulid记录在parents中。如下图所示:
为了既能写得快,又能读得快,我们就得引入 compaction,后者将一个或多个 blocks 中的数据合并成一个更大的 block,在合并的过程中会自动丢弃被删除的数据、合并多个版本的数据、重新结构化 chunks 来优化查询效率
当数据超过留存时间时,删除旧数据非常容易:
直接删除在边界之外的 block 文件夹即可。如果边界在某个 block 之内,则暂时将它留存,知道边界超出为止。当然,在 Compaction 中,我们会将旧的 blocks 合并成更大的 block;在 Retention 时,我们又希望能够粒度更小。所以 Compaction 与 Retention 的策略之间存在着一定的互斥关系。Prometheus 的系统参数可以对单个 block 的大小作出限制,来寻找二者之间的平衡。
当 Head 中的数据 spanchunkRange*3/2时,也就是存了3h后,第一个chunkRange数据(此处为 2h)被压缩成一个持久块,此时 WAL 被截断并创建了一个检查点。
Head block:
上面介绍的 mmap 中的 chunks 保存在名为 chunks_head 的目录下, 文件序列与 WAL 中的相似. 如下图:
文件格式:
┌──────────────────────────────┐
│ magic(0x0130BC91) <4 byte> │
├──────────────────────────────┤
│ version(1) <1 byte> │
├──────────────────────────────┤
│ padding(0) <3 byte> │
├──────────────────────────────┤
│ ┌──────────────────────────┐ │
│ │ Chunk 1 │ │
│ ├──────────────────────────┤ │
│ │ ... │ │
│ ├──────────────────────────┤ │
│ │ Chunk N │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘
magic number:是可以唯一标识一个文件时 head_chunk 的数字
version:告诉我们如何解码文件中的 chunks
padding:是为了将来可能需要的选项而预留出来的
一个 chunk 的格式如下所示:
┌─────────────────────┬───────────────────────┬───────────────────────┬───────────────────┬───────────────┬──────────────┬────────────────┐
| series ref <8 byte> | mint <8 byte, uint64> | maxt <8 byte, uint64> | encoding <1 byte> | len | data │ CRC32 <4 byte> │
└─────────────────────┴───────────────────────┴───────────────────────┴───────────────────┴───────────────┴──────────────┴────────────────┘
series ref 是用于访问内存中的序列的 id, 即上文中 mmap 中的引用
mint 是该 chunk 中 series 的最小时间戳
max 是该 chunk 中 series 的最大时间戳
encoding 是压缩该 chunk 时使用的编码
len 是此后的字节数
data 是压缩后的数据
CRC32 是用于检查数据完整性的校验和
Head Block 通过 series ref, 以及 mint、maxt 就可以实现不访问磁盘就选择 chunk.
其中, ref 是 8 bytes, 前 4 个字节告诉了 chunk 存在哪个文件中(file number),
后四个字节告诉了 chunk 在该文件中的偏移量.
persistent block:
文件格式:见这里的具体分析
【博客478】prometheus-----存储目录结构以及格式,作用分析
此处只讲head Block 与persistent block的chunk文件格式区别:
与 head_chunk 差不多, 区别是少了 series ref、mint、maxt。
在 Head_chunk 中需要这些附加信息是为了 prometheus 重启时能够创建内存索引,
但是在持久化 block 中, 这些信息在 index 文件中存储了, 因此不再需要.
同样通过 reference 来访问这些 chunk.
┌──────────────────────────────┐
│ magic(0x85BD40DD) <4 byte> │
├──────────────────────────────┤
│ version(1) <1 byte> │
├──────────────────────────────┤
│ padding(0) <3 byte> │
├──────────────────────────────┤
│ ┌──────────────────────────┐ │
│ │ Chunk 1 │ │
│ ├──────────────────────────┤ │
│ │ ... │ │
│ ├──────────────────────────┤ │
│ │ Chunk N │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘
┌───────────────┬───────────────────┬──────────────┬────────────────┐
│ len │ encoding <1 byte> │ data │ CRC32 <4 byte> │
└───────────────┴───────────────────┴──────────────┴────────────────┘
在现在的 TSDB 存储方案中,TSDB 的数据被根据时间段分割成一个个目录,每个目录内部都放着完整的一段时间的数据,每个目录的数据时间是不重叠的。
合并规则:
好处:
这样的好处就是存取,删除都很方便。如果要查询数据,那么可以根据查询的时间按需读取部分数据,而不用每次都读取全部目录的数据;并且,如果要删除多久以前的数据,那么直接删除对应时间段的目录就可以了,而不需要删除的目录根本不需要改变,就是这么方便。
不足:
这里有一个隐含的缺陷,那就是这里说了 50 小时的目录之后就不压缩了,如果你存放的数据太久的话,迟早也是 too many open file 的,Prometheus 官方对此的解释是,你不要用我原生的存储存放太久的数据(默认的存放周期是 15 天,超过 15 天的数据会被定期清除)。如果你想存放长期的数据,那么请使用通过 Remote Storage 使用第三方存储软件来存放。