[ thanos原始碼分析系列 ]thanos query元件原始碼簡析

nangonghen發表於2020-11-24

1 概述:

1.1 原始碼環境

版本資訊如下:
a、thanos元件版本:v0.16.0

1.2 Thanos Query的作用

Thanos Query元件是http伺服器 + grpc伺服器,它的資料來源是位於下游的已發現的實現STORE API的元件(例如Thanos Sidecar元件、Thanos Store元件、Thanos Ruler元件),同時實現了Prometheus官方的HTTP API。Thanos Query元件從下游處獲得資料後,能進行合併、去重等操作,最後將結果返回給外部的客戶端。因此,Thanos Query就是資料庫中介軟體的角色。
在這裡插入圖片描述

2 原始碼簡析:

使用github.com/oklog/run包來啟動一組協程,這些協程的邏輯主要是啟動了http server、grpc server、動態發現位於下游的實現STORE API的元件等。

2.1 main方法

Thanos的啟動命令格式如下,格式都是thanos開頭(因為是同一個可執行二進位制檔案)。啟動哪個元件,在於第一個引數,在本例子中是query,因此這條命令是啟動query元件的邏輯。

thanos query \
--log.level=debug \
--query.auto-downsampling \
--grpc-address=0.0.0.0:10901 \
--http-address=0.0.0.0:9090 \
--query.partial-response \
--query.replica-label=prometheus_replica \
--query.replica-label=rule_replica \
--store=dnssrv+_grpc._tcp.prometheus-headless.thanos.svc.cluster.local \
--store=dnssrv+_grpc._tcp.thanos-rule.thanos.svc.cluster.local \
--store=dnssrv+_grpc._tcp.thanos-store.thanos.svc.cluster.local 

來具體看看main方法。建立app物件,app物件包含了所有Thanos元件的啟動函式,但真正啟動時只從map中取出一個函式進行啟動,取出哪個函式取決於啟動命令。

func main() {


	/*
		其他程式碼
	*/

	app := extkingpin.NewApp(kingpin.New(filepath.Base(os.Args[0]), "A block storage based long-term storage for Prometheus").Version(version.Print("thanos")))
	/*
		其他程式碼
	*/


	// 把所有元件的啟動邏輯都放進app物件中的setups列表中
	registerSidecar(app)
	registerStore(app)
	registerQuery(app)
	registerRule(app)
	registerCompact(app)
	registerTools(app)
	registerReceive(app)
	registerQueryFrontend(app)

	// 根據命令列的資訊,從app物件的setups列表中取出一個元件邏輯
	cmd, setup := app.Parse()
	logger := logging.NewLogger(*logLevel, *logFormat, *debugName)

	/*
		其他程式碼
	*/

	var g run.Group
	var tracer opentracing.Tracer
	
	/*
		tracing相關的程式碼
	*/
	
	
	reloadCh := make(chan struct{}, 1)

	// 啟動特定的一個元件(sidecar、query、store等元件中的一種),底層還是執行g.Add(...)
	if err := setup(&g, logger, metrics, tracer, reloadCh, *logLevel == "debug"); err != nil {		
		os.Exit(1)
	}

	// 監聽來自系統的殺死訊號.
	{
		cancel := make(chan struct{})
		g.Add(func() error {
			return interrupt(logger, cancel)
		}, func(error) {
			close(cancel)
		})
	}

	// 監聽來配置過載的訊號
	{
		cancel := make(chan struct{})
		g.Add(func() error {
			return reload(logger, cancel, reloadCh)
		}, func(error) {
			close(cancel)
		})
	}

	// 阻塞地等待所有協程中的退出
	// 有一個協程返回,其他協程也會返回
	if err := g.Run(); err != nil {
		level.Error(logger).Log("err", fmt.Sprintf("%+v", errors.Wrapf(err, "%s command failed", cmd)))
		os.Exit(1)
	}
	
	// 到達此處,說明整個程式結束了。
	level.Info(logger).Log("msg", "exiting")
}

2.2 registerQuery方法


