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}
}
-
- 参数解析:从 HTTP 请求中解析查询所需的参数,包括起始时间、结束时间、步长、超时时间和限制数量等,并进行有效性检查。
- 上下文处理:根据请求中的超时参数设置查询的超时时间,确保查询不会无限期执行。
- 查询创建:使用查询引擎创建一个范围查询对象,传入解析后的参数。
- 查询执行:执行范围查询,并处理可能出现的错误。
- 结果处理:如果设置了结果限制,对查询结果进行截断处理,并添加相应的警告信息。
- 统计信息:根据请求中的
stats参数,决定是否在响应中包含查询统计信息。 - 结果返回:将查询结果、统计信息和警告信息封装在
apiFuncResult中返回。
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(非常重要):
- 即时查询 /api/v1/query:(原理)即时查询用于获取某个特定时间点的数据。它接收一个 PromQL 查询表达式和一个时间戳作为输入,然后在该时间戳上执行查询,并返回查询结果。
- 范围查询 /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
}
3.4、创建可执行范围查询对象——Engine.NewRangeQuery()
在确定了时间范围之后,Prometheus 使用 Engine.NewRangeQuery 方法创建一个 Query 接口实例,它是对外暴露的标准接口方法。这个方法在 https://github.com/prometheus/prometheus/blob/v3.4.0/promql/engine.go#L499 中定义,其主要作用是对查询进行初始化,包括解析查询表达式、设置查询的起始时间、结束时间和步长等。
// 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)见如下:
- 构建 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}
}
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
}
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() 中实现,外围处理错误和特殊功能

浙公网安备 33010602011771号