使用 Prometheus 也有两年了,一直很好奇它是怎么存储数据的:不仅能按时间范围查询,而且还能用各种 label 值进行过滤。于是研究了下,发现原理还是挺简单的。

在介绍数据存储格式时,首先我们要明确以下几个概念:

  • Metric: 也就是指标
  • LabelMetric 附带的标签,比如metric{service="gunicorn", protocol="https"}中的servieprotocol
  • Series:指的是metric name 以及其所有附带的 label-value 对。例如 metric{A="a"}metric{A="a", B="b"} 是两个不同的 Series
  • SampleSeries 在某个时间点的值

很显然,我们会按时间顺序写入Sample,并且会按时间和Label进行查询。最简单粗暴的方式是,每个Series都单独用一个文件来存储,新来的Sample往其对应的文件末尾追加。但是这种方式有几个问题:

  • 可能会用尽inode
  • 同时打开的文件描述符可能会过多
  • 会有频繁的文件写入
  • 查询时,各种标签的组合过滤不好实现,因为要跨文件
  • 数据过期不太好实现

Chunk

因此,为了减少inode及文件写入数量,需要把来自多个不同SeriesSamples进行聚合,最简单的方式是按照时间聚合,聚合的结果称之为Chunk tsdb/docs/format/chunks.md。也就是把多条Sample作为一个基本单位,进行读写。另外,Chunk 会经过压缩,以节约存储空间。

https://ganeshvernekar.com/blog/img/tsdb9.svg

新写入的数据会先写入HeadWALHead相当于一个内存缓冲区,当数据量累积到一定数目或超过一段时间后,会将数据写到磁盘文件中。由于局部性原理,最近新产生的Sample有更大的概率被访问,因此会通过M-map打开。当M-map区域的 Chunk 数量超出限制时,会将其移出去,生成真正持久化的版本:包括数据文件和索引文件。

Index

Chunk混入了多个不同Series的数据之后,那要怎么根据Label来查询呢?这时候我们就需要引入Index了。Index单独放在另一个文件中,目的就是加快根据Label的查询速度,包括等于、不等于、包含、正则、逻辑与或非等等复杂查询。

Index文件的结构如上图,包括了很多部分,但是核心是倒排索引。类似于新华字典,对于所有的 Label-Value pair都记录了其在数据文件中的位置,在查询时,就可以进行查表,找到对应的数据。

  • Symbol Table: 包括所有出现的字符串,去重后按序存储,目的是节省存储空间
  • Series: 存所有Series及所有包含它的Chunk的位置
  • Label Index: 存所有Label及其所有Value的值,目的是支持对Label进行正则匹配等非精确查询。
  • Label Offset Table: 存每个LabelLabel Index中的开始位置
  • Postings:就是最关键的倒排索引了,每个索引项都存储了其关联的Series
  • Posting Offset Table:存每个Label-Value pairPostings的开始位置

https://oscimg.oschina.net/oscnet/up-0b7a401836c5b8e549501f9ab6fc2161025.png

https://oscimg.oschina.net/oscnet/up-ede75bbb021a5450c9d1a18bb8f7bdf0202.png

查询时,会首先把Index中的内容加载到内存中,但并不会把所有的Posting都加进去,为了节省内存,会按顺序隔几个加载一次。然后会用二分查找来查询LabelPosting Offset Table中最近的位置,然后再查表,找到精确位置。大概就是这么个数据结构

posting map[string][]postingOffset

type postingOffset struct {
    value string
    offset int
}

比如我们要查询metric{A="a", B="b"},那么根据Posting Offset TablePostings,我们查出:A=a 出现在Series [1, 3, 5, 6, 8, 9, 10] 中,B=b 出现在Series [3, 4, 5, 11, 23] 中。二者求交集, 得出{A="a", B="b"} 出现在Series [3, 5] 中。而Series记录了其对应的Chunk在数据文件中的位置以及时间范围,那么就可以确定某个具体的Sample在数据文件的哪里了。

(特别的metric也作为特殊的Label进行存储记为__name__="metric"

File Structure

有了上述知识背景后,就能看明白 TSDB 的文件结构了:

  • chunks_head: 通过 mmap 打开的数据文件,没有关联的索引文件
  • 01GHQEVXYVJ9EQ4Z0JWS7EY545: 类似这样命名的文件称之为Block,相当于一个小型的TSDB,存放了一段时间的数据,多个 Block 可以合并为一个。其中包含了数据文件、索引文件、元信息等。
  • wal: 存放 WAL 和 checkpoint 等信息
storage_v2/
├── 01GHQEVXYVJ9EQ4Z0JWS7EY545
│   ├── chunks
│   │   └── 000001
│   ├── index
│   ├── meta.json
│   └── tombstones
├── 01GHQEVZ468HWPJ9QMD065FP61
│   ├── chunks
│   │   └── 000001
│   ├── index
│   ├── meta.json
│   └── tombstones
├── chunks_head
│   ├── 000106
│   └── 000107
├── lock
├── queries.active
└── wal
    ├── 00000104
    ├── 00000105
    ├── 00000106
    ├── 00000107
    └── checkpoint.00000103
        └── 00000000

meta.json 文件内容长这样,可以看出这个 Block 是由 3个 Block合并而来

/prometheus/storage_v2/01GHQEVZ468HWPJ9QMD065FP61 # cat meta.json 
{
        "ulid": "01GHQEVZ468HWPJ9QMD065FP61",
        "minTime": 1668276000000,
        "maxTime": 1668297600000,
        "stats": {
                "numSamples": 27940320,
                "numSeries": 19403,
                "numChunks": 232836
        },
        "compaction": {
                "level": 2,
                "sources": [
                        "01GHPT8R6VZVXMAX84BBAYNH1M",
                        "01GHQ14FET0WY8Z44TFYY569QR",
                        "01GHQ806PVY5AATJ4K4209HJFB"
                ],
                "parents": [
                        {
                                "ulid": "01GHPT8R6VZVXMAX84BBAYNH1M",
                                "minTime": 1668276000000,
                                "maxTime": 1668283200000
                        },
                        {
                                "ulid": "01GHQ14FET0WY8Z44TFYY569QR",
                                "minTime": 1668283200000,
                                "maxTime": 1668290400000
                        },
                        {
                                "ulid": "01GHQ806PVY5AATJ4K4209HJFB",
                                "minTime": 1668290400000,
                                "maxTime": 1668297600000
                        }
                ]
        },
        "version": 1
}/prometheus/storage_v2/01GHQEVZ468HWPJ9QMD065FP61 #

(完)

References: