前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Prometheus TSDB存储原理

Prometheus TSDB存储原理

作者头像
你大哥
发布2022-04-18 14:34:15
1.7K0
发布2022-04-18 14:34:15
举报
文章被收录于专栏:容器云实践

Prometheus 包含一个存储在本地磁盘的时间序列数据库,同时也支持与远程存储系统集成,比如 grafana cloud 提供的免费云存储API,只需将 remote_write接口信息填写在Prometheus配置文件即可。

本文不涉及远程存储接口内容,主要介绍Prometheus 时序数据的本地存储实现原理。

什么是时序数据?

在学习Prometheus TSDB存储原理之前,我们先来认识一下Prometheus TSDB、InfluxDB这类时序数据库的时序数据指的是什么?

时序数据通常以(key,value)的形式出现,在时间序列采集点上所对应值的集,即每个数据点都是一个由时间戳和值组成的元组。

代码语言:javascript
复制
identifier->(t0,v0),(t1,v1),(t2,v2)...

Prometheus TSDB的数据模型

代码语言:javascript
复制
<metric name>{<label name>=<label value>, ...}

具体到某个实例中

代码语言:javascript
复制
requests_total{method="POST", handler="/messages"}

在存储时可以通过name label来标记 metric name,再通过标识符@来标识时间,这样构成了一个完整的时序数据样本。

代码语言:javascript
复制
----------------------------------------key-----------------------------------------------value---------
{__name__="requests_total",method="POST", handler="/messages"}   @1649483597.197             52

一个时间序列是一组时间上严格单调递增的数据点序列,它可以通过metric来寻址。抽象成二维平面来看,二维平面的横轴代表单调递增的时间, metrics遍及整个纵轴。在提取样本数据时只要给定时间窗口和metric就可以得到value

时序数据如何在Prometheus TSDB存储?

上面我们简单了解了时序数据,接下来我们展开Prometheus TSDB存储(V3引擎)

Prometheus TSDB 概览

在上图中,Head 块是TSDB的内存块,灰色块Block是磁盘上的持久块。

首先传入的样本(t,v)进入 Head 块,为了防止内存数据丢失先做一次预写日志 (WAL),并在内存中停留一段时间,然后刷新到磁盘并进行内存映射(M-map)。当这些内存映射的块或内存中的块老化到某个时间点时,会作为持久块Block存储到磁盘。接下来多个Block在它们变旧时被合并,并在超过保留期限后被清理。

Head中样本的生命周期

当一个样本传入时,它会被加载到Head中的active chunk(红色块),这是唯一一个可以主动写入数据的单元,为了防止内存数据丢失还会做一次预写日志 (WAL)

一旦active chunk被填满时(超过2小时或120样本),将旧的数据截断为head_chunk1。

head_chunk1被刷新到磁盘然后进行内存映射。active chunk继续写入数据、截断数据、写入到内存映射,如此反复。

内存映射应该只加载最新的、最被频繁使用的数据,所以Prometheus TSDB将就是旧数据刷新到磁盘持久化存储Block,如上1-4为旧数据被写入到下图的Block中。

此时我们再来看一下Prometheus TSDB 数据目录基本结构,好像更清晰了一些。

代码语言:javascript
复制
./data
├── 01BKGV7JBM69T2G1BGBGM6KB12    
│   └── meta.json
├── 01BKGTZQ1SYQJTR4PB43C8PD98   # block ID
│   ├── chunks         # Block中的chunk文件
│   │   └── 000001     
│   ├── tombstones     # 数据删除记录文件
│   ├── index          # 索引
│   └── meta.json       # bolck元信息
├── chunks_head           # head内存映射
│   └── 000001          
└── wal                   # 预写日志
    ├── 000000002      
    └── checkpoint.00000001
        └── 00000000
WAL 中checkpoint的作用

我们需要定期删除旧的 wal 数据,否则磁盘最终会被填满,并且在TSDB重启时 replay wal 事件时会占用大量时间,所以wal中任何不再需要的数据,都需要被清理。而checkpoint会将wal 清理过后的数据做过滤写成新的段。

