likes
comments
collection

深入了解Prometheus监控平台

作者站长头像
站长
· 阅读数 14

之前一篇文章Grafana & prometheus 入门中简单介绍了Promtheus的使用。

最近工作中频繁的使用到了prometheus,所以也希望再深入了解一下这个系统。

系统架构

深入了解Prometheus监控平台 普罗米修斯系统的架构如图所示,其提供的功能大致上可以分为以下三类

  • 数据采集
  • 数据存储
  • 数据查询

数据采集

从架构图的左侧可以看出,prometheus采用的是拉取的方式从目标服务拉取指标(时序数据),拉取目标的信息配置中在prometheus.yml文件中。系统会每隔一定时间(默认15s)去目标服务器拉取。

深入了解Prometheus监控平台

目标服务需要启动一个http服务,开放端口供prometheus拉取数据(prometheus提供了客户端api,不需要另外实现http服务器)。

关于为什么是pull而不是push的问题,prometheus官方给出了答复

深入了解Prometheus监控平台 使用拉取的方式有以下优势:

  • 服务不需要知道监控平台的存在,只需要收集指标数据,可以根据需要增加实例
  • 如果目标实例挂掉,监控平台可以很容易地知道(无法成功拉取)
  • 你可以手动指定一个目标,并通过浏览器检查该目标实例的监控状况

当然,对于特殊情况只能使用push的场景,比如监控平台搭建在外网,服务在内网或者服务存活时间非常短暂。prometehus也提供了push网关。

目标主机可以上报数据到pushgateway,,然后prometheus server统一从pushgateway拉取数据

数据抓取基本逻辑:

数据抓取的实现主要在系统的scrape模块当中,由scrape_manager统一管理;

深入了解Prometheus监控平台 启动时将配置文件中需要监控的target列表传入,并生成target对象;

// Run receives and saves target set updates and triggers the scraping loops reloading.
// Reloading happens in the background so that it doesn't block receiving targets updates.
func (m *Manager) Run(tsets <-chan map[string][]*targetgroup.Group) error {
	go m.reloader()
	for {
		select {
		case ts := <-tsets:
			m.updateTsets(ts)

			select {
			case m.triggerReload <- struct{}{}:
			default:
			}

		case <-m.graceShut:
			return nil
		}
	}
}

update函数会生成target对象,并会调用内部sync,通过开多个携程来定时调用scrapeAndReport函数拉取目标数据

深入了解Prometheus监控平台

深入了解Prometheus监控平台 源码解析可以参考:ray1888.github.io/2019/10/06/…

数据存储

向Target拉取到数据之后就需要存储到数据库中,prometheus数据存储的是时序数据,大致的数据结构如下

type sample struct {
    t int64
    v float64
}

每个采样点sample由时间和值组成。

相同的label集合为一个series,其和时间的关系可以总结为下面这张图

深入了解Prometheus监控平台

对数据的操作基本可以总结为———垂直写,水平读

读取一个时间范围内的数据,写入当前时刻不同series的数据 深入了解Prometheus监控平台

我们先放下具体的数据结构的实现,看看prometheus是的文件结构是什么样子的。

prometheus文件结构

Prometheus默认使用的是存储在本地磁盘的时间序列数据库,同时也支持与远程存储系统集成。

promethus的本地时序数据库的存储结构主要分为三个部分

  • block:持久化到硬盘中的数据,2小时的数据为1个块,后期会被压缩的更久
  • chunks_head: 当前在写入的 block 对应的 chunks 文件,
  • WAL:预写式日志( WAL ),用于确保数据完整性

我们用docker部署prometheus后,可以看到目录下的文件结构如下

深入了解Prometheus监控平台 前面的每个文件夹是一个block,是持久化到内存中的时序数据。从时间上看后期大概被压缩到了大概是每18个小时一个block。但最近的block都是2小时一个block。

block

深入了解Prometheus监控平台 每个block中包含以下几个部分

  • chunk:是一个子目录,包含了若干个从000001开始编号的文件。文件中存储的就是在时间窗口[minTime,maxTime]以内的所有样本数据samples,本质上就是对于内存中符合要求的memChunk的持久化。
  • index: 存储了索引相关的内容
  • meta数据:包含了当前Block的元数据信息
  • tombstones: 用于存储对于时间序列的删除记录。如果删除了某个时间序列,Prometheus并不会立即对它进行清理,而是会在tombstones做一次记录,等到下一次Block压缩合并的时候统一清理。

