[ thanos源码分析系列 ]thanos sidecar组件源码简析

1 概述:

1.1 源码环境

版本信息如下:
a、thanos组件版本:v0.16.0

1.2 Thanos Sidecar的作用

Thanos Query组件和prometheus实例绑定在一起,三大作用:
1)作为访问代理,对客户端暴露grpc接口,业务逻辑是访问其绑定的prometheus实例的http接口,从而获取metrics和rule数据,最终返回给客户端。
2)如果开启对象存储功能,会将promethues tsdb目录下的所有block目录上传至指定的对象存储系统中。
3)监听promethues配置文件的变化,发现文件变化后也是访问prometheus实例的http接口让prometheus重载配置。
在这里插入图片描述

2 源码简析:

使用github.com/oklog/run包来启动一组协程,这些协程的逻辑主要是启动了http server、grpc server、动态发现位于下游的实现STORE API的组件等。

2.1 main方法

Thanos的启动命令格式如下,格式都是thanos开头(因为是同一个可执行二进制文件)。启动哪个组件,在于第一个参数,在本例子中是sidecar,因此这条命令是启动sidecar组件的逻辑。

thanos sidecar \
--prometheus.url=http://localhost:9090/ \
--tsdb.path=/prometheus \
--grpc-address=[$(POD_IP)]:10901 \
--http-address=[$(POD_IP)]:10902 \


来具体看看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 registerSidecar(app *extkingpin.App) {
	cmd := app.Command(component.Sidecar.String(), "Sidecar for Prometheus server")
	conf := &sidecarConfig{}
	// 解析命令行参数
	conf.registerFlag(cmd)
	
	// Setup()的入参方法,会被放入app对象的setups列表中
	// 最核心的是runSidecar()方法
	cmd.Setup(func(g *run.Group, logger log.Logger, reg *prometheus.Registry, tracer opentracing.Tracer, _ <-chan struct{}, _ bool) error {
		rl := reloader.New(log.With(logger, "component", "reloader"),
			extprom.WrapRegistererWithPrefix("thanos_sidecar_", reg),
			&reloader.Options{
				ReloadURL:     reloader.ReloadURLFromBase(conf.prometheus.url),
				CfgFile:       conf.reloader.confFile,
				CfgOutputFile: conf.reloader.envVarConfFile,
				WatchedDirs:   conf.reloader.ruleDirectories,
				WatchInterval: conf.reloader.watchInterval,
				RetryInterval: conf.reloader.retryInterval,
			})

		return runSidecar(g, logger, reg, tracer, rl, component.Sidecar, *conf)
	})
}

2.3 runSidecar方法

使用run.Group对象来启动http server、grpc server、传输block目录至对象存储的协程、监听prometheus配置文件的协程、定期检测prometheus实例存活的协程。

细节说明:
1)查看prometheus实例的心跳机制是通过/api/v1/status/config接口。
2)监听prometheus配置文件变化的工具包是github.com/fsnotify/fsnotify。
3)开启上传block功能,则每30s遍历prometheus tsdb目录下的所有的block目录(已上传的block或空block会被忽略,默认情况下被压缩过的block也会被忽略),并上传相应的文件至对象存储。
4)获取不到prometheus实例的external label或者prometheus没有配置external label,会导致sidecar启动失败。