如下有6个wal数据段

代码语言:javascript
复制
data
└── wal
    ├── 000000
    ├── 000001
    ├── 000002
    ├── 000003
    ├── 000004
    └── 000005

现在我们要清理时间点 T之前的样本数据,假设为前4个数据段:

检查点操作将按 000000 000001 000002 000003顺序遍历所有记录,并且:

  1. 删除不再在 Head 中的所有序列记录。
  2. 丢弃所有 time 在 T之前的样本。
  3. 删除 T之前的所有 tombstone 记录。
  4. 重写剩余的序列、样本和tombstone记录(与它们在 WAL 中出现的顺序相同)。

checkpoint被命名为创建checkpoint的最后一个段号 checkpoint.X

这样我们得到了新的wal数据,当wal在replay时先找checkpoint,先从checkpoint中的数据段回放,然后是checkpoint.000003的下一个数据段000004

代码语言:javascript
复制
data
└── wal
    ├── checkpoint.000003
    |   ├── 000000
    |   └── 000001
    ├── 000004
    └── 000005
Block的持久化存储

上面我们认识了wal和chunks_head的存储构造,接下来是Block,什么是持久化Block?在什么时候创建?为啥要合并Block?

Block的目录结构

代码语言:javascript
复制
├── 01BKGTZQ1SYQJTR4PB43C8PD98   # block ID
│   ├── chunks         # Block中的chunk文件
│   │   └── 000001     
│   ├── tombstones     # 数据删除记录文件
│   ├── index          # 索引
│   └── meta.json       # bolck元信息

磁盘上的Block是固定时间范围内的chunk的集合,由它自己的索引组成。其中包含多个文件的目录。每个Block都有一个唯一的 ID(ULID),他这个ID是可排序的。当我们需要更新、修改Block中的一些样本时,Prometheus TSDB只能重写整个Block,并且新块具有新的 ID(为了实现后面提到的索引)。如果需要删除的话Prometheus TSDB通过tombstones 实现了在不触及原始样本的情况下进行清理。

tombstones 可以认为是一个删除标记,它记载了我们在读取序列期间要忽略哪些时间范围。tombstones 是Block中唯一在写入数据后用于存储删除请求所创建和修改的文件。

tombstones中的记录数据结构如下,分别对应需要忽略的序列、开始和结束时间。

代码语言:javascript
复制
┌────────────────────────┬─────────────────┬─────────────────┐
│ series ref <uvarint64> │ mint <varint64> │ maxt <varint64> │
└────────────────────────┴─────────────────┴─────────────────┘

meta.json

meta.json包含了整个Block的所有元数据

代码语言:javascript
复制
{
    "ulid": "01EM6Q6A1YPX4G9TEB20J22B2R",
    "minTime": 1602237600000,
    "maxTime": 1602244800000,
    "stats": {
        "numSamples": 553673232,
        "numSeries": 1346066,
        "numChunks": 4440437
    },
    "compaction": {
        "level": 1,
        "sources": [
            "01EM65SHSX4VARXBBHBF0M0FDS",
            "01EM6GAJSYWSQQRDY782EA5ZPN"
        ]
    },
    "version": 1
}

记录了人类可读的chunks的开始和结束时间,样本、序列、chunks数量以及合并信息。version告诉Prometheus如何解析metadata

Block合并

我们可以从之前的图中看到当内存映射中chunk跨越2小时(默认)后第一个Block就被创建了,当 Prometheus 创建了一堆Block时,我们需要定期对这些块进行维护,以有效利用磁盘并保持查询的性能。

Block合并的主要工作是将一个或多个现有块(source blocks or parent blocks)写入一个新块,最后,删除源块并使用新的合并后的Block代替这些源块。

