[Série d'analyse du code source Thanos] Une brève analyse du code source du composant de requête thanos

1. Vue d'ensemble:

1.1 Environnement de code source

Les informations de version sont les suivantes:
a. Version du composant Thanos: v0.16.0

1.2 Le rôle de Thanos Query

Le composant Thanos Query est un serveur http + serveur grpc. Sa source de données est un composant en aval qui implémente l'API STORE (comme le composant Thanos Sidecar, le composant Thanos Store, le composant Thanos Ruler), et il implémente également l'API HTTP officielle de Prometheus. Une fois que le composant Thanos Query a obtenu les données de l'aval, il peut effectuer des opérations telles que la fusion et la déduplication, et enfin renvoyer le résultat au client externe. Par conséquent, Thanos Query est le rôle d'intergiciel de base de données.
Insérez la description de l'image ici

2 Brève analyse du code source:

Utilisez le package github.com/oklog/run pour démarrer un ensemble de coroutines. La logique de ces coroutines est principalement de démarrer le serveur http, le serveur grpc et de découvrir dynamiquement les composants en aval qui implémentent l'API STORE.

2.1 méthode principale

Le format de la commande de démarrage de Thanos est le suivant, et le format commence par thanos (car il s'agit du même fichier binaire exécutable). Le composant à démarrer dépend du premier paramètre, qui est la requête dans cet exemple, cette commande est donc la logique de démarrage du composant de requête.

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 

Regardons la méthode principale en détail. Créez un objet d'application. L'objet d'application contient les fonctions de démarrage de tous les composants de Thanos, mais une seule fonction est extraite de la carte pour démarrer lorsqu'elle est réellement lancée. La fonction supprimée dépend de la commande de démarrage.

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 Méthode 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 méthode runQuery

Utilisez l'objet run.Group pour démarrer le serveur http, le serveur grpc et la coroutine de découverte de services.


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 Structure de QueryAPI et ses méthodes

// 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))
}

Jetez un œil à 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 Structure du demandeur et sa méthode

L'interface Querier (github.com/prometheus/prometheus/storage/interface.go) est implémentée. La méthode principale de cette interface est Select (...). Cette méthode est utilisée dans des interfaces telles que / query et / 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 Objet 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
}

Lors de l'interrogation des métriques, les métriques seront interrogées à partir de tous les composants de l'API Store en aval, combinées et dédupliquées (si définies)

/*
根据客户端的请求,从下游的所有的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 Résumé:

Cet article analyse les grandes lignes du code et de nombreux détails n'ont pas été mentionnés, mais la structure du code du composant Thanos Query est claire et facile à comprendre. Le package github.com/oklog/run est utilisé pour démarrer un ensemble de coroutines, et d'écrire le serveur http et le serveur grpc L'idée et la découverte dynamique des composants de l'API Store en aval sont toutes dignes d'être imitées.

Je suppose que tu aimes

Origine blog.csdn.net/nangonghen/article/details/110010423
conseillé
Classement