ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Tempo TraceQL 引擎架构解析:从查询解析到存储层执行的完整链路

2026/9/18 21:28:35 拓冰建站 浏览量
Tempo TraceQL 引擎架构解析:从查询解析到存储层执行的完整链路 Tempo TraceQL 引擎架构解析从查询解析到存储层执行的完整链路【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempoGrafana Tempo 的 TraceQL 引擎是连接 Tempo API 处理器与存储层之间的核心桥梁它负责把用户书写的 TraceQL 查询解析为存储层可执行的扁平化条件从存储层拉取候选 spanset 后逐条重新验证最终组装并返回搜索结果。本文基于 Tempo 仓库中的架构文档与pkg/traceql、tempodb/encoding/vparquet4等模块源码完整还原 TraceQL 引擎的三个核心职责、两阶段执行模型、spanset 抽象以及存储层落地细节帮助你理解一次 TraceQL 搜索请求从 API 到 Parquet 数据块的完整执行链路。TraceQL 引擎在 Tempo 中的定位在 Tempo 的整体架构中TraceQL 引擎TraceQL Engine是连接Tempo API handler与存储层storage layer的中间组件。它在一次搜索请求中承担以下三项核心职责解析Parse传入的查询请求将其提取为存储层可以直接处理的扁平化条件flattened conditions从存储层拉取 spanset并对每一个拉取到的 spanset 使用完整的查询表达式重新验证revalidate确认其确实匹配该查询将验证通过的结果组装为搜索响应search response返回给上层调用方。从源码结构看这一设计对应的核心类型与文件分别是pkg/traceql/engine.goEngine类型与Compile、ExecuteSearch等入口函数承载解析、编译与两阶段执行编排pkg/traceql/storage.go定义FetchSpansRequest、Condition、Spanset、SpansetFetcher等引擎与存储层之间的契约接口pkg/traceql/ast_conditions.go实现从 AST抽象语法树到扁平化条件的提取逻辑tempodb/encoding/vparquet4/block_traceql.govParquet4 数据块上对FetchSpansRequest的落地实现。为什么需要 TraceQL默认搜索与精确查询的对比Tempo 的默认搜索default search会扫描整条 trace 的全部内容而 TraceQL 提供了一种构造精确查询的方法让你能够快速缩小到所需的数据范围。由于查询条件限制了被扫描的数据量TraceQL 查询通常能更快返回结果。例如默认搜索可能需要遍历块中所有 span 的元数据才能判断一条 trace 是否命中而一条{ .http.status 200 }这样的 TraceQL 查询可以在存储层通过 Parquet 列式剪枝column pruning直接跳过大量不相关的行组row groups只针对包含该属性的列做谓词过滤从而显著减少被检查的数据量关于列级下推的具体实现可参见 vParquet4 的 Fetch 实现。引擎执行链路全景一次 TraceQL 搜索请求在引擎中的完整路径可以概括为查询字符串如 { .http.status 200 } │ ▼ Parse词法/语法分析pkg/traceql/parse.go、expr.y │ ▼ validateAST 语义校验pkg/traceql/ast_validate.go │ ▼ extractConditionsAST → []Conditionpkg/traceql/ast_conditions.go │ ▼ FetchSpansRequestStartTime/EndTime Conditions SecondPass │ ▼ 存储层 Fetch如 vparquet4 块级 Fetch列下推 行组跳过 │ ▼ 迭代器逐个产出 Spanset → 引擎 SecondPass 回调重新验证 │ ▼ MetadataCombiner 合并结果 → tempopb.SearchResponse第一步解析与编译Parse Compile引擎的入口是traceql.Compile函数见 pkg/traceql/engine.go其流程如下func Compile(query string, opts ...CompileOption) (*RootExpr, Pipeline, SpansetFilterFunc, *FetchSpansRequest, error) { expr, err : Parse(query, opts...) // 词法与语法分析 if err ! nil { ... } err expr.validate() // 语义校验如操作数类型、运算符合法性 ... p, ok : expr.SinglePipeline() // 单管道提取不支持数学表达式时返回错误 ... req : FetchSpansRequest{AllConditions: true} requests : expr.extractConditions(req) // AST → 扁平化条件 ... return expr, p, p.evaluate, req, nil }Parse通过 parse.go 与 goyacc 生成的 expr.y.go 将查询字符串解析为 ASTRootExpr、Pipeline、SpansetFilter、BinaryOperation等节点validate对 AST 做语义校验ast_validate.goextractConditions遍历 AST把每个条件表达式转换为存储层可执行的Condition列表详见下一节返回的Pipeline及其evaluate函数将作为第二步重新验证阶段的过滤回调。另外CompileFetchSpanRequests 则面向支持多个子查询的场景如指标查询中的数学表达式返回按子查询划分的map[string]FetchSpansRequest。第二步AST 到扁平化条件的提取扁平化条件是指把嵌套的 TraceQL 表达式拆解为一系列Condition属性 运算符 操作数存储层可以针对每个Condition直接构造列级谓词。核心逻辑位于 pkg/traceql/ast_conditions.go其提取规则要点如下属性比较属性当二元运算两侧都是属性如parent.service.name ! .service.name时两边都会被提取为OpNone条件即只要读出这两列即可真正的比较留给引擎执行属性比较静态值当一侧是静态值如.http.status 200时会提取为带运算符和操作数的完整条件{Attribute: .http.status, Op: OpGreaterEqual, Operands: [200]}这样存储层就能用该条件构建谓词布尔运算遇到OpOr时request.AllConditions会被置为 false表示满足任一条件即可反之会保持AllConditions true让存储层可以做更激进的优化见 ast_conditions.goSelect 操作SelectOperation.extractConditions会把 select 子句中引用的属性放入SecondPassConditions由第二遍执行时读取。Condition结构体storage.go还带有CallBack字段可由上层 watcher例如引擎字节跟踪在列扫描过程中提前终止无用的列读取。第三步两阶段执行模型First Pass Second PassFetchSpansRequestpkg/traceql/storage.go是引擎与存储层之间最重要的契约它包含字段作用StartTimeUnixNanos/EndTimeUnixNanos查询的时间范围由搜索请求的Start/End转换而来Conditions第一遍扫描的扁平化条件AllConditions提示存储层是否可以优化为只返回满足所有条件的 spanset对应{ a b c }场景TraceSampler/SpanSampler采样器可选存储层可以不生效SecondPass/SecondPassConditions第二遍执行的过滤回调与需要额外读取的条件SecondPassSelectAll忽略第二遍条件、读取全部属性的开关这种设计的关键动机是分两遍读取数据第一遍存储层只按Conditions读取解析 TraceQL 表达式真正需要的列快速筛选出候选 spanset第二遍引擎通过SecondPass回调即编译得到的p.evaluate对每个候选 spanset 执行完整的表达式求值只有真正匹配的 spanset 才保留同时通过SecondPassConditions如SearchMetaConditions中的 trace 根服务名、trace 名、trace ID 等元数据列补齐搜索结果所需的元信息。这一机制在ExecuteSearch中体现得十分清晰pkg/traceql/engine.go引擎把求值函数注册为SecondPass对存储层产出的每个非空 spanset 执行eval([]*Spanset{inSS})结果为空则丢弃否则还会把每个 spanset 截断到SpansPerSpanSet默认 3见DefaultSpansPerSpanSet个 span 以减少后续元数据查询的开销并记录attributeMatched__matched属性统计命中 span 数。Spanset 抽象引擎与存储层协作的最小单元TraceQL 的语义是以 trace 为单位做 spanset 选择与管道处理因此引擎与存储层之间传递的核心数据结构是Spansetpkg/traceql/storage.gotype Spanset struct { Scalar Static Spans []Span TraceID []byte RootSpanName string RootServiceName string StartTimeUnixNanos uint64 DurationNanos uint64 ServiceStats map[string]ServiceStats Attributes []*SpansetAttribute ReleaseFn func(*Spanset) }一个 spanset 对应一条 trace 中被挑选出来的一组 span携带 trace 级元数据如根 span 名、根服务名、时长、各服务的 span 数/错误数Span接口storage.go抽象了 span 的取属性能力AttributeFor、AllAttributes、时间/时长以及结构关系运算SiblingOf、DescendantOf、ChildOf后者正是 TraceQL 结构运算符、、~得以在引擎层实现的根基ReleaseFn用于内存回收引擎消费完 spanset 后调用Release()归还内存。存储层通过SpansetFetcher接口Fetch/FetchSpans向引擎提供数据FetchSpansResponse除返回SpansetIterator外还携带Stats回调用于汇报FetchSpansStats扫描字节数、检查/跳过的行组与页数、按缓存角色区分的命中与未命中、后端读取等最终汇总进tempopb.SearchMetrics作为查询吞吐与 SLO 指标的依据见 pkg/traceql/engine.go。存储层落地vParquet 块上的 Fetch 实现引擎产出的FetchSpansRequest最终由存储层执行。以 vParquet4 为例backendBlock.Fetchtempodb/encoding/vparquet4/block_traceql.go的执行要点包括条件校验checkConditions检查每个Condition的操作数数量是否与运算符匹配、所有操作数类型是否一致、操作数类型是否与操作兼容例如OpNone/OpExists必须为 0 个操作数比较类运算符必须恰好 1 个操作数同时拦截当前编码不支持的固有属性如 vParquet4 不支持childCount固有属性会返回util.ErrUnsupported条件合并coalesceConditions合并语义等价的条件减少重复列扫描打开 Parquet 文件并选择行组通过openForSearch与rowGroupsFromFile拿到文件与行组结合元数据中的专用列DedicatedColumns信息构建迭代器fetch基于 parquetquery 构造列级迭代器利用谓词在读取过程中跳过不匹配的行组与数据页返回带统计的响应Stats回调汇报实际读取字节数供引擎填充搜索指标。类似实现同样存在于 vParquet5tempodb/encoding/vparquet5/不同编码版本会在支持的特性上存在差异例如固有属性的支持范围引擎在遇到util.ErrUnsupported时会优雅降级返回空结果而非报错见 engine.go。引擎与前端搜索分片器的配合在前端query-frontend侧TraceQL 引擎与搜索分片器Search Sharder协同工作modules/frontend/frontend.go中将newAsyncSearchSharder挂载到搜索请求的中间件链上负责把一次大范围搜索按时间/数据分片shard拆分为多个并行的子请求分片权重计算则通过traceql.CompileFetchSpanRequests解析查询后按提取出的子查询条件评估各分片的数据量权重见 modules/frontend/pipeline/async_weight_middleware.go。此外modules/frontend/tracefilter/filter.go 使用traceql.CompileSpansetFilter将 TraceQL 查询编译为 spanset 过滤器用于 trace-by-id 场景下的细粒度过滤。分阶段开发与当前限制TraceQL 语言与引擎是分阶段phases实现的。根据架构文档的说明引擎的初始迭代版本initial iteration聚焦于 spanset 选择spanset selection与管道pipelines两个能力spanset selection通过{}中的条件表达式从 trace 中挑选 span 集合pipeline通过|将多个表达式串联逐级传递与加工 spanset。从当前源码可以确认这两项能力已经在 pkg/traceql/ast.goAST 定义、pkg/traceql/ast_execute.go管道求值、pkg/traceql/spanset_filter_match.gospanset 过滤匹配中完整落地后续迭代又逐步加入了聚合函数、指标查询engine_metrics.go、数学表达式ast_metrics_math.go等能力。关于 TraceQL 的完整设计理念与后续扩展方向可参考仓库内的两份设计提案TraceQL Concepts2022-04 设计提案阐述语言的基本结构——基于 span 属性、时序与时长、结构关系以及聚合数据来选择 trace并给出{ .http.status 200 }、{ duration 2s }、{ } { }后代、{ } { }子、{ } ~ { }兄弟、count() 10、avg(duration) 1s、by(.region) | count() 5等语法示例TraceQL Extensions2023-11 设计提案记录 TraceQL 面向指标查询等方向的扩展设计。需要说明的是仓库中的 architecture.md 已被标记为 draft 并隐藏页面注释标明very out of date因此本文以当前源码实现为准进行论述上文所述的分阶段开发信息属于该文档保留的历史性说明。小结TraceQL 引擎通过解析提取扁平化条件 → 存储层列下推预筛选 → spanset 级重新验证 → 元数据合并的流水线设计把精确的查询语义下沉到 Parquet 列式存储中既保证了查询结果的准确性又借助列剪枝与行组跳过显著减少了被扫描的数据量。理解这条链路是排查 TraceQL 查询性能、理解引擎指标inspected bytes、spansets evaluated 等以及为 Tempo 贡献新查询能力的前提。若需进一步学习查询语法可参阅 构造 TraceQL 查询 与 调整 TraceQL 查询性能 等文档。【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考