查询流程总结:
(本文章仅设计thanos-query查sidecar,而sidecar直接查prometheus而不是开启pushgateway)
流程就是thanos-query通过grpc从sidecar端口拉取数据,然后sidecar收到请求以后就会对请求进行处理后再构造一个url去prometheus查询数据,然后把结果返回给thanos-query,thanos-query再去重。
1:前端js检查
当你在thanos-query中输入一条查询语句的时候,前端js会对它先做一个简单的检查,如果没有通过检查,那么就直接报错,就不会进入后面的流程了,比如只有一个数据源z,对应的external_label集合为{prometheus_replica="a"},但是我们输入的promql查询语句为:kube_pod_owner{prometheus_replica="b"},js检查发现排除了唯一的数据源z以后,没有任何数据源的exterl_label值为b,所以js此处就报错返回了。
2:thanos-query
1:获取要查询的数据源。一个数据源就是由--store启动参数指定的一个数据源,首先获取所有数据源,然后判断这个数据源是否可能存在我们所需要的数据,如果不需要,那么查询的时候就会过跳过该数据源。比如一个数据源x的external_label集合为{prometheus_replica="a"},但是我们的查询语句为kube_pod_owner{prometheus_replica="b"},所以prometheus_replica字段就会不匹配,所以本次查询thanos-query就不会去请求数据源x
2:查询。通过grpc,从所有可能存在所需数据的数据源拉取数据,然后再汇总
3:去重。因为sidecar返回的数据中不会包含由--query.replica-labels指定的标签,所以就可能多个数据源拉取到部分相同的数据,从而导致同一套lable对应多条数据,所以要去重
4:返回响应给用户。(略)
3:thanos-sidecar
1:检查请求。对thanos-query发过来的请求进行简单检查,如果不满足要求就会返回失败。对于一个promql:{prometheus_replica="xxx"},thanos-sidecar会首先剔除由--query.replica-labels指定的标签,即会删除prometheus_replica="xxx",然后这个promeql没有其他查询条件了,所以thanos-sidecar此处就报一个错,就不继续往下走了,直接返回失败给thanos-query
2:查询。根据请求中的查询条件构造url,直接去请求prometheus。查询路径为/api/v1/read,prometheus可以根据查询路径来判断是否需要导出external_labels,普通查询使用的是/api/v1/query,此时是不会导出external_labels
3:返回响应给thanos-query。(略)
thanos-query:
//thanos-query启动流程
main:
registerQuery //注册query,并添加启动参数选项。这里是注册,并不是启动,根据启动参数决定是否启动
runQuery //创建query
dns.NewProvider //解析启动参数里的--store
prepareEndpointSet //获取所有抓取目标,即--store指定的所有目标,查询的时候就是根据需要去查对应的endpoint子集
ui.NewQueryUI //注册ui界面,pkg\ui\static\react\index.html ,里面引用了main.js,main.js里实现在前端对promeql语句进行检查的功能
v1.NewQueryAPI
v1.Register //注册query相关的所有url,比如/api/v1/query
route.Router.Get("/query",v1.QueryApi.query) //注册query对应的处理函数,qapi.query这是我们后面要解析的
http.Server.ListenAndServe //这是thanos自带的httpserver
//thaons-query v1.QueryApi.query
v1.QueryApi.query
v1.QueryApi.parseXxx //parse系列函数解析请求
query.NewQueryableCreator //返回一个实现了promql.Queryable接口的对象的函数,该对象里对查询进行了配置,比如配置了replica-labels、是否dedup等,
//这个接口是prometheus对外暴露的,可以通过实现这个接口直接在prometheus的存储层面进行定制化的查询
//dedup=true:从查询中去掉replica-labels,也就是说如果有两条指标,除了replica-labels外其他标签都一样,
//如果开启dedup,那么就会把他们当做两条重复的指标,也就是说只会返回一条指标
//如果关闭dedup,那么就会保留所有标签,也就是说会把上述来两条指标看做不同的指标
//localhost:9090/api/v1/query?query=kube_pod_owner{prometheus_replica!=""}&dedup=false,可以直接通过url参数动态设置
//这个QueryableCreator是prometheus要求低,我们可以自定义一个配置对象,然后实现prometheus指定接口,然后prometheus调用我们这个对象的时候
//会调用我们自己实现的方法,从而实现自定义行为而无需改动prometheus框架,类似于kube-state-metrics源码里面我们自己实现一个store对象,
//然后复用k8s的reflector,这样k8s的reflector就会调用我们的store对象从而调用我们自己的add方法,从而实现自定义行为
promql.Engine.NewInstantQuery //这是prometheus包里面的,根据查询配置,新建一个查询对象
promql.query.Exec //这是prometheus包里面的,执行查询,然后获取指标,注意,这里是去查sidecar,不是prometheus,只是复用prometheus的这个包
promql.Engine.exec
promql.Engine.execEvalStmt //这里没有执行查询
promql.Queryable.Querier //这个Queryable对象就是上面query.NewQueryableCreator函数创建的对象
query.queryable.Querier //这个是thanos包下面的
query.newQuerier //返回一个query.querier对象
promql.Engine.populateSeries //查询
query.querier.Select //thanos下querier对象实现了prometheus中的queryable接口
go func query.querier.selectFn{ //异步执行查询
query.aggrsFromFunc //获取聚合函数
{ //根据是否开启dedup来设置查询的时候要不要用without去掉replica-label
req=&storepb.SeriesRequest{xxx}
req.WithoutReplicaLabels = q.replicaLabels
}
store.ProxyStore.Series //通过grpc去thanos-sidecar查询结果
store.matchesExternalLabels //把查询语句中和--selector-label参数指定的标签相同的查询条件去掉
store.storeMatches //获取所有可能存在所需数据的store对象,一个store对象是一个由--store参数指定的端口
//通过判断查询语句中的所有匹配规则中是否有和该store对象的exterlnal标签冲突的规则,如果有,则过滤掉该store,即该store上不可能存在数据
//举例:查询promql为:kube_pod_owner{prometheus_replica="a"},store的external_label为prometheus_replica="b",冲突,所以该store对象会被过滤掉
//sidecar可以看成是prometheus的一个代理
store.labelSetsMatch //执行上面所说的判断规则
store.newAsyncRespSet //通过grpc异步请求所有可能存在数据的store对象
storepb.storeClient.Series //grpc从store对象查询数据
grpc.ClientStream.SendMsg
---->query from thanos sidecar
<----result from thanos sidecar
if !store.Client.SupportsWithoutReplicaLabels()&& len(req.WithoutReplicaLabels) > 0{
/*
设置removelabels,这个removelabels告诉thanos,再下面的堆排序操作中在比较标签序的时候要忽略掉哪些标签
thanos-sidecar会在查询prometheus的时候通过without操作去掉replica-labels,因为一个sidecar只查一个prometheus,sidecar确定了,
prometheus也就确定了,即replica-labels也就确定了,即返回的结果中是不包含replica-labels中包含的标签的
如果一个store对象不支持without,说明他返回的结果中是含有replica-labels,所以我们需要告诉thaons排序的时候忽略掉这些标签
*/
}
store.newEagerRespSet //创建一个保存了从一个store对象查到的的所有数据的集合
store.NewProxyResponseHeap //把所有response放到一个heap(最小堆)里,这个heap后面会被用来做k路归并来去重,排序方法是比较他们的所有标签,是比较标签不是比较值
//response里数据部分叫seriesSet,seriesSet中每个元素叫series,表示一条数据,每条数据包含两部分,标签部分和数据部分
//!!注意,数据部分是一个数组,数组每个元素叫做chunk,即一个series包含多个具有相同标签的chunk
//这个chunk和和thanos-compaction组件以及thanos降采样相关,chunk这一块还有点模糊,这一块就跳过了,反正大概意思就是读时合并,给series排序
//还有,prometheus导出的数据本身就是chunk,然后thanos-sidecar再把prometheus导出的chunk包装秤自己的AggrChunk
heap.Init
store.NewDedupResponseHeap //创建一个dedup堆,并同时获取第一个元素(即标签序最小的堆的第一个元素),这里的每个元素指series
//这个dedup堆是对上面heap的一个包装,不过这个新的dedup堆的方法会对series做去重处理
store.dedupResponseHeap.Next //判断堆内是否还有下一个元素,并同时调整读取指针。如果有下一个元素,就会把指针调整到下一个元素的位置
//这个全新的数据是指该元素的标签序和上一条指标的标签序不相同,凡是和上一个元素的标签序相同的,我们都算作重复数据,都需要跳过
//这里本质就是一个读时合并的操作
store.dedupResponseHeap.At //直接取dedupResponseHeap的最小的resp的第一个series。因为我们是在next函数里完成调整堆的操作
query.seriesServer.Send //每次一个series,然后把这个数据保存到内部的数组中,这个server对象是前面创建的querier对象的内部对象,也就是说querier对象是查询源码的一个重要对象
dedup.NewSeriesSet //返回一个迭代器,可以迭代所有chunks
//上面的操作是给series去重和排序,我们这里就是把排好序的series的数据部分展开,即把排序序的series的chunk都按顺序收集起来
//即原先是数组seriesA{chunk1,chunk2}、seriesB{chunk1,chunk2}、seriesC{chunk1,chunk2}
//展开后就变成了seriesA.chunk1、seriesA.chunk2、seriesB.chunk1、seriesB.chunk2、seriesC.chunk1、seriesC.chunk2
//一样,thanos-compaction这一块不太清楚,所以这一块代码大致逻辑就是上面说的展开series的chunk
//dedupserieseSet内放了一个promeSeriesSet对象,然后promeSeriesSet对象里又放了一个overlapSplitSet对象,
//最终会把所有chunk按顺序放到这个overlapSplitSet对象的replicas数组中
promise<-set //异步执行完毕,通知等待线程
}
<-promise
promql.evaluator.Eval //生成查询结果。反序列化所有chunk,就是前面都是序列化以后的二进制数据块,即chunk,这里就是解析chunk并组装成最终的result对象
promql.evaluator.eval //执行解析操作
promql.checkAndExpandSeriesSet //获取一个chunk
promql.expandSeriesSet //获取一个series
query.lazySeriesSet.Next //这里query又回到thanos包了,即thanos的lazySeriesSet实现了prometheus里面的某个接口,所以prometheus sdk就会调用我们自己对象的制定方法
dedup.dedupSeriesSet.Next
dedup.dedupSeriesSet.next
query.promSeriesSet.At
dedup.overlapSplitSet.At //返回一个chunk,即按顺序从dedupserieseSet.promeSeriesSet.overlapSplitSet对象的replicas数组中返回chunk
...上面是获取一个chunk,这里是把chunk解析成对应的指标对象,逻辑太长,故直接省略了反序列化代码注释...
thanos-sidecar
//略去前面一些函数,直接进入最核心的
store.PrometheusStore.Series
store.PrometheusStore.externalLabelsFn //获取所有external_labels
store.matchesExternalLabels //把查询表达式中所有在external_labels中的查询条件去掉,
//因为prometheus保存的指标中是没有external_labels中的标签的,如果经过处理后查询条件为空,则直接返回报错
store.startPromRemoteRead //直接构造请求查prometehus: POST http://prometheusIp:port/api/v1/read
http.Client.Do //请求
io.ReadAll //读取结果
store.PrometheusStore.handleSampledPrometheusResponse
store.rmLabels //获取完整标签集。查的时候会把查询条件中的external_labels去掉,但是查完以后我们还要把external_labels中除了query.replica-label指定的标签外的其他标签组装到指标中
//query.replica-label中的标签thanos-query会用一个withoutReplicaLabels参数标识
for{ //prometheus的数据是按chunk来组织的,thanos读到的也是chunk,这里把prometheus的chunk包装成thanos的AggrChunk,仅仅是添加了一些额外的信息
store.hashChunk //计算chunk的hash值
thanosChunk:=&storepb.AggrChunk{ ...prometheus chunk...} //这里把prometheus的chunk包装一下,包装成thanos的AggrChunk
storepb.NewSeriesResponse //创建一个响应
storepb.Store_SeriesServer.Send //返回响应给thanos-query
}