meta 元数据

meta数据结构如图: 深入了解Prometheus监控平台 其中

  • ulid:用于识别这个Block的编号,它与Block的目录名一致

  • minTimemaxTime:表示这个Block存储的数据的时间窗口

  • stats:表示这个Block包含的样本sample, 时间序列series以及chunks数目

  • compaction:这个Block的压缩信息,因为随着时间的流逝,多个Block也会压缩合并形成更大的Block。level字段表示了压缩的次数,刚从内存持久化的Block的level为1,每被联合压缩一次,子Block的level就会在父Block的基础上加一,而sources字段则包含了构成当前这个Block的所有祖先Block的ulid。事实上,对于level >= 2的Block,还会有一个parent字段,包含了它的父Block的ulid以及时间窗口信息。

Index -- 快速检索的关键

block的索引结构index大概如下 深入了解Prometheus监控平台

主要可以分为以下几个部分:

  • TOC:TOC包含了整个index索引文件的全局信息,存储的内容是其余六部分的位置信息,即它们的起始位置在index文件中的偏移量。
  • Symbol Table:Symbol Table符号表存储的就是在[minTime, maxTime]范围内的时间序列的所有label的key和value集合(做了去重和排序),并且为每个symbol进行了编号。metric名字也是一种label(KEY是__name__)

深入了解Prometheus监控平台

  • Series:存储的是时间序列series的相关信息,首先存储series的各个label(是对应key和value在Symbol Table中的编号)。紧接着存储series相关的chunks信息,包含每个chunk的时间窗口,以及该chunk在chunks子目录下具体的位置信息。

深入了解Prometheus监控平台

  • Label Index:存储了各个label的key和它所有可能的value的关联关系。例如,对于一个有着四个不同的value的key,它在这部分存储的条目如下所示:

深入了解Prometheus监控平台

Label Index Table:存储了所有label的key,以及它们在Label Index中对应的位置信息。那么为什么要将这两部分的内容分开存储呢(Label Index部分没有key的信息)?

Prometheus在读取Block中的数据时会加载index文件,但是只会首先加载Label Index Table获取所有label的key,只有在需要对key相关的value进行匹配时,才会加载Label Index相应的部分以及对应的Symbol。通过Label Index TableLabel Index的分离,使得我们能够只对必要数据进行加载,从而加快了index文件的加载速度。

Postings: 这部分存储的是倒排索引的信息,每一个条目存储的都是包含某个label pair的所有series的ID。但是与Label Index相似,条目中并没有指定具体的key和value。

深入了解Prometheus监控平台 Postings Offset Table:类似Label Table,这部分直接对每个label的key和value以及相关索引在Postings中的位置进行存储。

同样,它会首先被加载到内存中,如果需要知道包含某个label的所有series,再通过相关索引的偏移位置从Postings中依次获取。

深入了解Prometheus监控平台

chunk

chunk 文件夹下存储的具体的时序数据,具体的监控数据 每个文件的最大大小为 512MB。 每个chunk文件的结构如下 深入了解Prometheus监控平台 其中每条chunk就是一条数据

深入了解Prometheus监控平台

chunk_head & WAL

是当前写入的块,程序通过mmap进行写入,减少io次数,每隔一定时间,cpu会从内存中将数据刷入硬盘

wal和大多数的数据库一样,写入日志,防止意外崩溃丢失数据

整个存储流程

深入了解Prometheus监控平台 了解完了prometheus的整个存储的文件结构,我们再过一遍的他的存储流程

深入了解Prometheus监控平台 我们每次取抓取到一个时间序列(t,v),会将数据写入Chunk_Head块;为了防止内存数据丢失先做一次预写日志 (WAL) 。这个数据会在内存中停留一段时间,然后刷新到磁盘(M-map) 。当这些内存映射的块老化到某个时间点时(2个小时),会作为持久块Block存储到磁盘。接下来多个Block在它们变旧时被合并,并在超过保留期限后被清理。

默认情况下,每两个小时,会将内存中的chunk刷入硬盘进行保存 深入了解Prometheus监控平台

深入了解Prometheus监控平台

