Prometheus源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:深度解析范围查询的实现机制

监控系统 Prometheus 3.4.0 源码解析:深度解析 API queryRange 查询(Prometheus核心功能之一)的实现机制

  近段时间工作节奏异常紧凑,回顾起来,Prometheus 2.26 版本的源码研读竟已过去近一年之久,期间虽积累了不少心得,却始终未能整理成文。今天偶然发现 https://github.com/prometheus/prometheus/releases 已经更新到 3.4.0 版本,这让我萌生了从最新版本入手,重新开启 Prometheus 源码解析系列的想法。

  本篇将聚焦 Prometheus 3.4.x 版本中极具技术深度的核心功能——范围查询(Range Query)的实现原理,通过源码层面的深度剖析,和读者一起揭开这一关键特性的技术面纱。

一、定位处理 API queryRange 的相关代码文件

1.1、https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go

这个文件是处理 API 查询的核心文件之一。在 Prometheus 中,Web API 是与外界交互的重要接口,而 api.go 负责定义和处理各种 API 端点。

r.Get("/query", wrapAgent(api.query))
r.Post("/query", wrapAgent(api.query))
r.Get("/query_range", wrapAgent(api.queryRange))
r.Post("/query_range", wrapAgent(api.queryRange))

 以下是对 queryRange 函数逐行注释的详细解释,有助于初学者理解 Prometheus API 范围查询的原理:

// queryRange 函数用于处理 Prometheus API 的范围查询请求。
// 范围查询允许用户在指定的时间范围内,按照指定的步长执行 PromQL 查询。
func (api *API) queryRange(r *http.Request) (result apiFuncResult) {
    // 解析请求中的 limit 参数,该参数用于限制返回的时间序列数量。
    // limit 为 0 表示不限制。
    limit, err := parseLimitParam(r.FormValue("limit"))
    // 如果解析 limit 参数时出现错误,返回一个包含错误信息的 apiFuncResult,
    // 指出 "limit" 参数无效。
    if err != nil {
        return invalidParamError(err, "limit")
    }

    // 解析请求中的 start 参数,该参数指定查询的起始时间。
    // 时间格式可以是 RFC3339 或 Unix 时间戳。
    start, err := parseTime(r.FormValue("start"))
    // 如果解析起始时间时出现错误,返回一个包含错误信息的 apiFuncResult,
    // 指出 "start" 参数无效。
    if err != nil {
        return invalidParamError(err, "start")
    }

    // 解析请求中的 end 参数,该参数指定查询的结束时间。
    // 时间格式可以是 RFC3339 或 Unix 时间戳。
    end, err := parseTime(r.FormValue("end"))
    // 如果解析结束时间时出现错误,返回一个包含错误信息的 apiFuncResult,
    // 指出 "end" 参数无效。
    if err != nil {
        return invalidParamError(err, "end")
    }

    // 检查结束时间是否早于起始时间,如果是,则返回一个包含错误信息的 apiFuncResult,
    // 指出结束时间不能早于起始时间。
    if end.Before(start) {
        return invalidParamError(errors.New("end timestamp must not be before start time"), "end")
    }

    // 解析请求中的 step 参数,该参数指定查询的步长,即时间间隔。
    // 步长可以是持续时间格式(如 "1m" 表示 1 分钟)或浮点数表示的秒数。
    step, err := parseDuration(r.FormValue("step"))
    // 如果解析步长时出现错误,返回一个包含错误信息的 apiFuncResult,
    // 指出 "step" 参数无效。
    if err != nil {
        return invalidParamError(err, "step")
    }

    // 检查步长是否为零或负数,如果是,则返回一个包含错误信息的 apiFuncResult,
    // 提示用户使用正整数作为步长。
    if step <= 0 {
        return invalidParamError(errors.New("zero or negative query resolution step widths are not accepted. Try a positive integer"), "step")
    }

    // 为了安全起见,限制每个时间序列返回的点数。
    // 如果查询的时间范围除以步长得到的点数超过 11000,则返回一个包含错误信息的 apiFuncResult,
    // 提示用户减小查询分辨率(即增大步长)。
    if end.Sub(start)/step > 11000 {
        err := errors.New("exceeded maximum resolution of 11,000 points per timeseries. Try decreasing the query resolution (?step=XX)")
        return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
    }

    // 从请求中获取上下文,用于处理请求的生命周期和超时。
    ctx := r.Context()
    // 检查请求中是否包含 timeout 参数,该参数用于指定查询的超时时间。
    if to := r.FormValue("timeout"); to != "" {
        var cancel context.CancelFunc
        // 解析 timeout 参数为时间间隔。
        timeout, err := parseDuration(to)
        // 如果解析超时时间时出现错误,返回一个包含错误信息的 apiFuncResult,
        // 指出 "timeout" 参数无效。
        if err != nil {
            return invalidParamError(err, "timeout")
        }

        // 使用 context.WithTimeout 为查询设置超时时间,并在函数结束时取消上下文。
        ctx, cancel = context.WithTimeout(ctx, timeout)
        defer cancel()
    }

    // 从请求中提取查询选项,如 lookback_delta 等。
    opts, err := extractQueryOpts(r)
    // 如果提取查询选项时出现错误,返回一个包含错误信息的 apiFuncResult,
    // 指出查询选项无效。
    if err != nil {
        return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
    }

    // 使用查询引擎创建一个范围查询对象。
    // 传入上下文、可查询对象、查询选项、查询表达式、起始时间、结束时间和步长。
    qry, err := api.QueryEngine.NewRangeQuery(ctx, api.Queryable, opts, r.FormValue("query"), start, end, step)
    // 如果创建查询对象时出现错误,返回一个包含错误信息的 apiFuncResult,
    // 指出 "query" 参数无效。
    if err != nil {
        return invalidParamError(err, "query")
    }

    // 从这一步开始,确保在函数结束时关闭查询对象。
    // 如果 result.finalizer 为空,则调用 qry.Close 关闭查询对象。
    defer func() {
        if result.finalizer == nil {
            qry.Close()
        }
    }()

    // 从请求中获取上下文,并添加额外的请求信息。
    ctx = httputil.ContextFromRequest(ctx, r)

    // 执行查询,返回查询结果。
    res := qry.Exec(ctx)
    // 如果查询过程中出现错误,返回一个包含错误信息的 apiFuncResult,
    // 并将错误信息转换为 API 错误类型。
    if res.Err != nil {
        return apiFuncResult{nil, returnAPIError(res.Err), res.Warnings, qry.Close}
    }

    // 获取查询结果中的警告信息。
    warnings := res.Warnings
    // 如果 limit 参数大于 0,对查询结果进行截断处理,限制返回的时间序列数量。
    if limit > 0 {
        var isTruncated bool
        // 调用 truncateResults 函数对结果进行截断。
        res, isTruncated = truncateResults(res, limit)
        // 如果结果被截断,添加一个警告信息,提示结果因限制而被截断。
        if isTruncated {
            warnings = warnings.Add(errors.New("results truncated due to limit"))
        }
    }

    // 如果请求中包含 "stats" 参数,则在响应中包含查询统计信息。
    // 获取统计信息渲染器,如果为空,则使用默认的渲染器。
    sr := api.statsRenderer
    if sr == nil {
        sr = DefaultStatsRenderer
    }
    // 调用统计信息渲染器,生成查询统计信息。
    qs := sr(ctx, qry.Stats(), r.FormValue("stats"))

    // 返回包含查询结果、统计信息和警告信息的 apiFuncResult。
    return apiFuncResult{&QueryData{
        ResultType: res.Value.Type(),
        Result:     res.Value,
        Stats:      qs,
    }, nil, warnings, qry.Close}
}

