
从 Trace 到时间序列Grafana Tempo TraceQL Metrics 指标查询的设计与实现【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo导读TraceQL 指标查询TraceQL Metrics是 Grafana Tempo 在 TraceQL 语言基础上扩展出的时序指标能力它让用户可以像 LogQL 从日志中算指标那样直接从 Trace 数据中聚合出错误率、请求速率、P95 延迟等时间序列而无需引入独立的 metrics-generator 派生指标管线。本文以 Tempo 仓库中的设计提案 docs/design-proposals/2023-11 TraceQL Metrics.md 为主体结合仓库源码逐层剖析其Span 查询 指标管线的核心模型、六个第一阶段的_over_time聚合函数、分组与过滤、算术运算、与 PromQL/LogQL 的设计差异以及查询 API 与前端配置等工程化落地细节。读完本文你将能够理解并编写{statuserror} | rate() by (resource.service.name)这类查询背后的完整执行链路并知道如何在 Tempo 中配置与调优指标查询。一、为什么要做 TraceQL 指标查询Tempo 的常规 TraceQL 查询解决的是找到匹配的 Span这类问题输入一段 Span 查询返回一批独立的 Trace 数据点时间、耗时、服务名、状态码等。但在可观测性实践中用户常常还需要从这些 Trace 里算出指标例如某个服务在最近 1 小时内的错误率随时间的变化某个接口的 P95 延迟曲线某个服务的每秒请求数是否超过阈值。设计提案 docs/design-proposals/2023-11 TraceQL Metrics.md下称提案提出的解决方案是在语言层面为 TraceQL 增加指标管线阶段metrics pipeline stages让一个常规 TraceQL 查询的返回结果继续被聚合为时间序列。这样既不需要预先为每个维度组合落盘指标也能复用 Tempo 已有的 Trace 存储与查询能力。从源码结构看该能力已经完整落地于pkg/traceql包词法器 pkg/traceql/lexer.go 注册了rate、count_over_time、min_over_time、max_over_time、avg_over_time、sum_over_time、quantile_over_time、histogram_over_time、compare、topk、bottomk等关键字语法文件 pkg/traceql/expr.y 定义了完整的指标聚合语法规则执行引擎 pkg/traceql/engine_metrics.go 与 pkg/traceql/ast_metrics.go 实现了从 Span 观测到时间序列输出的全链路。二、核心模型Span 查询 指标管线提案给出的核心结构非常简洁span query... | metrics query...前半段是任意合法的 TraceQL Span 查询负责找到匹配的 Span后半段是指标管线阶段负责把匹配到的 Span 按时间聚合为时间序列。两者通过管道符|连接。一个重要推论是任何合法的 TraceQL 查询都可以被改写成指标查询。提案中的示例是在 Span 查询中使用结构化关系运算符后接rate(){resource.service.nameA} {resource.service.nameB} | rate()这条查询先找出由服务 A 发起、指向服务 B的调用 Span再对这些 Span 计算每秒调用速率——即服务 A 对服务 B 的调用速率。在源码层面这个模型体现为 pkg/traceql/ast_metrics.go 中定义的三类处理器接口spanProcessor处理单个 Span 观测第一阶段直接把 Span 聚合成原始时间序列seriesProcessor处理预聚合的时间序列用于跨 job/pod 合并结果secondStageElement对第一阶段产出的 SeriesSet 做二次加工topk/bottomk、比较过滤、算术等。与之配套的AggregateMode枚举见 pkg/traceql/enum_aggregates.go把指标查询的执行分为三档模式含义典型场景AggregateModeRaw直接在 Span 上运行产出第一层原始时间序列每个执行 job/分片内部AggregateModeSum对多个 job/pod 的中间结果做合并rate/count 是简单相加min/max 再做一层 min/max分布式执行时汇总分片AggregateModeFinal必须在单点完成的最终计算分位数、平均值等前端最终聚合这种Raw → Sum → Final的分级设计正是为了支撑指标查询在多分片/多 job 下的大规模并行执行。三、第一阶段把 Span 变成时间序列第一阶段的职责是把 Span 变成时间序列这是 Trace 数据与指标数据唯一的交汇点。提案用_over_time后缀区分这些专用聚合函数与其如 PromQL 中的同名概念。3.1 聚合函数一览提案给出的第一阶段函数如下函数说明rate()每秒的 Span 速率count_over_time()Span 的总计数avg_over_time(field)数值字段的平均值如duration或http.request.body.size等语义约定属性max_over_time(field)数值字段的最大值min_over_time(field)数值字段的最小值quantile_over_time(field, q1, q2, ...)数值字段的分位数如 P95。可一次请求多个分位数每个分位数生成一条时间序列在仓库的最终实现中函数集合进一步扩充MetricsAggregateOp枚举pkg/traceql/enum_aggregates.go还包括了提案未列出的sum_over_time与histogram_over_time。语法规则pkg/traceql/expr.y 第 330-349 行完整定义了这些函数的形参rate()、count_over_time()无参数min_over_time(attr)、max_over_time(attr)、sum_over_time(attr)、avg_over_time(attr)一个属性参数quantile_over_time(attr, numericList)属性 一个或多个分位数histogram_over_time(attr)一个属性参数内部用对数桶实现。3.2 源码视角聚合器如何工作第一阶段的执行核心在 pkg/traceql/engine_metrics.goCountOverTimeAggregator统计 Span 数量NewRateAggregator(rateMult)复用同一结构在Sample()时乘上rateMult 1.0 / step 秒数从而把每个时间桶的计数换算成每秒速率见 pkg/traceql/ast_metrics.go 第 223-227 行OverTimeAggregator针对某个属性执行 min/max/sum 聚合对duration等内在字段或任意属性使用FloatizeAttribute提取数值StepAggregator按查询的 step 把时间窗切成多个桶每个桶内跑一个VectorAggregator从而产出每个时间点一个值的序列InstantAggregator用于瞬时查询instant query只有一个桶输出单个数据点。分组by()则由GroupingAggregator实现它按分组属性组合把 Span 路由到不同系列并为每个系列维护独立的内部聚合器未分组时退化为UngroupedAggregator输出单条无标签序列并自动补一个__name__rate之类的指标名标签与 Prometheus 行为对齐。3.3 关于 Interval聚合间隔的演进说明提案设想所有聚合函数都接受一个可选的 interval 参数如rate(5m)、quantile_over_time(duration, 0.95, 5m)未指定时自动匹配查询的 step interval。需要注意从当前仓库语法pkg/traceql/expr.y看这些聚合函数的括号内并未接收独立的 interval 参数——输出的时间分辨率统一由查询范围的step参数决定。这与提案 Notes 中Step Interval一节的思想一脉相承step 是查询的显式分辨率step1m意味着每 60 秒返回一个数据点。提案举例说明 step 与聚合 interval 是两回事rate(1h)配合step1m仍然每 60 秒返回一个点但每个点是前 1 小时窗口内平滑后的速率。在实现中该语义收敛为StepAggregator按 step 分桶、rate用 step 换算每秒速率而滑动窗口平滑这类更复杂的窗口语义属于后续演进方向。Tempo 还提供了自动步长选择逻辑DefaultQueryRangeSteppkg/traceql/engine_metrics.go按时间窗长度尽量取约 240 个数据点步长在 1 小时、5 分钟、1 分钟、15 秒、5 秒、1 秒等粒度中向下取整时间窗小于 1 分钟时则允许毫秒级步长最小 50ms。3.4 分组Grouping所有第一阶段聚合都支持by (attr1, attr2, ...)按一个或多个属性分组为每种属性值组合生成一条独立时间序列。提案中的两个示例{ span.http.path /myapi } | rate() by (span.user_id, span.http.status_code)按用户 ID 和 HTTP 状态码绘制/myapi的请求速率。{ resource.service.name myservice } | quantile_over_time(duration, 0.95) by (span.http.path)按 HTTP 路径绘制myservice的 P95 延迟。从源码看分组有几个值得注意的实现细节分组数量上限maxGroupBys 5pkg/traceql/engine_metrics.go 第 708 行MetricsAggregate.validate()会拒绝超过上限的分组其中quantile_over_time/histogram_over_time内部需要为桶标签预留一个槽位因此分组数上限再减 1见 pkg/traceql/ast_metrics.go 的validate()。无作用域属性的双端查找未限定作用域的属性不带span./resource.前缀会先查 span 级再查 resource 级。nil 值的处理分组值缺失时对应标签会被丢弃若全部为 nil则强制补一个值为nil的标签避免下游 Prometheus 侧因零标签出错。3.5 过滤Filtering聚合结果可以继续接比较运算符来过滤输出。提案示例{ } | rate() by (resource.service.name) 1000只保留速率超过 1000 req/s 的服务序列。这在源码中由MetricsFilter实现pkg/traceql/ast_metrics.go 第 540-630 行它把不满足条件的数据点置为 NaN若整条序列过滤后全为 NaN 则整条丢弃支持的运算符包括、、、、、!validate()会拒绝其他运算符。另外过滤时还会同步过滤掉不满足条件的 Exemplar保证下游展示的示例点与曲线语义一致。四、附加阶段Additional Stages对时间序列再加工第一阶段之后还可以继续追加指标阶段对时间序列做进一步变换。提案强调这些阶段接收时间序列并产出新的时间序列因此与第一阶段不同——它们本质上是在每个时间点上跨输入工作的瞬时函数没有自己的 interval也不会改变数据点的频率如果原聚合每 60 秒产出一个点这些阶段同样每 60 秒输出一个点。提案给出的函数表函数说明... \| max() [by(...)]每个时间点上的最大值... \| min() [by(...)]每个时间点上的最小值... \| avg() [by(...)]每个时间点上的平均值... \| stddev() [by(...)]每个时间点上的标准差... \| quantile(q) [by(...)]每个时间点上的分位数... \| topk(N)返回每个时间点上的前 N 条序列分组方式待定提案示例——找出每个集群内单 Pod 故障率最高的那条{ status error } | rate() by (cluster, pod) | max() by (cluster)当前仓库已经落地了其中一类重要操作topk(N)与bottomk(N)。语法规则pkg/traceql/expr.y 第 356-358 行定义了TOPK OPEN_PARENS INTEGER CLOSE_PARENS实现类TopKBottomKpkg/traceql/ast_metrics.go要求limit 0按序列总和对候选排序后取前 N/后 N 条。多个第二阶段元素可以链式组合ChainedSecondStage依次执行例如{statuserror} | rate() | topk(5) 10这种先取 TopK、再过滤的组合是合法的。五、算术运算算错误率算术运算是 TraceQL 指标查询最实用的能力之一两个时间序列之间做*、/、、-从而把错误请求速率 / 总请求速率这类比值算成错误率。提案中的 5xx 错误率示例({ span.http.path /myapi span.http.status_code 500 } | rate()) / ({ span.http.path /myapi | rate())分子是满足路径为 /myapi 且状态码 500的 Span 速率分母是 /myapi 的全部请求速率两者逐时间点相除即得 5xx 错误率曲线。该能力在源码中由 pkg/traceql/ast_metrics_math.go 实现MathExpression以二叉树结构表示(A) op (B)叶子节点通过内部的__query_fragment标签从共享的 SeriesSet 中提取属于自己子查询的时间序列applyBinaryOp按标签匹配左右两侧的序列逐点执行运算applyArithmeticOp中除法遇到除数为 0 时输出 NaNMetricsScalarOp额外支持时间序列与标量常量之间的运算例如100 * ({} | rate())或({} | rate()) / 1000标量可在左也可在右。运算结果的指标名会被合并为形如(a / b)的形式标签合并时会去重并跳过指标名标签与 Prometheus 的向量匹配语义对齐。提案也指出系列与系列之间算术需要明确定义join 语义参考 PromQL 的 vector-matching当前实现采取按标签精确匹配、无标签序列可扇出fan-out到所有序列的默认行为并预留了on()、group_left()等 Prometheus 式修饰符作为后续演进方向。六、与 PromQL/LogQL 的设计差异提案用专门一节阐述了为什么选择管线式语法 专用聚合函数而不是照搬 PromQL/LogQL。这些取舍直接塑造了如今的语言形态。6.1 标签Labels没有流stream只有一片 SpanTempo 的底层架构与 Prometheus/Loki 不同后者天然存在流/序列概念标签由埋点与指标管线共同定义而 Tempo 中没有流定义只有大量离散的 Span。提案用一个例子说明问题要按服务绘制速率PromQL 式写法是sum(rate({}[5m])) by (resource.service.name)但rate({}[5m])在 PromQL 语义下返回的是每个流一条序列流的标签从何而来在 Tempo 中只有两种可能返回一条无标签的总速率除非显式给出分组标签——这正是rate() by(...)语法的由来用可用数据resource/span 属性模拟流——但这不可行因为没有规则决定该选哪些属性全选会带来灾难性的高基数一个普通 HTTP 请求的 path、method、status_code 适合做流定义但 content length、headers、带 query 参数的完整 URL 会令基数失控。结论很直白{} | rate()在方案 1 下是一条时间序列在方案 2 下可能是数十亿条。6.2 Rate没有 range vectorPromQL/LogQL 的速率依赖 range vector selector如metric[5m]。这在 Tempo 中无法平移{}合法找 Span但{}[5m]在缺少聚合时没有意义。于是 interval 被简化成聚合函数的一个参数在实现中进一步统一为查询级 step而不是专门语法。6.3 管线Pipeline单向流动易于增删提案选择管线语法而非函数嵌套语法理由有三计算单向流动更容易理解修改更容易——增删一个阶段即可而无需小心翼翼地增删嵌套函数及其括号把找 Span 的现有查询升级为指标查询只需在管道末尾追加一个阶段改动集中在一处。例如把{statuserror}变成错误率曲线只需追加| rate()原查询完全不动。七、工程化落地API、查询参数与配置7.1 查询 API 与参数TraceQL 指标查询通过指标查询端点暴露前端 HTTP 处理器位于 modules/frontend/metrics_query_handler.go/api/metrics/query_range范围查询返回一段时间内的序列/api/metrics/query_range_instant及 query_range 单步形态瞬时查询内部会被改写成单步的 query_range 请求。请求解析在 pkg/api/http.go 的ParseQueryRangeRequest中完成主要参数包括参数说明qTraceQL 指标查询语句如{statuserror} \| rate() by (resource.service.name)start/end查询时间范围unix 时间戳step步长如1m决定输出数据点频率不传时由 Tempo 按范围自动计算exemplars是否在结果中携带 Exemplar值得注意范围查询的start/end会被AlignRequest对齐到 step 的整数倍使得最近 1 小时这类查询在每次刷新时得到稳定一致的时间分桶。序列输出时SeriesSet.ToProto会自动跳过 NaN 数据点与空序列Exemplar 也会做严格的时间桶边界校验见 pkg/traceql/engine_metrics.go 的ToProto。7.2 前端配置与调优指标查询是计算密集型的官方配置文档 docs/sources/tempo/metrics-from-traces/metrics-queries/configure-traceql-metrics.md 给出了工程实践建议超时这类查询可能耗时较长需要检查链路各处的超时——Grafana 前的代理、指向 Tempo 的 Prometheus 数据源、以及 Tempo 配置中的querier.search.query_timeout、server.http_server_read_timeout、server.http_server_write_timeout。query_frontend.metrics配置块控制所有 TraceQL 指标查询的执行参数concurrent_jobs并发执行 job 数target_bytes_per_job单个 job 的目标数据量字节例如2.25e08约 225MB或1.25e09约 1.25GBintervaljob 切分的时间间隔max_duration指标查询允许的最大时间范围默认 24 小时注意这与常规 TraceQL 查询默认 168 小时/7 天不同query_backend_after查询后端存储与 live-store 的分界默认15m比该值更老的数据只查对象存储/后端更新的数据查 live-store。query_frontend: metrics: concurrent_jobs: 1000 target_bytes_per_job: 2.25e08 # ~225MB interval: 30m0s文档给出的经验法则是云环境适合更多更小的 job高并发、小数据量本地部署on-prem则相反可降低并发并增大 job 体积以获得更好的查询吞吐。7.3 采样与性能大规模数据集上指标查询还支持采样提示sampling hints每个 Span 在写入时会携带采样倍数信息源码中的IntrinsicSpanMultiplier即 1/采样概率聚合器在计数/求和时按该倍数外推SetExtrapolate从而在采样数据上依然得到接近真实总量的指标而 min/max 等极值聚合不会被外推缩放极值不随采样成比例放大。相关实现在 pkg/traceql/engine_metrics.go 的spanExtrapolation与CountOverTimeAggregator/OverTimeAggregator中。启用采样后单 job 处理时间缩短可以支撑更高的并发。八、未来方向提案展望了两个演进方向其中部分已被实现或部分落地8.1 时间序列间的 Join算术运算引出了 join 语义的需求。当前实现采取标签精确匹配 无标签序列扇出的默认行为后续计划提供与 PromQL vector-matching 相当的能力并支持 one-to-many / many-to-one 等专门化场景。8.2 单查询多指标在同一查询里计算多个指标如同时画错误率与总速率是常见诉求Tempo 只需一次扫描即可算完更高效。quantile_over_time已迈出第一步一次请求可算 P50 和 P95 两条序列更完整的方案需要 pipeline-splitting 之类的高层构造。源码中batchSeriesProcessor按__query_fragment内部标签把序列路由到不同子处理器已经为一条查询包含多个算术分支提供了基础设施可以视为这一方向的初步落地。8.3 额外提及compare()除提案内容外仓库还实现了compare()函数pkg/traceql/engine_metrics_compare.gocompare(spansetFilter, [topN], [start, end])对任意属性的取值做基线 vs 选择的对比输出带__meta_typebaseline/selection 及其 total标签的系列并对超出topN上限 1000的基数打上__too_many_values__错误标记——这体现了 TraceQL 指标家族持续扩展的态势。结语TraceQL Metrics 把查询 Trace与计算指标统一进了同一种语言前半段沿用强大的 Span 查询能力筛选数据后半段通过_over_time系列聚合函数、by()分组、比较过滤、二阶变换与序列算术把匹配到的 Span 在线聚合成错误率、速率、分位数延迟等时间序列。整套能力已在 Tempo 的pkg/traceql包中完整落地并配套了/api/metrics/query_range查询端点与query_frontend.metrics配置块配合采样外推与自动步长等机制为大规模 Trace 数据上的指标观测提供了一条无需预写指标的便捷路径。想要深入探索的读者可以从 pkg/traceql/ast_metrics.go 与 pkg/traceql/engine_metrics.go 的聚合器实现入手再到 pkg/traceql/engine_metrics_test.go含 avg 加权均值等数值精度测试与 modules/frontend/metrics_query_handler_test.go 中查看完整的测试用例理解每一类聚合在边界条件下的行为。【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考