深入了解Prometheus监控平台

PS.mmap 我们传统的IO方式,底层实际上通过调用read()和write()来实现。通过read()把数据从硬盘读取到内核缓冲区,再复制到用户缓冲区;然后再通过write()写入到socket缓冲区,最后写入网卡设备。速度较慢

mmap实际上是将文件的地址映射到了进程中用户缓冲区,进程通过读写进程的用户缓存区,即可直接写入文件,不需要通过cpu将数据搬运到内核空间,再通过DMA写入文件。

参考文章:juejin.cn/post/695603… 深入了解Prometheus监控平台

存储的数据结构

程序运行的过程中,内存的时间序列的数据结构大致如下


type memSeries stuct {
    ......
    ref uint64 // 其Seriesid
    lst labels.Labels // 对应的标签集合
    chunks []*memChunk // 数据集合
    headChunk *memChunk // 正在被写入的chunk
    ......
}
type Label struct {
   Name, Value string
}

这个series结构会保存序列的label集合,有这个序列数据的chunk列表以及正在被写入的chunk指针 深入了解Prometheus监控平台 每次获得到新的时序时序数据后,就会写入到headChunk中。

prometheus内存中维护了一个id到series的hash map,可以快速拿到对应的数据的memSeries

同时,通过对label作hash,也能快速得到memseries,因为也同样维护了一张map

type stripeSeries struct {
    series [stripeSize]map[uint64]*memSeries // 记录refId到memSeries的映射
    hashes [stripeSize]seriesHashmap // 记录hash值到memSeries,hash冲突采用拉链法
    locks  [stripeSize]stripeLock // 分段锁
}
type seriesHashmap map[uint64][]*memSeries

同时因为go的map不是线程安全的,通过加分段锁,即保证了线程安全,也防止锁的范围太大,影响性能。

深入了解Prometheus监控平台

数据检索

数据检索流程

数据存储到硬盘后,如何能够快速的寻址series,就需要靠前面讲到的Index结构了,

比如我们给出几条时间序列的查询条件

{__name__:http_requests}{group:canary}{instance:0}{job:api-server}   
{__name__:http_requests}{group:canary}{instance:1}{job:api-server}
{__name__:http_requests}{group:production}{instance:1}{job,api-server}
{__name__:http_requests}{group:production}{instance:0}{job,api-server}

prometheus利用类似文档搜索的倒排索引,引入以Label为key的倒排索引(前面讲的index中的posting)

从posting table中查到这个label对于posting序号,然后获取到每一个label对应的seriesID列表

通过取交集的形式,快速获得满足要求的series序号

然后通过查找series表,快速定位到相关的chunk位置 深入了解Prometheus监控平台

Prometheus如何做到高可用?

在Prometheus官方的推荐方案中,对于高可用的处理是通过部署多套Prometheus,配置同样的目标实例来实现的。在这个方案里面,多套Prometheus会获取相同的监控指标,并且触发同样的告警规则,而对于警报的去重工作则由Alertmanager来负责。 深入了解Prometheus监控平台 但此方案也存在着明显缺点,比如当某个Prometheus出现故障或中断时,那么该节点将会出现数据丢失的情况,并与另一个节点存在数据差异。当在该节点上进行查询操作时,就会遇到这个问题。

对此,我们可以与远程存储方案结合起来,将Prometheus的读写放到远程存储端,通过高可用 +远程存储的方式来解决上面的问题。

深入了解Prometheus监控平台

Prometheus也支持集群模式,在大规模的监控环境中,当单个Prometheus无法处理大量的监控采集任务时,我们可以基于联邦的模式将采集任务划分到不同的Prometheus实例中,再由顶层的Prometheus进行数据的统一管理。

深入了解Prometheus监控平台 在此方案中,工作节点的Prometheus根据拆分原则,负责指定目标的数据采集及规则告警工作,而主节点则通过/federate接口从工作节点获取数据指标,并写入到远程存储中,同时对接Grafana实现监控展示。

Promql

prometheus中查询数据使用的是promql,相关的指标和用法介绍在前一篇文章Grafana & prometheus 入门中介绍了,后面再整理过来。

Reference

juejin.cn/post/708639…

www.cnblogs.com/YaoDD/p/113…

zhuanlan.zhihu.com/p/514223931