上一章节我们通过对Query Frontend组件进行debug了解各middleWare的加载和调用顺序,对在Query Frontend里middleWare的层层包含进行了拆解,了解其运行机制,这一节将按照请求进入后是如何在各middleWare间传递的,每个middleWare的作用是什么来阅读Query Frontend组件的源码。
initQueryFrontend()
在初始化Query Frontend组件时,initQueryFrontend
函数将对traceByIDHandler
进行定义,且可以看到traceByIDHandler
被t.HTTPAuthMiddleware
、httpGzipMiddleware()
中间件封装了一次middleware.Merge的处理。
一次Wrap操作封装又传入了queryFrontend.TraceByIDHandler
。

那么,当/api/traces/
请求进入Query Frontend组件时,会依次调用t.HTTPAuthMiddleware
、httpGzipMiddleware()
、queryFrontend.TraceByIDHandler
。
HTTPAuthMiddleware
在启动程序加载middleware时,会根据是否开启多租户配置,确定AuthMiddleware
的具体实现。

如果是多租户模式,则获取租户ID。如果为非多租户模式,则注入key为orgIDContextKey
,value 为 "single-tenant"的键值对至Context。
httpGzipMiddleware()
httpGzipMiddleware是通过github.com/klauspost/compress/tree/master/gzhttp
实现的对传输包进行解压缩,在Github上有该库的介绍,gzhttp
将以比标准库更快的速度进行gzip解压缩。

TraceByIDHandler
TraceByIDHandler
是Query Frontend结构的一个属性,在创建QueryFrontend
对象时进行构造。
在QueryFrontend
的New
方法里构造TraceByIDHandler
的过程可以看到再一次被MergeMiddlewares、Wrap函数的封装。

按照在上面对middleware的讲解过程,可以得出这里的调用顺序依次为:
1. newHandler.ServeHTTP
2. newTraceByIDMiddleware(cfg, o, logger)
3. retryWare
newTraceByIDMiddleware
在newTraceByIDMiddleware
的定义中可以看到会执行rt.RoundTrip(r)
,而rt由NewRoundTripper()
构造而来。

在NewRoundTripper的实现里又看到了熟悉的函数MergeMiddlewares
,可以看到rt是由多个middlerware进行MergeMiddlewares
操作,然后执行next返回的。

那么这个NewRoundTripper将会按照顺序依次执行:
1. newDeduper(logger)
2. newTraceByIDSharder(&cfg.TraceByID, o, logger)
3. newHedgedRequestWare(cfg.TraceByID.Hedging)
最后执行Next.
TraceByIDHandler封装的所有middleware
到这里TraceByIDHandler所有经过的middleware的执行顺序可以确定:
1. newHandler.ServeHTTP
2. newDeduper(logger)
3. newTraceByIDSharder(&cfg.TraceByID, o, logger)
4. newHedgedRequestWare(cfg.TraceByID.Hedging)
5. retryWare
这些中间件的处理过后会执行next,这里的Next在initQueryFrontend
时定义,我们先依次讲解所有的middleware执行过程,最后再来讲解next。
newHandler.ServeHTTP
ServeHTTP将是包含/api/traces/
请求的所有请求最终汇集的函数。也是输出请求日志的函数:

level=info ts=2023-12-13T12:58:39.786900347Z caller=handler.go:135 tenant=PE-Hawk method=GET traceID=58a56149ccadb246 url="/api/traces/9845b56b9f9aa8cc879eba73d7128eb4?start=1702466900&end=1702474100" duration=9.444251ms response_size=1328 status=200
在ServeHTTP中,将会依次调用pre
、RoundTrip(r)
、post
,

而在定义TraceByIDHandler
时newHandler传入了traceByIDSLOPostHook
作为post
。
对ServeHTTP拆解后,完整的middleware调用顺序如下
1. newDeduper(logger)
2. newTraceByIDSharder(&cfg.TraceByID, o, logger)
3. newHedgedRequestWare(cfg.TraceByID.Hedging)
4. retryWare
5. traceByIDSLOPostHook
最后执行next。
newDeduper(logger)
newDeduper
对请求获取到的Trace数据进行解析,然后对Trace进行去重操作。

在dedupe()
中调用groupSpansByID()
、dedupeSpanIDs()

