Prometheus TSDB存储原理

Prometheus TSDB存储原理Python 微信订餐小程序课程视频 https blog csdn net m0 article details Python 实战量化交易理财系统 https blog csdn net m0 article details Prometheus 包含一个存储在本地磁盘的时间序列数据库 同时也支持与远程存储系统集成 比如 grafanacloud 提供的免费云存储 API 只需将 remote write 接口信息填写在 Prome

Python微信订餐小程序课程视频

https://blog.csdn.net/m0_/article/details/

Python实战量化交易理财系统

image-20220412141006992

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

什么是时序数据?


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

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

identifier->(t0,v0),(t1,v1),(t2,v2)... 

Prometheus TSDB的数据模型

<metric name>{<label name>=<label value>, ...} 

具体到某个实例中

requests\_total{method="POST", handler="/messages"} 

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

 ----------------------------------------key-----------------------------------------------value--------- {__name__="requests\_total",method="POST", handler="/messages"} @.197 52 

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

series

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


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

Prometheus TSDB 概览

image-20220413104124771

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

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

Head中样本的生命周期

image-20220413120050962

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

image-20220413120803681

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

image-20220413121223066

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

image-20220413121732282

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

image-20220413113035412

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

./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数据段

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

data └── wal ├── checkpoint.000003 | ├── 000000 | └── 000001 ├── 000004 └── 000005 
Block的持久化存储

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

Block的目录结构

├── 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中的记录数据结构如下,分别对应需要忽略的序列、开始和结束时间。

┌────────────────────────┬─────────────────┬─────────────────┐ │ series ref <uvarint64> │ mint <varint64> │ maxt <varint64> │ └────────────────────────┴─────────────────┴─────────────────┘ 

meta.json

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

{ "ulid": "01EM6Q6A1YPX4G9TEB20J22B2R", "minTime": 00, "maxTime": 00, "stats": { "numSamples": , "numSeries": , "numChunks":  }, "compaction": { "level": 1, "sources": [ "01EM65SHSX4VARXBBHBF0M0FDS", "01EM6GAJSYWSRDY782EA5ZPN" ] }, "version": 1 } 

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

Block合并

image-20220413113035412

我们可以从之前的图中看到当内存映射中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之间的样本进行重复数据删除,合并这些重叠块避免了重复数据删除的需要。
  5. image-20220414120529698

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

Block是如何删除的?

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

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

image-20220413202322093

因为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(n2),接着更多标签复杂度指数增长到O(n3)、O(n4)、O(n5)… 这是不可接受的。那咋办呢?

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

"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存储文件的数据结构等等,如果你需要进一步学习可移步参考文章。通过博客阅读:iqsing.github.io


本文参考于:

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

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

PromCon 2017: Storing 16 Bytes at Scale – Fabian Reinartz

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/233470.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • Windows 7 及 Vista无法启动MSN的解决办法 (转)

    Windows 7 及 Vista无法启动MSN的解决办法 (转)

    2021年5月7日
    137
  • java prototype是什么,Java设计模式之原型模式(Prototype模式)介绍

    java prototype是什么,Java设计模式之原型模式(Prototype模式)介绍Prototype模式定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。如何使用原型模式因为Java中的提供clone()方法来实现对象的克隆,所以Prototype模式…

    2025年6月14日
    0
  • Django之mysql表单操作

    在Django之ORM模型中总结过django下mysql表的创建操作,接下来总结mysql表记录操作,包括表记录的增、删、改、查。1.添加表记录对于表单的添加有三种方式:2.删除表记录m

    2021年12月29日
    39
  • windows vista模拟器_windows vista旗舰版

    windows vista模拟器_windows vista旗舰版由于科技的进步,微软当然不示落后,让很多市面上的笔记本电脑预装了WindowsVista操作系统,而使没有安装这一

    2022年8月31日
    0
  • 项目开发序言「建议收藏」

    项目开发序言「建议收藏」今天决定换成uni-app来开发。用到的工具:HBuilder +微信开发者工具 + 小程序appid1.功能概述 消费者端:分为首页、商城、我的 首页:banner广告展示、菜品预览 商城:banner广告展示、全部商品、热销商品、公益、非遗 我的:积分和信用分的展示、我的兑换、今日签到、设置 商家端:功能、我的 功能:…

    2022年8月18日
    6
  • 首选DNS服务器地址不显示,首选dns服务器如何设置?如何设置DNS地址

    首选DNS服务器地址不显示,首选dns服务器如何设置?如何设置DNS地址首选dns服务器如何设置?如何设置DNS地址分类:云服务资讯编辑:聊聊云计算浏览量:1652021-01-2915:18:29现在有很多朋友对于首选dns服务器的设置方法不是很了解,不知道如何操作,今天新网就给大家详细的介绍下首选dns服务器如何设置以及如何设置DNS地址等问题,希望提供些帮助。首选dns服务器怎么设置?在“开始”中找到“运行”或者直接【Win】+【R】,然后输入“cmd”进入管…

    2022年6月13日
    25

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号