原理总结

      1. 参数解析:从 HTTP 请求中解析查询所需的参数,包括起始时间、结束时间、步长、超时时间和限制数量等,并进行有效性检查。
      2. 上下文处理:根据请求中的超时参数设置查询的超时时间,确保查询不会无限期执行。
      3. 查询创建:使用查询引擎创建一个范围查询对象,传入解析后的参数。
      4. 查询执行:执行范围查询,并处理可能出现的错误。
      5. 结果处理:如果设置了结果限制,对查询结果进行截断处理,并添加相应的警告信息。
      6. 统计信息:根据请求中的 stats 参数,决定是否在响应中包含查询统计信息。
      7. 结果返回:将查询结果、统计信息和警告信息封装在 apiFuncResult 中返回。
通过这些步骤,Prometheus 的 API 范围查询功能可以在指定的时间范围内,按照指定的步长执行 PromQL 查询,并返回相应的结果,单测就不展开细说了:

https://github.com/prometheus/prometheus/blob/v3.4.0/cmd/promtool/main_test.go 
https://github.com/prometheus/prometheus/blob/v3.4.0/cmd/prometheus/query_log_test.go
https://github.com/prometheus/prometheus/blob/v3.4.0/web/web_test.go

二、深入查看 API 处理的具体实现

2.1、查询入口

Prometheus 提供了两种主要的查询 API(非常重要)

    1. 即时查询 /api/v1/query:(原理)即时查询用于获取某个特定时间点的数据。它接收一个 PromQL 查询表达式和一个时间戳作为输入,然后在该时间戳上执行查询,并返回查询结果。
    2. 范围查询 /api/v1/query_range:(原理)范围查询用于获取一段时间内的数据。它接收一个 PromQL 查询表达式、一个起始时间、一个结束时间和一个步长作为输入,然后在指定的时间范围内,按照指定的步长执行查询,并返回查询结果。

