Prometheus 存储结构浅析

Reference

不同开源项目的存储架构:

Question

  1. 数据存储结构是怎样的?
  2. block、series、chunk、样本数据(sample) 分别是什么,存储结构是怎样的?
  3. block、series、chunk 之间的关系是怎样的?
  4. 查询指标的过程是怎样的?
  5. 一个block包含多少series?
  6. 一个series包含多少chunk?
  7. 一个chunk 包含多少 series?
  8. series 如何确定唯一性?
  9. 查询过程如何体现在源码中?
  10. block 的 index 结构是怎样的?

定义

  • WAL:Write-Ahead-Log,预写日志,数据库系统中常见的一种手段,用于保证数据操作的原子性和持久性
  • 倒排索引:倒排索引是实现"单词-文档矩阵"的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两个部分组成:"单词词典"和"倒排文件" blog.csdn.net/qq_43403025...

存储结构

查看prometheus 存储路径下的文件结构;如果你是手动编译的prometheus,并且没有制定存储路径,则默认存储在当前可执行文件下的 data 目录中

bash 复制代码
tree .
.
├── 01JNZ7Q3HVHAJMXQK923KMFTFY
│   ├── chunks
│   │   └── 000001
│   ├── index
│   ├── meta.json
│   └── tombstones
├── chunks_head
│   ├── 000037
│   └── 000038
├── lock
├── queries.active
└── wal
    ├── 00000036
    ├── 00000037
    ├── 00000038
    └── checkpoint.00000035
        └── 00000000

