迅闻网
让更多人看到你

Prometheus时序数据库

笔者最近担起了公司监控的重担,而当前监控最流行的数据库即是Prometheus。依照笔者打破砂锅问到底的精神,天然要把这个开源组件源码搞明白才行。在经过一系列源码/资料的阅览以及各种Debug之后,对其内部机制有了一定的认识。今日,笔者就来介绍下Prometheus的存储结构。
因为篇幅较长,所以笔者分为两篇,本篇首要是描绘Prometheus监控数据在内存中的存储结构。下一篇,首要描绘的是监控数据在磁盘中的存储结构。
Gorilla
Prometheus的存储结构-TSDB是参阅了Facebook的Gorilla之后,自行实现的。所以阅览
这篇文章《Gorilla:AFast,Scalable,In-MemoryTimeSeriesDatabase》
,可以对Prometheus为何选用这样的存储结构有着清晰的理解。
监控数据点
下面是一个十分典型的监控曲线。
可以调查到,监控数据都是由一个一个数据点组成,所以可以用下面的结构来保存最基本的存储单元
typesamplestruct{
tint64
vfloat64
}
同时咱们还需求注意到的信息是,咱们需求知道这些点归于什么机器的哪种监控。这种信息在Promtheus中就用Label(标签来表明)。一个监控项一般会有多个Label(例如图中),所以一般用labels[]Label。
因为在咱们的习气中,并不关心单独的点,而是要关心这段时刻内的曲线状况。所以天然而然的,咱们存储结构肯定逻辑上是这个姿态:
这样,咱们就可以很容易的经过一个Labels(标签们)找到对应的数据了。
监控数据在内存中的表明方式

数据库
最近的数据保存在内存中
Prometheus将最近的数据保存在内存中,这样查询最近的数据会变得十分快,然后经过一个compactor定时将数据打包到磁盘。数据在内存中最少保存2个小时(storage.tsdb.min-block-duration。至于为什么设置2小时这个值,应该是Gorilla那篇论文中调查得出的定论
即紧缩率在2小时时分达到最高,假如保存的时刻更短,就无法最大化的紧缩。
内存序列(memSeries)
接下来,咱们看下具体的数据结构
typememSeriesstuct{
……
refuint64//其id
lstlabels.Labels//对应的标签调集
chunks[]*memChunk//数据调集
headChunk*memChunk//正在被写入的chunk
……
}
其间memChunk是真实保存数据的内存块,将在后面讲到。咱们先来调查下memSeries在内存中的安排。
由此咱们可以看到,针对一个终究端的监控项(包含抓取的一切标签,以及新增加的标签,例如ip),咱们都在内存有一个memSeries结构。
寻址memSeries
假如经过一堆标签快速找到对应的memSeries。天然的,Prometheus就选用了hash。首要结构体为:
typestripeSeriesstruct{
series[stripeSize]map[uint64]*memSeries//记载refId到memSeries的映射
hashes[stripeSize]seriesHashmap//记载hash值到memSeries,hash冲突选用拉链法
locks[stripeSize]stripeLock//分段锁
}
typeseriesHashmapmap[uint64][]*memSeries
因为在Prometheus中会频频的对map[hash/refId]memSeries进行操作,例如查看这个labelSet对应的memSeries是否存在,不存在则创立等。因为golang的map非线程安全,所以其选用了分段锁去拆分锁。
而hash值是依据labelSets的值而算出来。
数据点的存储
为了让Prometheus在内存和磁盘中保存更大的数据量,必然需求进行紧缩。而memChunk在内存中保存的正是选用XOR算法紧缩过的数据。在这里,笔者只给出Gorilla论文中的XOR描绘
更具体的算法在论文中有详细描绘。总之,使用了XOR算法后,均匀每个数据点能从16bytes紧缩到1.37bytes,也就是说所用空间直接降为本来的1/12!
内存中的倒排索引
上面讨论的是标签悉数给出的查询状况。那么咱们怎样快速找到某个或某几个标签(非悉数标签)的数据呢。这就需求引进以Label为key的倒排索引。咱们先给出一组标签调集
{__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}
可以看到,因为标签取值不同,咱们会有四种不同的memSeries。假如一次性给定4个标签,应该是很容易从map中直接获取出对应的memSeries(虽然Prometheus并没有这么做)。但大部分咱们的promql只是给定了部分标签,怎么快速的查找符合标签的数据呢?
这就引进倒排索引。
先看一下,上面比如中的memSeries在内存中会有4种,同时内存中还夹杂着其它监控项的series
假如咱们想知道job:api-server,group为production在一段时刻内一切的http恳求数量,那么有必要获取标签带着
({__name__:http_requests}{job:api-server}{group:production})的一切监控数据。
假如没有倒排索引,那么咱们有必要遍历内存中一切的memSeries(数万乃至数十万),逐个依照Labels去比对,这显然在性能上是不行接受的。而有了倒排索引,咱们就可以经过求交集的手段迅速的获取需求哪些memSeries。
注意,这边倒排索引存储的refId有必要是有序的。这样,咱们就可以在O(n)复杂度下顺利的算出交集,另外,针对其它恳求方式,还有并集/差集的操作,对应实现结构体为:
typeintersectPostingsstruct{…}//交集
typemergedPostingsstruct{…}//并集
typeremovedPostingsstruct{…}//差集
倒排索引的插入安排即为Prometheus下面的代码
Add(labels,t,v)
|->getOrCreateWithID
|->memPostings.Add
//Addalabelsettothepostingsindex.
func(p*MemPostings)Add(iduint64,lsetlabels.Labels){
p.mtx.Lock()
//将新创立的memSeriesrefId都加到对应的Label倒排里去
for_,l:=rangelset{
p.addFor(id,l)
}
p.addFor(id,allPostingsKey)//allPostingKey””,””everyone都加进去
p.mtx.Unlock()
}
正则支持
事实上,给定特定的Label:Value还是无法满足咱们的需求。咱们还需求对标签正则化,例如取出一切ip为1.1.*前缀的http_requests监控数据。为了这种正则,Prometheus还保护了一个标签一切可能的取值。对应代码为:
Add(labels,t,v)
|->getOrCreateWithID
|->memPostings.Add
func(h*Head)getOrCreateWithID(id,hashuint64,lsetlabels.Labels){

for_,l:=rangelset{
valset,ok:=h.values[l.Name]
if!ok{
valset=stringset{}
h.values[l.Name]=valset
}
//将可能取值塞入stringset中
valset.set(l.Value)
//符号表的保护
h.symbols[l.Name]=struct{}{}
h.symbols[l.Value]=struct{}{}
}

}
那么,在内存中,咱们就有了如下的表
图中所示,有了label和对应一切value调集的表,一个正则恳求就可以很容的分解为若干个非正则恳求并最后求交/并/查集,即可得到终究的结果。
总结
Prometheus作为当今最流行的时序数据库,其间有十分多的值得咱们借鉴的设计和机制。这一篇笔者首要描绘了监控数据在内存中的存储结构。

未经允许不得转载:迅闻网 » Prometheus时序数据库
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

迅闻网-让更多人看到你

登录/注册返回首页