Prometheus时序数据库-磁盘中的存储结构 前言
之前的文章里,笔者详细描述了监控数据在Prometheus内存中的结构。而其在磁盘中的存储结构,也是非常有意思的,关于这部分内容,将在本篇文章进行阐述。
磁盘目录结构首先我们来看Prometheus运行后,所形成的文件目录结构
在笔者自己的机器上的具体结构如下: prometheus-data |-01EY0EH5JA3ABCB0PXHAPP999D (block) |-01EY0EH5JA3QCQB0PXHAPP999D (block) |-chunks |-000001 |-000002 ..... |-000021 |-index |-meta.json |-tombstones |-wal |-chunks_head Block
一个Block就是一个独立的小型数据库,其保存了一段时间内所有查询所用到的信息。包括标签/索引/符号表数据等等。Block的实质就是将一段时间里的内存数据组织成文件形式保存下来。
最近的Block一般是存储了2小时的数据,而较为久远的Block则会通过compactor进行合并,一个Block可能存储了若干小时的信息。值得注意的是,合并操作只是减少了索引的大小(尤其是符号表的合并),而本身数据(chunks)的大小并没有任何改变。 meta.json
我们可以通过检查meta.json来得到当前Block的一些元信息。
{ "ulid":"01EY0EH5JA3QCQB0PXHAPP999D" // maxTime-minTime = 7200s => 2 h "minTime": 1611664000000 "maxTime": 1611671200000 "stats": { "numSamples": 1505855631, "numSeries": 12063563, "numChunks": 12063563 } "compaction":{ "level" : 1 "sources: [ "01EY0EH5JA3QCQB0PXHAPP999D" ] } "version":1 }其中的元信息非常清楚明了。这个Block记录了从2个小时的数据。
让我们再找一个比较陈旧的Block看下它的meta.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中。如下图所示:
所有的Chunk文件在磁盘上都不会大于512M,对应的源码为:
func (w *Writer) WriteChunks(chks ...Meta) error { ...... for i, chk := range chks { cutNewBatch := (i != 0) && (batchSize+SegmentHeaderSize > w.segmentSize) ...... if cutNewBatch { ...... } ...... } }当写入磁盘单个文件超过512M的时候,就会自动切分一个新的文件。
一个Chunks文件包含了非常多的内存Chunk结构,如下图所示:
图中也标出了,我们是怎么寻找对应Chunk的。通过将文件名(000001,前32位)以及(offset,后32位)编码到一个int类型的refId中,使得我们可以轻松的通过这个id获取到对应的chunk数据。 chunks文件通过mmap去访问
由于chunks文件大小基本固定(最大512M),所以我们很容易的可以通过mmap去访问对应的数据。直接将对应文件的读操作交给操作系统,既省心又省力。对应代码为:
func NewDirReader(dir string, pool chunkenc.Pool) (*Reader, error) { ...... for _, fn := range files { f, err := fileutil.OpenMmapFile(fn) ...... } ...... bs = append(bs, realByteSlice(f.Bytes())) } 通过sgmBytes := s.bs[offset]就直接能获取对应的数据 index索引结构前面介绍完chunk文件,我们就可以开始阐述最复杂的索引结构了。
寻址过程索引就是为了让我们快速的找到想要的内容,为了便于理解。笔者就通过一次数据的寻址来探究Prometheus的磁盘索引结构。考虑查询一个
拥有系列三个标签 ({__name__:http_requests}{job:api-server}{instance:0}) 且时间为start/end的所有序列数据我们先从选择Block开始,遍历所有Block的meta.json,找到具体的Block
前文说了,通过Labels找数据是通过倒排索引。我们的倒排索引是保存在index文件里面的。 那么怎么在这个单一文件里找到倒排索引的位置呢?这就引入了TOC(Table Of Content) TOC(Table Of Content)