可以看到,其文件结构大致分为两个层级,先对上述的层级有一个概念:

  • Block 块:
    • Block 块 ID:01JNZ7Q3HVHAJMXQK923KMFTFY(ULID[[分布式ID#ULID]])
    • Block 作用:存储历史时间序列数据,按时间(每两小时)分割的独立存储单元。
    • chunks :存放实际采样数据的块文件(如000001),每个块包含连续时间戳的数据,也就是一系列的 series
      • chunk 结构:
        • series ref:唯一标识时间线,由文件series ID 和偏移量组成,用于区分不同的 series和快速索引样本数据的位置
        • mintime/maxtime:记录该 chunk 的时间范围
        • data :存储压缩后的样本数据(如 [(t1, v1), (t2, v2), ...]
    • index:索引文件,记录标签到数据块的反向映射,支持快速查询
    • meta.json:元数据文件,包含块的时间范围、校验和等信息
    • tombstones:墓碑文件,标记已删除的时间序列,用于数据清理
  • Head Block(chunks_head):
    • 存储内存中尚未持久化的最新数据块,采用LRU策略定期刷盘
  • Lock:文件锁
  • WAL:
    • 预写日志,记录内存中待持久化的数据变更,防止进程崩溃导致数据丢失
    • 检查点(Checkpoint)​checkpoint.00000035文件,定期生成内存数据的快照,加速恢复
  • queries.active:
    • 活跃查询管理,记录当前正在执行的PromQL查询,用于资源管理和超时控制

总结一下:

  • prometheus使用分块存储,每两小时一个Block,目录名为block id(ULID,有序),Block 有多个文件,用于索引index、记录块信息 meta、存放实际的时序数据 chunks、WAL;
  • chunks 也是由多个chunk文件组成,每个文件默认为 512MB 大小,超过则切分;
  • WAL 则是写入chunk之前需要做的预写日志操作;
  • 其他的文件格式,在下面的查询流程中详细介绍

查询流程

定位 block

在大概认识了prometheus的存储结构后,下面从一个PromQL的例子讲解,Prometheus是如何查询指标数据的;

PromQL (http_requests{job=api-server,instance=0}) 时间范围 [start, end] 根据上面介绍的存储文件结构,可以知道,我们要查询这个PromQL 满足的所有数据,需要分层级查找,block => chunk = > series => sample,首先根据时间范围查询对应的block,block块的meta.json文件存储了每个块中数据的时间范围,遍历即可找到对应的block

然后,根据 block 的索引文件(index),判断数据存在哪些chunk中

index 文件结构

bash 复制代码
+------------+--------------+
| magic (4B) | version (1B) |
+------------+--------------+
|          Symbols          |
+---------------------------+
|           Series          |
+---------------------------+
|        LabelIndices       |
+---------------------------+
|          Postings         |
+---------------------------+
|     LabelIndicesTable     |
+---------------------------+
|       PostingsTable       |
+---------------------------+
|            TOC            |
+---------------------------+

`` 字段含义:

1、​Symbol Tables

符号表,用于优化标签存储和索引效率的核心结构;将 lable 的 key 和 value 等字符串按照字典序排序,然后映射为唯一的数字标识符ID(以Block块为单位)

结构:

  • len:符号总数
  • #symbols:符号数量
  • 后续为每个符号的len(长度)和str(字节数据),最后通过CRC32校验完整性

例如:jobprometheusnode-exporter 等字符串分配唯一 ID,job → 1,prometheus → 2,node-exporter → 3,然后查询和存储时,都使用ID来进行代替 具体作用:

  • 字符串符号化,减少存储冗余
  • 加速查询时的字符串匹配,不需要比较字符串,只需判断ID是否相等
  • 支持索引结构的紧凑存储,将长字符串转为ID,可以压缩存储体积

2、Series

记录每个时间序列的元数据,包括标签、时间范围(mint/maxt)及对应的chunk文件引用信息

结构:

  • len:序列总长度
  • labels count:标签对数量
  • 后续为每个标签对的ref(符号表索引),标签名和标签值通过符号表索引定位
  • chunks count:所属chunk文件数量
  • 每个chunk的元数据包括:
    • mint(起始时间戳)、maxt(结束时间戳)、chunk size
    • ref(chunk文件在磁盘中的偏移量)

3、LabelIndices

标签名到其唯一值的映射关系,支持按标签名快速查找所有可能的标签值;主要用于标签合法性校验

为方便理解,后面介绍的索引中存储的标签名和值的表现形式字符串,但实际都是存储的符号表的索引ID

结构

  • name:标签名
  • values:该标签名下所有可能的标签值列表 例子:
bash 复制代码
Label Key: "job"  
Label Values: ["prometheus", "node-exporter", "k8s"]  

4、Postings(倒排索引)

记录标签值组合到时间序列series的映射,支持高效查询特定标签组合下的所有时间序列

结构

  • name:标签名
  • value:标签值
  • offset:指向具体倒排列表的偏移量
bash 复制代码
Label Pair: "job=prometheus AND status=200"  
Postings List: [ref(series1), ref(series2)] 

​5、LabelIndicesTable & PostingsTable

  • LabelIndicesOffsetTable :将标签名映射到其在LabelIndices中的偏移量,加速标签名到索引的查找
  • PostingsOffsetTable :将(name, value)标签组合映射到其在Postings中的偏移量,支持快速定位倒排列表

6、TOC

目录表,存储索引文件各部分的偏移量,包括Symbol Table、Series、LabelIndices等,用于快速定位文件内容

结构:

  • 固定52字节,包含各部分的偏移量(如refSymbolsrefSeries等)

定位 chunk和series

在了解了 index 文件的结构后,我们对其每个作用有了大体的认识,下面继续回到如何查询数据上;我们已经找到了数据所在的block,然后通过读取其 index 文件来定位 chunk; 具体流程:

  1. 我们要找到 job =~ api-server.*的数据,会先访问TOC, 通过LabelOffsetTable 定位到 job 对应的值在 labelIndies 中的 offset
  2. 然后根据 labelIndies 找到所有满足的值, 根据这些值,到 PostingOffsetTable 中找到这些值对应在 Postings 中的 offset,从而找到所有的满足的 series ref id,取交集
  3. 遍历series ref id:
    1. 提取4字节Chunk ID,根据index文件中的series机构,定位到具体chunk文件
    2. 提取4字节偏移量,在chunk文件内定位样本数据
    3. 检查Chunk的minTime/maxTime是否在查询范围内
    4. 是的话就进行扫描读取样本
  4. 有有效样本数据返回

总结一下,按照步骤的输入和输出,基本可以分为几步:

概念 逻辑层级 唯一标识 数据范围 存储内容
Block 物理存储单元 ULID(Universally Unique Lexicographically Sortable Identifier) 固定时间段(默认2小时) 包含 chunks/(数据文件)、index(倒排索引)、meta.json(元数据)、tombstones(逻辑删除标记)
Series 逻辑时间序列单元 Metric名称 + 标签键值对集合(如 http_requests_total{method="GET", instance="localhost"} 所有时间(动态增长) 属于同一逻辑序列的所有样本数据(可能分散在多个 Block 的 Chunk 中)
Chunk 物理数据片段 Series 标识 + 时间范围(如 01G7Z74ZPB79Z5Z1D234567890_000001 固定时间段(默认2小时,可通过配置调整) 单个 Series 的连续时间样本数据(压缩格式,如Snappy)
Sample 样本数据 value 某个具体的值 根据值类型来定义范围 指标在这个时间点的具体值
相关推荐
杰瑞学AI1 小时前
Devops之GitOps:什么是Gitops,以及它有什么优势
运维·git·云原生·kubernetes·devops·argocd
夜寒花碎1 小时前
GO入门——Hello, World
后端·go
AKAMAI2 小时前
GitOps实战:使用Flux新建Kubernetes集群
后端·云原生·云计算
forever232 小时前
jaeger组件部署
go
王中阳Go3 小时前
15~30K,3年以上golang开发经验
后端·面试·go
KubeSphere 云原生3 小时前
云原生周刊:K8s 中的 GPU 共享
云原生·容器·kubernetes
探索云原生4 小时前
大模型微调实战:通过 LoRA 微调修改模型自我认知
ai·云原生·llm·sft
晓风残月淡4 小时前
Kubernetes详细教程(三):部署应用、了解常用命令及编写资源清单
云原生·容器·kubernetes
LeicyII6 小时前
面试题:Eureka和Nocas的区别
java·云原生·eureka