func registerQuery(app *extkingpin.App) {

	
	cmd := app.Command(comp.String(), "query node exposing PromQL enabled Query API with data retrieved from multiple store nodes")	
	
	/*
		解析命令列引數
		secure := cmd.Flag("grpc-client-tls-secure", "Use TLS when talking to the gRPC server").Default("false").Bool()
		等等諸如此類
	*/


	//Setup()的入參方法,會被放入app物件的setups列表中
	//最核心的是runQuery()方法
	cmd.Setup(func(g *run.Group, logger log.Logger, reg *prometheus.Registry, tracer opentracing.Tracer, _ <-chan struct{}, _ bool) error {
		
		return runQuery(
			g,
			logger,
			reg,
			tracer,
			*requestLoggingDecision,
			*grpcBindAddr,
			time.Duration(*grpcGracePeriod),
			*grpcCert,
			*grpcKey,
			*grpcClientCA,
			/*
				其他程式碼
			*/
		)
	)

}

2.3 runQuery方法

使用run.Group物件來啟動http server、grpc server、服務發現協程。


func runQuery(
	g *run.Group,		//其實來自main()方法
	logger log.Logger,
	reg *prometheus.Registry,
	tracer opentracing.Tracer,
	requestLoggingDecision string,
	grpcBindAddr string,
	grpcGracePeriod time.Duration,
	grpcCert string,
	grpcKey string,
	grpcClientCA string,
	/*
		其他程式碼
	*/
) error {

	
	var (
	
		// stores物件的型別StoreSet。它包含了一組store元件(位於下游的實現Store API的元件),這一組store元件是可以動態變化的
		/*
		type StoreSet struct {
			//其他屬性
			stores       map[string]*storeRef
		}		
		*/		
		stores = query.NewStoreSet(...)
		
		// proxy物件,即下游的Store API元件的代理
		// 下游的Store API元件的列表,其實就是構造方法的入參stores.Get這個方法來獲取
		proxy            = store.NewProxyStore(logger, reg, stores.Get, component.Query, selectorLset, storeResponseTimeout)
		rulesProxy       = rules.NewProxy(logger, stores.GetRulesClients)
				
		/*
			queryableCreator是一個方法,用於建立一個querier結構體物件;			
			querier結構體的屬性proxy就是proxy物件,它包含了一組會動態變化的thanos store元件(動態變化是因為啟動了一些額外的專門的協程來動態地修改這個切片);
		*/	
		queryableCreator = query.NewQueryableCreator(
			logger,
			extprom.WrapRegistererWithPrefix("thanos_query_", reg),
			proxy,
			maxConcurrentSelects,
			queryTimeout,
		)
							
		/*
			這一段程式碼都是啟動一些協程,定時發現和動態發現Store API元件的變化,隨即更新stores物件中的型別為map[string]*storeRef的屬性	
		*/
				
		
		// 建立http server,註冊http handler,並啟動server
		{

			router := route.New()
			//新建QueryAPI結構體物件
			api := v1.NewQueryAPI(
						logger,
						stores,
						engine,
						queryableCreator,
						rules.NewGRPCClientWithDedup(rulesProxy, queryReplicaLabels),
						enableAutodownsampling,
						enableQueryPartialResponse,
						enableRulePartialResponse,
						queryReplicaLabels,
						flagsMap,
						instantDefaultMaxSourceResolution,
						defaultMetadataTimeRange,
						gate.New(
							extprom.WrapRegistererWithPrefix("thanos_query_concurrent_", reg),
							maxConcurrentQueries,
						),
					)
					
			// 為router物件註冊http方法	
			api.Register(router.WithPrefix("/api/v1"), tracer, logger, ins, logMiddleware)
			
			srv := httpserver.New(logger, reg, comp, httpProbe,
					httpserver.WithListen(httpBindAddr),
					httpserver.WithGracePeriod(httpGracePeriod),
			)
			// http伺服器使用router物件
			srv.Handle("/", router)
			
			g.Add(func() error {
				statusProber.Healthy()
				// 啟動http server
				return srv.ListenAndServe()		
			}, func(err error) {
				statusProber.NotReady(err)
				defer statusProber.NotHealthy(err)
				srv.Shutdown(err)
		})
		}
			
		// 建立gprc server,註冊grpc handler,並啟動server
		{
			tlsCfg, err := tls.NewServerConfig(log.With(logger, "protocol", "gRPC"), grpcCert, grpcKey, grpcClientCA)
			if err != nil {
				return errors.Wrap(err, "setup gRPC server")
			}

			s := grpcserver.New(logger, reg, tracer, comp, grpcProbe,				
				grpcserver.WithServer(store.RegisterStoreServer(proxy)),		// 註冊grpc handler
				grpcserver.WithServer(rules.RegisterRulesServer(rulesProxy)),   // 註冊grpc handler
				grpcserver.WithListen(grpcBindAddr),
				grpcserver.WithGracePeriod(grpcGracePeriod),
				grpcserver.WithTLSConfig(tlsCfg),
			)

			g.Add(func() error {
				statusProber.Ready()
				// 啟動grpc server
				return s.ListenAndServe()		
			}, func(error) {
				statusProber.NotReady(err)
				s.Shutdown(err)
			})
		}

		// 至此,http server和grpc server都啟動了。
		level.Info(logger).Log("msg", "starting query node")
		return nil			
	)
	
}

2.4 QueryAPI結構體及其方法

// QueryAPI is an API used by Thanos Query.
type QueryAPI struct {
	baseAPI         *api.BaseAPI
	logger          log.Logger
	gate            gate.Gate
	
	// 構造方法,用於建立一個querier結構體物件
	queryableCreate query.QueryableCreator
	
	queryEngine     *promql.Engine
	ruleGroups      rules.UnaryClient
	/*
	其他程式碼
	*/
	replicaLabels []string
	storeSet      *query.StoreSet
}


func (qapi *QueryAPI) Register(r *route.Router, tracer opentracing.Tracer, logger log.Logger, ins extpromhttp.InstrumentationMiddleware, logMiddleware *logging.HTTPServerMiddleware) {
	qapi.baseAPI.Register(r, tracer, logger, ins, logMiddleware)
	instr := api.GetInstr(tracer, logger, ins, logMiddleware)
	/*
		其他程式碼
	*/
	
	// 把qapi.query、qapi.series、 qapi.stores註冊到入參r,從而完成http handler的註冊
	// 不管是/query介面和/series介面,每次請求到達都會建立querier物件,而querier物件內含了一組的Store API元件
	r.Get("/query", instr("query", qapi.query))
	r.Get("/series", instr("series", qapi.series))
	r.Get("/stores", instr("stores", qapi.stores))
}

看看qapi.series。

//返回指標資料
func (qapi *QueryAPI) series(r *http.Request) (interface{}, []error, *api.ApiError) {
	/*
	其他程式碼
	*/
	
	// 建立一個querier物件
	// querier物件的屬性proxy則包含了一組thanos store元件
	q, err := qapi.queryableCreate(enableDedup, replicaLabels, storeDebugMatchers, math.MaxInt64, enablePartialResponse, true).
		Querier(r.Context(), timestamp.FromTime(start), timestamp.FromTime(end))
		
	/*
	其他程式碼
	*/
	
	var (
		metrics = []labels.Labels{}
		sets    []storage.SeriesSet
	)
	for _, mset := range matcherSets {
		// 呼叫querier物件的Select()方法獲取指標
		sets = append(sets, q.Select(false, nil, mset...))
	}
	set := storage.NewMergeSeriesSet(sets, storage.ChainedSeriesMerge)
	for set.Next() {
		metrics = append(metrics, set.At().Labels())
	}	
	return metrics, set.Warnings(), nil
}

2.5 querier結構體及其方法

實現了 Querier介面(github.com/prometheus/prometheus/storage/interface.go),此介面的核心方法是Select(…),這個方法在/query和/series等介面中都會被使用到。

type querier struct {
	ctx                 context.Context
	logger              log.Logger
	cancel              func()
	mint, maxt          int64
	replicaLabels       map[string]struct{}
	storeDebugMatchers  [][]*labels.Matcher
	
	// proxy包含了一組動態的thanos store元件
	proxy               storepb.StoreServer
	
	deduplicate         bool	
	maxResolutionMillis int64
	partialResponse     bool
	skipChunks          bool
	selectGate          gate.Gate
	selectTimeout       time.Duration
}

func (q *querier) Select(_ bool, hints *storage.SelectHints, ms ...*labels.Matcher) storage.SeriesSet {
	/*
		其他程式碼
	*/	
	promise := make(chan storage.SeriesSet, 1)
	go func() {
		defer close(promise)
		var err error
		/*
			其他程式碼
		*/	
		//獲取到指標資料
		set, err := q.selectFn(ctx, hints, ms...)
		if err != nil {
			// 把錯誤送至管道,並退出本協程
			promise <- storage.ErrSeriesSet(err)
			return
		}
		//將指標資料送至管道
		promise <- set
	}()

	// 返回指標的封裝
	return &lazySeriesSet{
		create: func() (storage.SeriesSet, bool) {
			/*
				其他程式碼
			*/	
			// 從管道中讀取指標
			set, ok := <-promise	
			return set, set.Next()
		}
	}
}

// 獲取指標,呼叫的是屬性proxy的Series(...)方法
func (q *querier) selectFn(ctx context.Context, hints *storage.SelectHints, ms ...*labels.Matcher) (storage.SeriesSet, error) {

	/*
		其他程式碼
	*/	
	
	// seriesServer結構體重寫了Send()方法,在Sender()方法中將gprc返回的資料資料儲存到它的seriesSet屬性
	resp := &seriesServer{ctx: ctx}	
	
	// q.proxy的實現是ProxyStore結構體
	// q.proxy.Series()是grpc方法(流式)
	// q.proxy.Series()呼叫完畢後,resp的seriesSet屬性的值會被填充	
	if err := q.proxy.Series(&storepb.SeriesRequest{
		MinTime:                 hints.Start,
		MaxTime:                 hints.End,
		Matchers:                sms,
		/*
			其他程式碼
		*/
	}, resp); err != nil {
		return nil, errors.Wrap(err, "proxy Series()")
	}
	
	/*
		其他程式碼
	*/
	
	
	set := &promSeriesSet{
		mint:  q.mint,
		maxt:  q.maxt,
		set:   newStoreSeriesSet(resp.seriesSet),  // 把resp的seriesSet屬性抽出來
		aggrs: aggrs,
		warns: warns,
	}
	// set就是指標
	return newDedupSeriesSet(set, q.replicaLabels, len(aggrs) == 1 && aggrs[0] == storepb.Aggr_COUNTER), nil
}

2.6 ProxyStore物件

// ProxyStore implements the store API that proxies request to all given underlying stores.
type ProxyStore struct {
	logger         log.Logger
	
	// 返回位於下游的實現Store API介面的元件,查詢指標時會用到此屬性
	stores         func() []Client
	
	component      component.StoreAPI
	selectorLabels labels.Labels

	responseTimeout time.Duration
	metrics         *proxyStoreMetrics
}

查詢指標時,會從下游的所有的Store API的元件中查詢指標以及進行合併、去重(如果設定了)

/*
根據客戶端的請求,從下游的所有的Store API的元件中查詢指標以及進行合併、去重,最後將指標傳輸給入參srv.
這是一個gprc流式介面。
*/
func (s *ProxyStore) Series(r *storepb.SeriesRequest, srv storepb.Store_SeriesServer) error {
	
	/*
		其他程式碼
	*/
	g, gctx := errgroup.WithContext(srv.Context())
	respSender, respCh := newCancelableRespChannel(gctx, 10)


	// 生產者協程
	g.Go(func() error {		
		/*
			本協程會從後端的thanos store元件中獲取指標,並進行指標合併操作。
			本協程的關閉,消費者協程也會關閉。
		*/
		
		var (
			seriesSet      []storepb.SeriesSet
			storeDebugMsgs []string		
			wg = &sync.WaitGroup{}
		)

		defer func() {
			wg.Wait()
			//close()方法會引發消費者協程退出
			close(respCh)
		}()

		// 遍歷後端的Store API元件
		for _, st := range s.stores() {
		
			/*
				其他程式碼
			*/	
			
			sc, err := st.Series(seriesCtx, r)		
			seriesSet = append(seriesSet, startStreamSeriesSet(seriesCtx, s.logger, closeSeries,
				wg, sc, respSender, st.String(), !r.PartialResponseDisabled, s.responseTimeout, s.metrics.emptyStreamResponses))
		
			/*
				其他程式碼
			*/
			
		// 獲得合併後的指標,再傳送給respCh管道
		mergedSet := storepb.MergeSeriesSets(seriesSet...)
		for mergedSet.Next() {		
			lset, chk := mergedSet.At()
			// respSender.send(...)其實是將指標傳送給respCh管道
			respSender.send(storepb.NewSeriesResponse(&storepb.Series{Labels: labelpb.ZLabelsFromPromLabels(lset), Chunks: chk}))
		}
		return mergedSet.Err()
	})
	
	// 消費者協程
	g.Go(func() error {				
		// 響應(已被merged)被本協程獲取,並將響應輸送給方法入參srv.
		for resp := range respCh {
			if err := srv.Send(resp); err != nil {
				return status.Error(codes.Unknown, errors.Wrap(err, "send series response").Error())
			}
		}
		return nil
	})
	
	// 等待生產者協程和消費者協程結束
	if err := g.Wait(); err != nil {			
		return err
	}
	return nil
}

3 總結:

本文分析了程式碼的輪廓,還有許多細節沒有被提及,但Thanos Query元件的程式碼結構清晰易懂,使用了github.com/oklog/run包來啟動一組協程,編寫http server和grpc server的思路、動態發現下游Store API元件的套路都值得模仿。

相關文章