Prometheus时序数据库-数据的查询

运维 数据库运维
Promql是非常强大的,可以满足我们的各种需求。其运行原理自然也激起了笔者的好奇心,本篇文章虽然只分析了一条简单的Promql,但万变不离其宗,任何Promql都是类似的运行逻辑。希望本文对读者能有所帮助。

[[387578]]

前言

在之前的博客里,笔者详细阐述了Prometheus数据的插入过程。但我们最常见的打交道的是数据的查询。Prometheus提供了强大的Promql来满足我们千变万化的查询需求。在这篇文章里面,笔者就以一个简单的Promql为例,讲述下Prometheus查询的过程。

Promql

一个Promql表达式可以计算为下面四种类型:

  1. 瞬时向量(Instant Vector) - 一组同样时间戳的时间序列(取自不同的时间序列,例如不同机器同一时间的CPU idle) 
  2. 区间向量(Range vector) - 一组在一段时间范围内的时间序列 
  3. 标量(Scalar) - 一个浮点型的数据值 
  4. 字符串(String) - 一个简单的字符串 

我们还可以在Promql中使用svm/avg等集合表达式,不过只能用在瞬时向量(Instant Vector)上面。为了阐述Prometheus的聚合计算以及篇幅原因,笔者在本篇文章只详细分析瞬时向量(Instant Vector)的执行过程。

瞬时向量(Instant Vector)

前面说到,瞬时向量是一组拥有同样时间戳的时间序列。但是实际过程中,我们对不同Endpoint采样的时间是不可能精确一致的。所以,Prometheus采取了距离指定时间戳之前最近的数据(Sample)。如下图所示:

 

当然,如果是距离当前时间戳1个小时的数据直观看来肯定不能纳入到我们的返回结果里面。

所以Prometheus通过一个指定的时间窗口来过滤数据(通过启动参数—query.lookback-delta指定,默认5min)。

对一条简单的Promql进行分析

好了,解释完Instant Vector概念之后,我们可以着手进行分析了。直接上一条带有聚合函数的Promql吧。

  1. SUM BY (group) (http_requests{job="api-server",group="production"}) 

首先,对于这种有语法结构的语句肯定是将其Parse一把,构造成AST树了。调用

  1. promql.ParseExpr 

由于Promql较为简单,所以Prometheus直接采用了LL语法分析。在这里直接给出上述Promql的AST树结构。

 

