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 某个具体的值 根据值类型来定义范围 指标在这个时间点的具体值
相关推荐
木雷坞3 小时前
K8s GPU 推理服务 ImagePullBackOff 排查与预热
云原生·容器·kubernetes·gpu算力
审判长烧鸡4 小时前
GO时区【1】定义与使用
go·时区
审判长烧鸡5 小时前
GO错误处理【5】显式错误处理
go·错误处理·报错链条
jeff聊企业数字化5 小时前
私有化即时通讯选型指南:兼顾安全与高效
go·业界资讯·即时通讯
AOwhisky6 小时前
Kubernetes调度与服务暴露:从“定时任务”到“服务发现”的完全指南
linux·运维·云原生·容器·kubernetes·服务发现
审判长烧鸡6 小时前
GO错误处理【6】显式哲学
go·显式哲学
Cyber4K6 小时前
【Kubernetes专项】温故而知新,重温技术原理(6)
云原生·容器·kubernetes
审判长烧鸡7 小时前
GO错误处理【3】返回err与日志的结合
go·架构设计·报错处理
运维老郭8 小时前
K8s故障排查:一条分层排查路径解决99%线上问题
运维·云原生·kubernetes
AI攻城狮8 小时前
谷歌花400亿投了"对手":这不是矛盾,这是最高明的战略对冲
云原生