func runSidecar(
	g *run.Group,
	logger log.Logger,
	reg *prometheus.Registry,
	tracer opentracing.Tracer,
	reloader *reloader.Reloader,
	comp component.Component,
	conf sidecarConfig,
) error {

	// 用一个结构体来保存prometheus实例的url、prometheus实例的external label、prometheus client等信息。
	var m = &promMetadata{
		promURL: conf.prometheus.url,

		mint: conf.limitMinTime.PrometheusTimestamp(),
		maxt: math.MaxInt64,

		limitMinTime: conf.limitMinTime,
		client:       promclient.NewWithTracingClient(logger, "thanos-sidecar"),
	}
	
	// 获取对象存储的配置信息,如果有,说明是开启上传block至对象存储的功能。
	confContentYaml, err := conf.objStore.Content()
	if err != nil {
		return errors.Wrap(err, "getting object store config")
	}
	var uploads = true
	if len(confContentYaml) == 0 {
		level.Info(logger).Log("msg", "no supported bucket was configured, uploads will be disabled")
		uploads = false
	}
	

	grpcProbe := prober.NewGRPC()
	httpProbe := prober.NewHTTP()
	statusProber := prober.Combine(
		httpProbe,
		grpcProbe,
		prober.NewInstrumentation(comp, logger, extprom.WrapRegistererWithPrefix("thanos_", reg)),
	)
	
	// 创建http server,并启动server(只有/metrics、/-/healthy、/-/ready等接口)
	srv := httpserver.New(logger, reg, comp, httpProbe,
		httpserver.WithListen(conf.http.bindAddress),
		httpserver.WithGracePeriod(time.Duration(conf.http.gracePeriod)),
	)
	g.Add(func() error {
		statusProber.Healthy()
		return srv.ListenAndServe()
	}, func(err error) {
		statusProber.NotReady(err)
		defer statusProber.NotHealthy(err)
		srv.Shutdown(err)
	})


	// 获取promehtues实例的external label,并做心跳
	{
		// promUp记录promehtues是否正常,0表示不正常,1表示正常
		promUp := promauto.With(reg).NewGauge(prometheus.GaugeOpts{
			Name: "thanos_sidecar_prometheus_up",
			Help: "Boolean indicator whether the sidecar can reach its Prometheus peer.",
		})
		// lastHeartbeat记录最后一次心跳时间
		lastHeartbeat := promauto.With(reg).NewGauge(prometheus.GaugeOpts{
			Name: "thanos_sidecar_last_heartbeat_success_time_seconds",
			Help: "Timestamp of the last successful heartbeat in seconds.",
		})

		ctx, cancel := context.WithCancel(context.Background())
		// 获取prometheus实例的external label(/api/v1/status/config接口),并通过定期(30s)做这件事情来做心跳
		g.Add(func() error {
			/*
				检查性代码
			*/
			
			// 获取prometheus实例的external label
			err := runutil.Retry(2*time.Second, ctx.Done(), func() error {
				// m.UpdateLabels(ctx)去访问prometheus实例的/api/v1/status/config接口,并将返回的数据设置到自己的属性labels
				if err := m.UpdateLabels(ctx); err != nil {						
					promUp.Set(0)
					statusProber.NotReady(err)
					return err
				}			
				promUp.Set(1)
				statusProber.Ready()
				// 记录心跳时间
				lastHeartbeat.SetToCurrentTime()
				return nil
			})
			
			// 拿不到prometheus实例的external label或者prometheus没有配置external label则退出
			if err != nil {
				return errors.Wrap(err, "initial external labels query")
			}			
			if len(m.Labels()) == 0 {
				return errors.New("no external labels configured on Prometheus server, uniquely identifying external labels must be configured; see https://thanos.io/tip/thanos/storage.md#external-labels for details.")
			}

			// 每个30s从prometheus实例获取exterlan label,通过此方式来记录心跳时间
			return runutil.Repeat(30*time.Second, ctx.Done(), func() error {				
				/*
					其他代码
				*/
				
				if err := m.UpdateLabels(iterCtx); err != nil {
					level.Warn(logger).Log("msg", "heartbeat failed", "err", err)
					promUp.Set(0)
				} else {
					promUp.Set(1)
					// 记录心跳时间
					lastHeartbeat.SetToCurrentTime()
				}
				return nil
			})
		}, func(error) {
			cancel()
		})
	}
	
	// 使用github.com/fsnotify/fsnotify包监听prometheus实例的配置文件的变化
	// 如果文件发生变化则发送一个POST请求给prometheus实例,让它重新加载配置文件
	{
		ctx, cancel := context.WithCancel(context.Background())
		g.Add(func() error {
			return reloader.Watch(ctx)
		}, func(error) {
			cancel()
		})
	}

	{
		t := exthttp.NewTransport()
		t.MaxIdleConnsPerHost = conf.connection.maxIdleConnsPerHost
		t.MaxIdleConns = conf.connection.maxIdleConns
		c := promclient.NewClient(&http.Client{Transport: tracing.HTTPTripperware(logger, t)}, logger, thanoshttp.ThanosUserAgent)

		promStore, err := store.NewPrometheusStore(logger, reg, c, conf.prometheus.url, component.Sidecar, m.Labels, m.Timestamps)
		if err != nil {
			return errors.Wrap(err, "create Prometheus store")
		}

		tlsCfg, err := tls.NewServerConfig(log.With(logger, "protocol", "gRPC"),
			conf.grpc.tlsSrvCert, conf.grpc.tlsSrvKey, conf.grpc.tlsSrvClientCA)
		if err != nil {
			return errors.Wrap(err, "setup gRPC server")
		}

		// 创建并grpc server
		s := grpcserver.New(logger, reg, tracer, comp, grpcProbe,
			// 注册grpc handler(通过http client从prometheus实例中获取指标数据)
			grpcserver.WithServer(store.RegisterStoreServer(promStore)),	
			// 注册grpc handler(通过http client从prometheus实例中获取rule数据)
			grpcserver.WithServer(rules.RegisterRulesServer(rules.NewPrometheus(conf.prometheus.url, c, m.Labels))), 
			grpcserver.WithListen(conf.grpc.bindAddress),
			grpcserver.WithGracePeriod(time.Duration(conf.grpc.gracePeriod)),
			grpcserver.WithTLSConfig(tlsCfg),
		)
		g.Add(func() error {
			statusProber.Ready()
			return s.ListenAndServe()
		}, func(err error) {
			statusProber.NotReady(err)
			s.Shutdown(err)
		})
	}

	// 若开启了上传block功能,则定期遍历prometehus tsdb目录下的所有block目录并上传文件至对象存储。
	if uploads {
		
		// 获取一个对象存储bucket
		bkt, err := client.NewBucket(logger, confContentYaml, reg, component.Sidecar.String())
		if err != nil {
			return err
		}
		
		/*
			其他代码
		*/

		ctx, cancel := context.WithCancel(context.Background())
		g.Add(func() error {
			/*
				其他代码
			*/

			/*
				拿不到prometheus实例的external label或者prometheus没有配置external label则退出
			*/
			
			s := shipper.New(logger, reg, conf.tsdb.path, bkt, m.Labels, metadata.SidecarSource,
				conf.shipper.uploadCompacted, conf.shipper.allowOutOfOrderUpload)
			
			// 每30执行一次s.Sync(ctx)
			// s.Sync(ctx)会遍历prometheus tsdb目录下的所有block目录(已上传的block或空block会被忽略,默认情况下被压缩过的block也会被忽略),并上传相应的文件
			return runutil.Repeat(30*time.Second, ctx.Done(), func() error {
				if uploaded, err := s.Sync(ctx); err != nil {
					// 至少有一个block上传失败,则打印日志
				}
				/*
					其他代码
				*/
				return nil
			})
		}, func(error) {
			cancel()
		})
	}
	
	level.Info(logger).Log("msg", "starting sidecar")
	return nil
}

3 总结:

Thanos Sidecar组件的代码逻辑简单、易懂,通过http协议访问与其绑定的prometheus实例,从prometheus实例中获取到的数据则通过grpc接口对外进行暴露,遍历所有block目录进行文件上传,还有监听promethues配置文件变化的小功能。

猜你喜欢

转载自blog.csdn.net/nangonghen/article/details/110731518