为什么需要对Block进行合并?

  1. 上面对tombstones介绍我们知道Prometheus在对数据的删除操作会记录在单独文件stombstone中,而数据仍保留在磁盘上。因此,当stombstone序列超过某些百分比时,需要从磁盘中删除该数据。
  2. 如果样本数据值波动非常小,相邻两个Block中的大部分数据是相同的。对这些Block做合并的话可以减少重复数据,从而节省磁盘空间。
  3. 当查询命中大于1个Block时,必须合并每个块的结果,这可能会产生一些额外的开销。
  4. 如果有重叠的Block(在时间上重叠),查询它们还要对Block之间的样本进行重复数据删除,合并这些重叠块避免了重复数据删除的需要。

如上图示例所示,我们有一组顺序的Block [1,2,3,4]。数据块1,2,和3可以被合并形成的新的块是 [1,4]。或者成对压缩为[1,3]。所有的时间序列数据仍然存在,但是现在总体的数据块更少。这显著降低了查询成本。

Block是如何删除的?

对于源数据的删除Prometheus TSDB采用了一种简单的方式:即删除该目录下不在我们保留时间窗口的块。

如下图所示,块1可以安全地被删除,而2必须保留到完全落在边界之后

因为Block合并的存在,意味着获取越旧的数据,数据块可能就变得越大。因此必须得有一个合并的上限,,这样块就不会增长到跨越整个数据库。通常我们可以根据保留窗口设置百分比。

如何从大量的series中检索出数据?

在Prometheus TSDB V3引擎中使用了倒排索引,倒排索引基于它们内容的子集提供对数据项的快速查找,例如我们要找出所有带有标签 app="nginx"的序列,而无需遍历每一个序列然后再检查它是否包含该标签。

首先我们给每个序列分配一个唯一ID,查询ID的复杂度是O(1),然后给每个标签建一个倒排ID表。比如包含 app="nginx"标签的ID为1,11,111那么标签"nginx"的倒排序索引为[1,11,111],这样一来如果n是我们的序列总数,m是查询的结果大小,那么使用倒排索引的查询复杂度是O(m),也就是说查询的复杂度由m的数量决定。但是在最坏的情况下,比如我们每个序列都有一个“nginx”的标签,显然此时的复杂度变为O(n)了,如果是个别标签的话无可厚非,只能稍加等待了,但是现实并非如此。

标签被关联到数百万序列是很常见的,并且往往每次查询会检索多个标签,比如我们要查询这样一个序列app =“dev”AND app =“ops” 在最坏情况下复杂度是O(n^2),接着更多标签复杂度指数增长到O(n^3)、O(n^4)、O(n^5)... 这是不可接受的。那咋办呢?

如果我们将倒排表进行排序会怎么样?

代码语言:javascript
复制
"app=dev" -> [100,1500,20000,51166]
"app=ops" -> [2,4,8,10,50,100,20000]

他们的交集为[100,20000],要快速实现这一点,我们可以通过2个游标从列表值较小的一端率先推进,当值相等时就是可以加入到结果集合当中。这样的搜索成本显然更低,在k个倒排表搜索的复杂度为O(k*n)而非最坏情况下O(n^k)

剩下就是维护这个索引,通过维护时间线与ID、标签与倒排表的映射关系,可以保证查询的高效率。

以上我们从较浅的层面了解一下Prometheus TSDB存储相关的内容,本文仍然有很多细节没有提及,比如wal如何做压缩与回放,mmap的原理,TSDB存储文件的数据结构等等,如果你需要进一步学习可移步参考文章。

本文参考于:

Prometheus维护者Ganesh Vernekar的系列博客Prometheus TSDB

https://ganeshvernekar.com/blog/prometheus-tsdb-the-head-block/

Prometheus维护者Fabian的博客文章Writing a Time Series Database from Scratch(原文已失效)

https://fabxc.org/tsdb/

PromCon 2017: Storing 16 Bytes at Scale - Fabian Reinartz

https://www.youtube.com/watch?v=b_pEevMAC3I

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 容器云实践 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是时序数据?
  • 时序数据如何在Prometheus TSDB存储?
    • Prometheus TSDB 概览
      • Head中样本的生命周期
        • WAL 中checkpoint的作用
          • Block的持久化存储
          • 如何从大量的series中检索出数据?
          相关产品与服务
          数据库
          云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档