Prometheus对于语法树的遍历过程都是通过vistor模式,具体到代码为:

  1. ast.go vistor设计模式 
  2. func Walk(v Visitor, node Node, path []Node) error { 
  3.     var err error 
  4.     if v, err = v.Visit(node, path); v == nil || err != nil { 
  5.         return err 
  6.     } 
  7.     path = append(path, node) 
  8.  
  9.     for _, e := range Children(node) { 
  10.         if err := Walk(v, e, path); err != nil { 
  11.             return err 
  12.         } 
  13.     } 
  14.  
  15.     _, err = v.Visit(nil, nil) 
  16.     return err 
  17. func (f inspector) Visit(node Node, path []Node) (Visitor, error) { 
  18.     if err := f(node, path); err != nil { 
  19.         return nil, err 
  20.     } 
  21.  
  22.     return f, nil 

通过golang里非常方便的函数式功能,直接传递求值函数inspector进行不同情况下的求值。

  1. type inspector func(Node, []Node) error 

求值过程

具体的求值过程核心函数为:

  1. func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *EvalStmt) (Value, storage.Warnings, error) { 
  2.     ...... 
  3.     querier, warnings, err := ng.populateSeries(ctxPrepare, query.queryable, s)     // 这边拿到对应序列的数据 
  4.     ...... 
  5.     val, err := evaluator.Eval(s.Expr) // here 聚合计算 
  6.     ...... 
  7.  

populateSeries

首先通过populateSeries的计算出VectorSelector Node所对应的series(时间序列)。这里直接给出求值函数

  1. func(node Node, path []Node) error { 
  2.     ...... 
  3.     querier, err := q.Querier(ctx, timestamp.FromTime(mint), timestamp.FromTime(s.End)) 
  4.     ...... 
  5.     case *VectorSelector: 
  6.         ....... 
  7.         set, wrn, err = querier.Select(params, n.LabelMatchers...) 
  8.         ...... 
  9.         n.unexpandedSeriesSet = set 
  10.     ...... 
  11.     case *MatrixSelector: 
  12.         ...... 
  13. return nil 

可以看到这个求值函数,只对VectorSelector/MatrixSelector进行操作,针对我们的Promql也就是只对叶子节点VectorSelector有效。

 

select

获取对应数据的核心函数就在querier.Select。我们先来看下qurier是如何得到的.

  1. querier, err := q.Querier(ctx, timestamp.FromTime(mint), timestamp.FromTime(s.End)) 

根据时间戳范围去生成querier,里面最重要的就是计算出哪些block在这个时间范围内,并将他们附着到querier里面。具体见函数

  1. func (db *DB) Querier(mint, maxt int64) (Querier, error) { 
  2.     for _, b := range db.blocks { 
  3.         ...... 
  4.         // 遍历blocks挑选block 
  5.     } 
  6.     // 如果maxt>head.mint(即内存中的block),那么也加入到里面querier里面。 
  7.     if maxt >= db.head.MinTime() { 
  8.         blocks = append(blocks, &rangeHead{ 
  9.             head: db.head, 
  10.             mint: mint, 
  11.             maxt: maxt, 
  12.         }) 
  13.     } 
  14.     ...... 

 

知道数据在哪些block里面,我们就可以着手进行计算VectorSelector的数据了。

  1. // labelMatchers {job:api-server} {__name__:http_requests} {group:production} 
  2.  querier.Select(params, n.LabelMatchers...) 

有了matchers我们很容易的就能够通过倒排索引取到对应的series。为了篇幅起见,我们假设数据都在headBlock(也就是内存里面)。那么我们对于倒排的计算就如下图所示:

 

这样,我们的VectorSelector节点就已经有了最终的数据存储地址信息了,例如图中的memSeries refId=3和4。


 

如果想了解在磁盘中的数据寻址,可以详见笔者之前的博客

  1. <<Prometheus时序数据库-磁盘中的存储结构>> 

通过populateSeries找到对应的数据,那么我们就可以通过evaluator.Eval获取最终的结果了。计算采用后序遍历,等下层节点返回数据后才开始上层节点的计算。那么很自然的,我们先计算VectorSelector。

  1. func (ev *evaluator) eval(expr Expr) Value { 
  2.     ...... 
  3.     case *VectorSelector: 
  4.     // 通过refId拿到对应的Series 
  5.     checkForSeriesSetExpansion(ev.ctx, e) 
  6.     // 遍历所有的series 
  7.     for i, s := range e.series { 
  8.         // 由于我们这边考虑的是instant query,所以只循环一次 
  9.         for ts := ev.startTimestamp; ts <= ev.endTimestamp; ts += ev.interval { 
  10.             // 获取距离ts最近且小于ts的最近的sample 
  11.             _, v, ok := ev.vectorSelectorSingle(it, e, ts) 
  12.             if ok { 
  13.                     if ev.currentSamples < ev.maxSamples { 
  14.                         // 注意,这边的v对应的原始t被替换成了ts,也就是instant query timeStamp 
  15.                         ss.Points = append(ss.Points, Point{V: v, T: ts}) 
  16.                         ev.currentSamples++ 
  17.                     } else { 
  18.                         ev.error(ErrTooManySamples(env)) 
  19.                     } 
  20.                 } 
  21.             ...... 
  22.         } 
  23.     } 

如代码注释中看到,当我们找到一个距离ts最近切小于ts的sample时候,只用这个sample的value,其时间戳则用ts(Instant Query指定的时间戳)代替。

其中vectorSelectorSingle值得我们观察一下:

  1. func (ev *evaluator) vectorSelectorSingle(it *storage.BufferedSeriesIterator, node *VectorSelector, ts int64) (int64, float64, bool){ 
  2.     ...... 
  3.     // 这一步是获取>=refTime的数据,也就是我们instant query传入的 
  4.     ok := it.Seek(refTime) 
  5.     ...... 
  6.         if !ok || t > refTime {  
  7.         // 由于我们需要的是<=refTime的数据,所以这边回退一格,由于同一memSeries同一时间的数据只有一条,所以回退的数据肯定是<=refTime的 
  8.         t, v, ok = it.PeekBack(1) 
  9.         if !ok || t < refTime-durationMilliseconds(LookbackDelta) { 
  10.             return 0, 0, false 
  11.         } 
  12.     } 

就这样,我们找到了series 3和4距离Instant Query时间最近且小于这个时间的两条记录,并保留了记录的标签。这样,我们就可以在上层进行聚合。

 

SUM by聚合

叶子节点VectorSelector得到了对应的数据后,我们就可以对上层节点AggregateExpr进行聚合计算了。代码栈为:

  1. evaluator.rangeEval 
  2.     |->evaluate.eval.func2 
  3.         |->evelator.aggregation grouping keygroup 

具体的函数如下图所示:

  1. func (ev *evaluator) aggregation(op ItemType, grouping []string, without bool, param interface{}, vec Vector, enh *EvalNodeHelper) Vector { 
  2.     ...... 
  3.     // 对所有的sample 
  4.     for _, s := range vec { 
  5.         metric := s.Metric 
  6.         ...... 
  7.         group, ok := result[groupingKey]  
  8.         // 如果此group不存在,则新加一个group 
  9.         if !ok { 
  10.             ...... 
  11.             result[groupingKey] = &groupedAggregation{ 
  12.                 labels:     m, // 在这里我们的m=[group:production] 
  13.                 value:      s.V, 
  14.                 mean:       s.V, 
  15.                 groupCount: 1, 
  16.             } 
  17.             ...... 
  18.         } 
  19.         switch op { 
  20.         // 这边就是对SUM的最终处理 
  21.         case SUM
  22.             group.value += s.V 
  23.         ..... 
  24.         } 
  25.     } 
  26.     ..... 
  27.     for _, aggr := range result { 
  28.         enh.out = append(enh.out, Sample{ 
  29.         Metric: aggr.labels, 
  30.         Point:  Point{V: aggr.value}, 
  31.         }) 
  32.     } 
  33.     ...... 
  34.     return enh.out 

好了,有了上面的处理,我们聚合的结果就变为:

 

这个和我们的预期结果一致,一次查询的过程就到此结束了。

总结

 

Promql是非常强大的,可以满足我们的各种需求。其运行原理自然也激起了笔者的好奇心,本篇文章虽然只分析了一条简单的Promql,但万变不离其宗,任何Promql都是类似的运行逻辑。希望本文对读者能有所帮助。

 

责任编辑:武晓燕 来源: 解Bug之路
相关推荐

2021-03-08 10:18:55

数据库数据Prometheus

2021-02-22 10:37:47

存储Prometheus

2021-03-01 10:20:52

存储

2022-07-06 15:41:55

数据库

2022-09-23 07:44:48

时序数据库物联网

2017-11-20 11:37:19

时序数据数据存储HBase

2021-09-26 10:08:33

TSDB时序数据库压缩解压

2022-07-11 10:45:12

数据库分析

2020-03-11 09:50:21

时序数据库快速检索

2022-07-11 11:12:32

数据分析

2022-12-18 19:38:31

时序数据库数据库

2021-08-31 14:01:59

时序数据库数据库数据

2022-07-07 12:23:29

数据库

2022-06-10 17:37:37

数据库

2022-07-07 12:37:27

数据

2017-09-05 14:45:14

时序数据数据库大数据

2018-06-26 09:37:07

时序数据库FacebookNoSQL

2021-08-04 05:49:40

数据库数时序数据库技术

2019-05-30 08:31:39

数据库QTSDB分布式

2018-04-16 08:44:51

InfluxDB TS时序数据库存储
点赞
收藏

51CTO技术栈公众号