groupSpansByID()
将具有相同的spanId的spans放至spansByID map[uint64][]*v1.Span
。

dedupeSpanIDs()
如果某个server类型的Span的SpanID和一个client类型的Span的SpanID相同,说明这个span跨越了前后端,需要将这两个span区分开(不然在显示上会将分别属于前后端的两个span显示在一起)。
如果存在上述情形,dedupeSpanIDs()
会生成一个新的ID赋值给当前这个server类型的Span,同时赋值这个server类型的Span的Parent的spanID为这个client类型的Span的SpanID。


然后将spanB的子span的parantSpanId赋值为a'


newTraceByIDSharder(&cfg.TraceByID, o, logger)
newTraceByIDSharder对接收到的Trace查询拆分成若干个小的block范围查询,发送至querier组件进行查询满足TraceID和时间范围的block。

buildShardedRequests
buildShardedRequests
將请求拆分blockBoundaries
个请求。
QueryModeBlocks
类型的请求的开始的BlockID(blockStart)、结束的BlockID(blockEnd)由blockBoundaries决定,同时还有一个QueryModeIngesters
类型的请求。
blockBoundaries
blockBoundaries在newTraceByIDSharder
时定义,拆分为配置的query_shards(默认值50)-1个单元,因为第一个单元的请求是Ingester
类型的。

在CreateBlockBoundaries()
函数内,将Block最大值除以
queryShards取整表示每个单元将处理哪些范围BlockID,还有取余操作是将余数均匀的分摊至每个单元。

blockBoundaries
表示的如下所示的数组(假设boundary=34, numLarger=9)
0 : 00000000 00000000 boundary=34 numLarger=9
1 : 00000034 00000000 boundary=68 numLarger=8
2 : 00000068 00000000 boundary=102 numLarger=7
3 : 00000102 00000000 boundary=136 numLarger=6
4 : 00000136 00000000 boundary=170 numLarger=5
5 : 00000170 00000000 boundary=204 numLarger=4
shardQuery并发请求
经过buildShardedRequests
拆分后的若干个请求,将会根据配置concurrent_shards
(若为0 则取query_shards)并发地继续向后调用

这些拆分的请求返回的结果将交给combiner汇总,最终由combiner汇总后返回给上一层,也就是对span进行去重的处理。

newHedgedRequestWare(cfg.TraceByID.Hedging)
newHedgedRequestWare
不对查询逻辑做处理,用来记录query_frontend_hedged_roundtrips_total
监控指标。使用的github.com/cristalhq/hedgedhttp做统计数据的处理。这里暂时先不详细讲解,后续章节详细说明。
retryWare
在retryWare
中会对上一步使用的newTraceByIDSharder
中拆分的若干个小的请求发起请求后如果出现异常进行重试的中间件,重试次数由配置max_retries(默认2次)决定。

到这里,完成了一次请求进入Query Frontend组件后到发起请求前所有的middleWare,以下是执行顺序:
HTTPAuthMiddleware:获取租户信息
httpGzipMiddleware:对请求body进行gzip解压
进入handler.ServeHTTP,所有请求的汇集处,打印请求的日志。
newDeduper:对查询返回的Span作去重、相同spanID的前后端服务父节点关系整理。
newTraceByIDSharder:对请求拆分成若干个小的请求后并发的进行查询后端(Querier)
newHedgedRequest:对请求的统计,提供监控指标
retryWare:对子请求出现异常后,进行重试
下一节将会对最后的next执行进行讲解。也就是向Querier真正发起请求的处理过程。
近期回顾:
Grafana Tempo源码解读(八)Query Frontend组件的MiddleWare使用解读
Grafana Tempo源码解读(七)Compactor将Block进行压缩和删除的过程
Grafana Tempo源码解读(六)Compactor的代码结构和同步Block过程
Grafana Tempo源码解读(五)总结Tempo接收数据的限制以及配置调整
Grafana Tempo源码解读(四)Ingester组件将数据写入持久化存储
Grafana Tempo源码解读(三)Ingester组件接收Trace数据的过程
Grafana Tempo源码解读(二)Distributor对Trace数据的处理和发送至Ingester
Grafana Tempo源码解读(一)Distributor建立监听接收Trace数据