三、范围查询处理流程

    当 Prometheus 接收到 /api/v1/query_range 请求后,会按照以下流程创建并执行一个范围查询任务,最终返回时间序列数据结果。

3.1、解析请求参数

    在 https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#L544 文件的 func (api *API) queryRange(r *http.Request) (result apiFuncResult) 函数中,会对传入的 HTTP 请求参数进行解析,这些参数是构建查询语句的核心依据。 

主要包括

      • query:PromQL 查询表达式,用于指定要查询的数据。
      • start:查询的起始时间戳,可以是 RFC3339 格式或 Unix 时间戳。
      • end:查询的结束时间戳,可以是 RFC3339 格式或 Unix 时间戳。
      • step:查询的步长,可以是持续时间格式(如 1m 表示 1 分钟)或浮点数表示的秒数。
      • timeout:查询的超时时间,可选参数。
      • limit:返回结果的最大数量,可选参数。

3.2、 构建 EvalStmt 表达式树

    接下来,系统调用 parser.ParseExpr (https://github.com/prometheus/prometheus/blob/v3.4.0/promql/parser/parse.go#L171将 PromQL 字符串解析为抽象语法树(AST),并构造 EvalStmt (https://github.com/prometheus/prometheus/blob/v3.4.0/promql/parser/ast.go#L62) 对象,用于表示整个查询语句及其上下文信息,这个对象将作为后续查询构建和执行的基础结构。 

3.3、时间范围确定

    在 Prometheus 接收到一个范围查询请求后,首先需要确定查询的时间范围。这一过程由 FindMinMaxTime(https://github.com/prometheus/prometheus/blob/v3.4.0/promql/engine.go#L870) 函数完成,该函数会遍历表达式树,找到所有需要访问的时间序列选择器【如 VectorSelector(https://github.com/prometheus/prometheus/blob/v3.4.0/web/ui/mantine-ui/src/promql/ast.ts#L183) 和 MatrixSelector(https://github.com/prometheus/prometheus/blob/v3.4.0/web/ui/mantine-ui/src/promql/ast.ts#L142),并根据它们的偏移量和范围调整时间窗口。

// FindMinMaxTime 函数用于分析PromQL查询语句,提取其中所有时间选择器涉及的最小和最大时间戳
// 这有助于确定查询执行时需要扫描的时间序列数据范围
func FindMinMaxTime(s *parser.EvalStmt) (int64, int64) {
    // 初始化最小和最大时间戳,分别设置为可能的最大值和最小值作为初始条件
    var minTimestamp, maxTimestamp int64 = math.MaxInt64, math.MinInt64
    
    // evalRange 用于跟踪MatrixSelector设置的时间范围,在嵌套选择器中传递时间范围信息
    // MatrixSelector的格式为 <vector_selector>[<duration>],其中的duration会被记录在这里
    var evalRange time.Duration
    
    // 使用parser.Inspect递归遍历PromQL表达式的抽象语法树(AST)
    parser.Inspect(s.Expr, func(node parser.Node, path []parser.Node) error {
        // 根据当前节点类型进行不同处理
        switch n := node.(type) {
        case *parser.VectorSelector:
            // 处理向量选择器(例如:metric_name{label="value"} 或 metric_name{label="value"}[5m])
            // getTimeRangesForSelector 函数根据当前选择器和上下文中的时间范围计算实际时间区间
            start, end := getTimeRangesForSelector(s, n, path, evalRange)
            
            // 更新最小时间戳
            if start < minTimestamp {
                minTimestamp = start
            }
            
            // 更新最大时间戳
            if end > maxTimestamp {
                maxTimestamp = end
            }
            
            // 处理完VectorSelector后重置evalRange,避免影响后续不相关的选择器
            evalRange = 0
            
        case *parser.MatrixSelector:
            // 处理矩阵选择器(例如:metric_name{label="value"}[5m])
            // 记录MatrixSelector中指定的时间范围,供后续的VectorSelector使用
            evalRange = n.Range
        }
        
        return nil // 继续遍历AST
    })

    // 特殊情况处理:如果AST中没有找到任何选择器,返回0作为默认时间范围
    if maxTimestamp == math.MinInt64 {
        minTimestamp = 0
        maxTimestamp = 0
    }

    return minTimestamp, maxTimestamp
}
    1. 时间范围跟踪机制:
      • evalRange 变量在遇到 MatrixSelector 时被设置为其 Range 字段值
      • 后续遇到的 VectorSelector 会使用这个 evalRange 值来计算具体时间范围
      • 处理完 VectorSelector 后重置 evalRange,确保时间范围不会错误传递
    2. 时间戳更新逻辑:
      • 初始时 minTimestamp 和 maxTimestamp 分别设为 math.MaxInt64 和 math.MinInt64
      • 在遍历过程中不断比较并更新这两个值,确保最终得到的是所有选择器中的最小和最大时间戳
    3. 边界情况处理:
      • 如果表达式中没有任何选择器(如纯数学表达式),返回 0,0 表示没有时间范围限制

3.4、创建可执行范围查询对象——Engine.NewRangeQuery()

    在确定了时间范围之后,Prometheus 使用 Engine.NewRangeQuery 方法创建一个 Query 接口实例,它是对外暴露的标准接口方法。这个方法在 https://github.com/prometheus/prometheus/blob/v3.4.0/promql/engine.go#L499 中定义,其主要作用是对查询进行初始化,包括解析查询表达式、设置查询的起始时间、结束时间和步长等。

1、首先创建 query(https://github.com/prometheus/prometheus/blob/v3.4.0/promql/engine.go#L185)对象,该对象承载查询的上下文信息。这一过程通过内部调用 newQuery()(https://github.com/prometheus/prometheus/blob/v3.4.0/promql/engine.go#L521) 方法完成,它会初始化底层 query(https://github.com/prometheus/prometheus/blob/v3.4.0/promql/engine.go#L185) 实例,设置表达式、时间区间、步长等元数据,并返回一个待填充的 `*parser.Expr` 和 `*query` 对象。

2、再调用 NewRangeQuery()方法(对外暴露接的接口方法,用于创建一个可用于执行的范围查询对象。1、调用 newQuery()获取初始查询对象,2、将其放入活动查询队列中,这是是为了防止并发超限,3、解析 PromQL 表达式,4、验证表达式类型是否为合法范围查询类型,比如 Vector/Scalar,最后调用 https://github.com/prometheus/prometheus/blob/v3.4.0/promql/engine.go#L3658 做预处理,并返回完整的 https://github.com/prometheus/prometheus/blob/v3.4.0/promql/engine.go#L138 接口实力 来初始化它。

// NewRangeQuery 创建一个新的范围查询对象。它接收上下文、可查询的存储对象、查询选项、查询字符串、开始时间、结束时间和时间间隔作为参数,并返回一个查询对象和可能的错误。
func (ng *Engine) NewRangeQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, start, end time.Time, interval time.Duration) (Query, error) {
    // 调用 newQuery 方法创建一个新的查询对象和解析后的表达式指针
    // pExpr 是指向解析后的表达式的指针,qry 是新创建的查询对象
    pExpr, qry := ng.newQuery(q, qs, opts, start, end, interval)

    // 尝试将查询放入活动队列中
    // finishQueue 是一个函数,用于在查询完成后清理队列资源
    // err 是可能发生的错误
    finishQueue, err := ng.queueActive(ctx, qry)
    if err != nil {
        // 如果入队失败,返回 nil 和错误信息
        return nil, err
    }
    // 确保在函数返回时调用 finishQueue 来清理队列资源
    defer finishQueue()

    // 解析查询字符串为表达式
    // expr 是解析后的表达式,err 是可能发生的解析错误
    expr, err := parser.ParseExpr(qs)
    if err != nil {
        // 如果解析失败,返回 nil 和错误信息
        return nil, err
    }

    // 验证查询选项是否有效
    // err 是可能发生的验证错误
    if err := ng.validateOpts(expr); err != nil {
        // 如果验证失败,返回 nil 和错误信息
        return nil, err
    }

    // 检查表达式的类型是否为有效的范围查询类型
    // 范围查询只支持向量类型和标量类型
    if expr.Type() != parser.ValueTypeVector && expr.Type() != parser.ValueTypeScalar {
        // 如果表达式类型无效,返回 nil 和错误信息,指出需要的类型
        return nil, fmt.Errorf("invalid expression type %q for range query, must be Scalar or instant Vector", parser.DocumentedType(expr.Type()))
    }

    // 对解析后的表达式进行预处理
    // 预处理可能包括优化、转换等操作
    // err 是可能发生的预处理错误
    *pExpr, err = PreprocessExpr(expr, start, end)

    // 返回查询对象和可能的错误
    return qry, err
}

    这段代码定义了 Query 接口,该接口规定了一个查询对象应具备的方法,包括执行查询、关闭查询、获取解析语句、获取统计信息、取消查询以及获取原始查询字符串等。同时,还实现了 query 结构体的 Exec 方法,该方法在执行查询前会尝试为查询操作添加 trace 信息,然后调用引擎的 exec 方法执行查询,并封装结果返回。 

这个流程(典型的调用栈举例:/api/v1/query_range → api.queryRange → engine.NewRangeQuery → engine.exec → engine.execEvalStmt → evaluator.Eval)见如下:

Prometheus 在接收到 /api/v1/query_range 请求后,会按照如下流程创建并执行一个范围查询:
解析请求参数,提取 start、end、step、query;
  • 构建 EvalStmt 表达式树,表示当前查询的 AST;
  • 调用 FindMinMaxTime () 优化底层数据扫描(通过遍历表达式树获取所有时间序列选择器,结合其偏移量和范围计算需扫描的最小 / 最大时间戳,以此实现数据扫描优化),而查询时间范围仍由 HTTP 请求参数 start、end、step 最终决定;
  • 调用 Engine.NewRangeQuery () 创建 Query 接口实例:
  • 内部先调用 newQuery () 构造基础结构
  • 接着解析 PromQL 表达式(ParseExpr)并验证其类型
  • 再调用 PreprocessExpr () 对表达式进行预处理(如常量折叠、时间修正等,并非所有表达式都需要复杂预处理)
  • 调用 query.Exec () 执行查询:
  • 即时查询(/api/v1/query)和范围查询(/api/v1/query_range)在 Exec 内部有分支,主要因 EvalStmt 的 Start、End、Interval 参数不同,导致 evaluator 行为不同
  • 范围查询执行后返回结果矩阵(Matrix),即时查询返回 Vector 或 Scalar
  • 最后对结果排序输出。

关键函数的职责补充

  • web/api/v1/api.go::queryRange:负责 HTTP 参数解析、调用 Engine.NewRangeQuery、执行查询、返回结果。
  • promql/engine.go::NewRangeQuery:负责创建 Query 对象,参数校验,表达式预处理。
  • promql/engine.go::execEvalStmt:负责实际的表达式评估,数据填充,调用 evaluator。
  • promql/engine.go::evaluator.Eval:递归评估 AST,执行 PromQL 运算。
  • promql/engine.go::FindMinMaxTime:优化数据扫描范围,辅助底层存储查询。

3.2、执行查询 

调用查询对象的 Exec 方法执行查询的实现细节主要与 promql/engine.go 文件相关。

3.2.1、Exec 方法的调用位置

https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go 文件中的 query 函数里调用了查询对象的 Exec 方法:

res := qry.Exec(ctx)
if res.Err != nil {
    return apiFuncResult{nil, returnAPIError(res.Err), res.Warnings, qry.Close}
}

这里的qry是通过api.QueryEngine.NewInstantQuery创建的查询对象,然后调用其Exec方法执行查询并获取结果res

3.2.2、Exec 方法的实现逻辑

https://github.com/prometheus/prometheus/blob/v3.4.0/promql/engine.go 从 promql/engine.go 文件里看到查询执行的核心逻辑,尤其是 execEvalStmt 函数,它承担了评估查询表达式的任务: 

// execEvalStmt 为给定的时间范围评估求值语句的表达式。
func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.EvalStmt) (parser.Value, annotations.Annotations, error) {
    // 创建查询准备阶段的计时器,用于记录查询准备时间
    prepareSpanTimer, ctxPrepare := query.stats.GetSpanTimer(ctx, stats.QueryPreparationTime, ng.metrics.queryPrepareTime)
    // 从评估语句中提取最小和最大时间戳
    mint, maxt := FindMinMaxTime(s)
    // 创建一个用于指定时间范围的查询器
    querier, err := query.queryable.Querier(mint, maxt)
    if err != nil {
        // 发生错误时结束准备计时器并返回错误
        prepareSpanTimer.Finish()
        return nil, nil, err
    }
    // 函数结束时关闭查询器以释放资源
    defer querier.Close()

    // 填充时间序列数据到评估语句中
    ng.populateSeries(ctxPrepare, querier, s)
    // 结束查询准备阶段的计时
    prepareSpanTimer.Finish()

    // 为@修饰符修改向量和矩阵选择器的偏移量
    // 相对于开始时间,因为只对它们进行一次评估
    setOffsetForAtModifier(timeMilliseconds(s.Start), s.Expr)
    // 创建内部评估阶段的计时器,用于记录内部评估时间
    evalSpanTimer, ctxInnerEval := query.stats.GetSpanTimer(ctx, stats.InnerEvalTime, ng.metrics.queryInnerEval)
    
    // 即时评估:作为只有一个步骤的范围评估执行
    if s.Start == s.End && s.Interval == 0 {
        // 获取开始时间的毫秒表示
        start := timeMilliseconds(s.Start)
        // 创建评估器实例,配置单次评估参数
        evaluator := &evaluator{
            startTimestamp:           start,
            endTimestamp:             start,
            interval:                 1,
            maxSamples:               ng.maxSamplesPerQuery,
            logger:                   ng.logger,
            lookbackDelta:            s.LookbackDelta,
            samplesStats:             query.sampleStats,
            noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn,
            enableDelayedNameRemoval: ng.enableDelayedNameRemoval,
            querier:                  querier,
        }
        // 初始化样本统计信息,跟踪采样情况
        query.sampleStats.InitStepTracking(start, start, 1)

        // 执行表达式评估
        val, warnings, err := evaluator.Eval(ctxInnerEval, s.Expr)

        // 结束内部评估阶段的计时
        evalSpanTimer.Finish()

        // 处理评估过程中发生的错误
        if err != nil {
            return nil, warnings, err
        }

        // 声明用于存储矩阵结果的变量
        var mat Matrix

        // 根据评估结果的类型进行不同处理
        switch result := val.(type) {
        case Matrix:
            // 如果结果是矩阵类型,直接赋值
            mat = result
        case String:
            // 如果是字符串类型,直接返回
            return result, warnings, nil
        default:
            // 对于不支持的类型,抛出异常
            panic(fmt.Errorf("promql.Engine.exec: invalid expression type %q", val.Type()))
        }

        // 将评估结果存入查询对象
        query.matrix = mat
        // 根据表达式类型处理评估结果
        switch s.Expr.Type() {
        case parser.ValueTypeVector:
            // 将每个序列只有一个值的矩阵转换为向量
            vector := make(Vector, len(mat))
            for i, s := range mat {
                // 点可能有不同的时间戳,强制将其设为评估时间戳
                if len(s.Histograms) > 0 {
                    vector[i] = Sample{Metric: s.Metric, H: s.Histograms[0].H, T: start, DropName: s.DropName}
                } else {
                    vector[i] = Sample{Metric: s.Metric, F: s.Floats[0].F, T: start, DropName: s.DropName}
                }
            }
            return vector, warnings, nil
        case parser.ValueTypeScalar:
            // 处理标量类型结果
            return Scalar{V: mat[0].Floats[0].F, T: start}, warnings, nil
        case parser.ValueTypeMatrix:
            // 对矩阵结果进行排序
            ng.sortMatrixResult(ctx, query, mat)
            return mat, warnings, nil
        default:
            // 对于意外的表达式类型,抛出异常
            panic(fmt.Errorf("promql.Engine.exec: unexpected expression type %q", s.Expr.Type()))
        }
    }

    // 范围评估:处理指定时间范围内的多次评估
    evaluator := &evaluator{
        startTimestamp:           timeMilliseconds(s.Start),
        endTimestamp:             timeMilliseconds(s.End),
        interval:                 durationMilliseconds(s.Interval),
        maxSamples:               ng.maxSamplesPerQuery,
        logger:                   ng.logger,
        lookbackDelta:            s.LookbackDelta,
        samplesStats:             query.sampleStats,
        noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn,
        enableDelayedNameRemoval: ng.enableDelayedNameRemoval,
        querier:                  querier,
    }
    // 初始化样本统计信息,跟踪采样情况
    query.sampleStats.InitStepTracking(evaluator.startTimestamp, evaluator.endTimestamp, evaluator.interval)
    // 执行表达式评估
    val, warnings, err := evaluator.Eval(ctxInnerEval, s.Expr)

    // 结束内部评估阶段的计时
    evalSpanTimer.Finish()

    // 处理评估过程中发生的错误
    if err != nil {
        return nil, warnings, err
    }

    // 类型断言,确保结果是矩阵类型
    mat, ok := val.(Matrix)
    if !ok {
        // 对于不支持的类型,抛出异常
        panic(fmt.Errorf("promql.Engine.exec: invalid expression type %q", val.Type()))
    }
    // 将评估结果存入查询对象
    query.matrix = mat

    // 检查上下文是否已取消,确保评估未超时或被中断
    if err := contextDone(ctx, "expression evaluation"); err != nil {
        return nil, warnings, err
    }

    // TODO(fabxc): 确保指标标签是存储内部标签的副本的位置
    // 对矩阵结果进行排序
    ng.sortMatrixResult(ctx, query, mat)

    // 返回评估结果、警告信息和错误(如果有)
    return mat, warnings, nil
} 

在 execEvalStmt 函数中,依据查询类型(即时查询或范围查询)创建不同的评估器 evaluator,接着调用评估器的 Eval 方法评估查询表达式,最终返回查询结果 val、警告信息 warnings 以及错误信息 err 

3.3、解析查询表达式

在 Prometheus 的 https://github.com/prometheus/prometheus/blob/main/promql/engine.go#L625 文件中,Engine 的 exec 方法负责执行查询。其第一步就是解析查询表达式,将原始 PromQL 字符串转换为可执行的 AST(抽象语法树)。 

// exec 执行查询的具体逻辑
func (e *Engine) exec(ctx context.Context, q *query) (parser.Value, annotations.Annotations, error) {
    // 解析查询表达式
    expr, err := parser.ParseExpr(q.q)
    if err != nil {
        return nil, nil, err
    }
   ……
}  

说明:

      • q.q 是原始的 PromQL 查询字符串。
      • parser.ParseExpr 会将字符串解析为 AST(如 EvalStmt、VectorSelector 等节点)。
      • 如果解析失败,直接返回错误。
      • AST 结构举例:解析后得到的 expr 可能是 *parser.EvalStmt,它包含了表达式本身、起止时间、步长等信息。

3.4、评估查询表达式

解析出 AST 后,下一步就是评估表达式,即根据表达式类型(Vector、Matrix、Scalar、String)调用不同的评估函数,最终得到查询结果。

prometheus 3.4.0 源码片段:

https://github.com/prometheus/prometheus/blob/main/promql/engine.go#L721

// execEvalStmt evaluates the expression of an evaluation statement for the given time range.
// 该函数用于在给定时间范围内评估表达式语句
func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.EvalStmt) (parser.Value, annotations.Annotations, error) {
    // 开始查询准备阶段的计时
    prepareSpanTimer, ctxPrepare := query.stats.GetSpanTimer(ctx, stats.QueryPreparationTime, ng.metrics.queryPrepareTime)
    
    // 获取查询的时间范围(最小和最大时间戳)
    mint, maxt := FindMinMaxTime(s)
    
    // 创建Querier用于查询数据,时间范围为[mint, maxt]
    querier, err := query.queryable.Querier(mint, maxt)
    if err != nil {
        // 如果出错,结束计时并返回错误
        prepareSpanTimer.Finish()
        return nil, nil, err
    }
    // 确保Querier最终会被关闭
    defer querier.Close()

    // 填充系列数据(可能涉及预处理)
    ng.populateSeries(ctxPrepare, querier, s)
    // 结束查询准备阶段的计时
    prepareSpanTimer.Finish()

    // 修改向量和矩阵选择器的偏移量(针对@修饰符)
    // 相对于开始时间,因为只会在它们上执行一次评估
    setOffsetForAtModifier(timeMilliseconds(s.Start), s.Expr)
    
    // 开始内部评估阶段的计时
    evalSpanTimer, ctxInnerEval := query.stats.GetSpanTimer(ctx, stats.InnerEvalTime, ng.metrics.queryInnerEval)
    
    // 情况1:即时评估(开始时间等于结束时间且间隔为0)
    if s.Start.Equal(s.End) && s.Interval == 0 {
        // 将开始时间转换为毫秒时间戳
        start := timeMilliseconds(s.Start)
        
        // 创建评估器,设置各种参数
        evaluator := &evaluator{
            startTimestamp:           start,
            endTimestamp:             start,
            interval:                 1,  // 间隔设为1(因为这是单点评估)
            maxSamples:               ng.maxSamplesPerQuery,
            logger:                   ng.logger,
            lookbackDelta:            s.LookbackDelta,
            samplesStats:             query.sampleStats,
            noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn,
            enableDelayedNameRemoval: ng.enableDelayedNameRemoval,
            enableTypeAndUnitLabels:  ng.enableTypeAndUnitLabels,
            querier:                  querier,
        }
        // 初始化步骤跟踪
        query.sampleStats.InitStepTracking(start, start, 1)

        // 执行表达式评估
        val, warnings, err := evaluator.Eval(ctxInnerEval, s.Expr)
        // 结束评估计时
        evalSpanTimer.Finish()

        if err != nil {
            return nil, warnings, err
        }

        var mat Matrix

        // 处理不同类型的评估结果
        switch result := val.(type) {
        case Matrix:
            mat = result  // 如果是矩阵类型,直接使用
        case String:
            return result, warnings, nil  // 字符串类型直接返回
        default:
            panic(fmt.Errorf("promql.Engine.exec: invalid expression type %q", val.Type()))
        }

        // 将结果存储在查询对象中
        query.matrix = mat
        
        // 根据表达式类型转换结果
        switch s.Expr.Type() {
        case parser.ValueTypeVector:
            // 将矩阵(每个系列一个值)转换为向量
            vector := make(Vector, len(mat))
            for i, s := range mat {
                // 强制将点的时间戳设为评估时间戳
                if len(s.Histograms) > 0 {
                    vector[i] = Sample{Metric: s.Metric, H: s.Histograms[0].H, T: start, DropName: s.DropName}
                } else {
                    vector[i] = Sample{Metric: s.Metric, F: s.Floats[0].F, T: start, DropName: s.DropName}
                }
            }
            return vector, warnings, nil
        case parser.ValueTypeScalar:
            // 标量类型直接返回第一个值
            return Scalar{V: mat[0].Floats[0].F, T: start}, warnings, nil
        case parser.ValueTypeMatrix:
            // 矩阵类型需要排序结果
            ng.sortMatrixResult(ctx, query, mat)
            return mat, warnings, nil
        default:
            panic(fmt.Errorf("promql.Engine.exec: unexpected expression type %q", s.Expr.Type()))
        }
    }

    // 情况2:范围评估(Start != End 或 Interval > 0)
    
    // 创建评估器,设置时间范围和间隔
    evaluator := &evaluator{
        startTimestamp:           timeMilliseconds(s.Start),
        endTimestamp:             timeMilliseconds(s.End),
        interval:                 durationMilliseconds(s.Interval),
        maxSamples:               ng.maxSamplesPerQuery,
        logger:                   ng.logger,
        lookbackDelta:            s.LookbackDelta,
        samplesStats:             query.sampleStats,
        noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn,
        enableDelayedNameRemoval: ng.enableDelayedNameRemoval,
        enableTypeAndUnitLabels:  ng.enableTypeAndUnitLabels,
        querier:                  querier,
    }
    // 初始化步骤跟踪
    query.sampleStats.InitStepTracking(evaluator.startTimestamp, evaluator.endTimestamp, evaluator.interval)
    
    // 执行表达式评估
    val, warnings, err := evaluator.Eval(ctxInnerEval, s.Expr)
    // 结束评估计时
    evalSpanTimer.Finish()

    if err != nil {
        return nil, warnings, err
    }

    // 检查结果是否为矩阵类型
    mat, ok := val.(Matrix)
    if !ok {
        panic(fmt.Errorf("promql.Engine.exec: invalid expression type %q", val.Type()))
    }
    // 将结果存储在查询对象中
    query.matrix = mat

    // 检查上下文是否已完成
    if err := contextDone(ctx, "expression evaluation"); err != nil {
        return nil, warnings, err
    }

    // TODO(fabxc): 确保指标标签是存储内部结构的副本
    
    // 对矩阵结果进行排序
    ng.sortMatrixResult(ctx, query, mat)

    // 返回矩阵结果和警告
    return mat, warnings, nil
} 

关键点说明:

      • evaluator.Eval(ctx, s.Expr) 是表达式评估的核心入口。
      • Eval 方法会根据 AST 节点类型递归调用不同的评估函数。
      • 例如:
        • 如果是 VectorSelector,就去底层存储取样本。
        • 如果是 BinaryExpr,就递归评估左右子树再做运算。
        • 如果是 MatrixSelector,就取一段时间的样本。 

Eval 方法源码片段(https://github.com/prometheus/prometheus/blob/main/promql/engine.go#L1124)

// Eval evaluates the given expression within the evaluator's context.
// It handles panics, evaluates the expression, and optionally cleans up metric labels.
// 参数:
//   ctx - 上下文,用于控制超时和取消
//   expr - 要评估的表达式节点
// 返回值:
//   v - 评估结果值
//   ws - 评估过程中产生的警告(annotations)
//   err - 评估过程中产生的错误
func (ev *evaluator) Eval(ctx context.Context, expr parser.Expr) (v parser.Value, ws annotations.Annotations, err error) {
    // 设置延迟恢复(recover)机制,捕获eval过程中可能发生的panic
    // 将panic转换为错误返回,避免进程崩溃
    defer ev.recover(expr, &ws, &err)

    // 实际执行表达式评估的核心方法
    // 返回结果值(v)和警告信息(ws)
    v, ws = ev.eval(ctx, expr)

    // 如果启用了延迟名称移除功能(experimental feature)
    // 则对结果中的指标标签进行清理
    if ev.enableDelayedNameRemoval {
        ev.cleanupMetricLabels(v)
    }

    // 返回结果值、警告信息(nil错误,因为panic已被recover处理)
    return v, ws, nil
}

关键点说明:
1、错误处理:

    • 使用 defer ev.recover() 捕获可能的 panic,将其转换为错误返回
    • 这是 Prometheus 查询引擎的防御性编程实践,防止单个查询错误影响整个系统

2、核心评估:

    • 实际评估工作委托给 ev.eval() 方法
    • 该方法会递归地处理表达式树(parser.Expr)

3、实验性功能:

    • enableDelayedNameRemoval 是一个实验性标志,控制是否在评估后清理指标标签
    • 清理操作通过 cleanupMetricLabels() 方法实现

4、返回值:

    • 总是返回 nil 错误,因为任何 panic 都已被 recover 处理
    • 警告信息(annotations)会原样返回,这些通常是非致命性的问题

5、设计模式:

    • 使用了典型的 "error handling wrapper" 模式
    • 主逻辑在 eval() 中实现,外围处理错误和特殊功能
posted @ 2025-05-21 18:54  左扬  阅读(69)  评论(0)    收藏  举报