Thanos源码专题【左扬精讲】——Thanos Sidecar 组件(release-0.26)源码阅读和分析(第三章—— cmd/sidecar.go 源代码注释)

Thanos Sidecar 组件(release-0.26)源码阅读和分析(第三章—— cmd/sidecar.go 源代码注释)

https://github.com/thanos-io/thanos/blob/v0.26.0/cmd/thanos/sidecar.go

// Copyright (c) The Thanos Authors.
// Licensed under the Apache License 2.0.

package main

import (
	"context"
	"math"
	"net/url"
	"sync"
	"time"

	extflag "github.com/efficientgo/tools/extkingpin"
	"github.com/go-kit/log"
	"github.com/go-kit/log/level"
	grpc_logging "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tags"
	"github.com/oklog/run"
	"github.com/opentracing/opentracing-go"
	"github.com/pkg/errors"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/common/model"
	"github.com/prometheus/prometheus/model/labels"

	"github.com/thanos-io/thanos/pkg/block/metadata"
	"github.com/thanos-io/thanos/pkg/component"
	"github.com/thanos-io/thanos/pkg/exemplars"
	"github.com/thanos-io/thanos/pkg/extkingpin"
	"github.com/thanos-io/thanos/pkg/extprom"
	"github.com/thanos-io/thanos/pkg/httpconfig"
	"github.com/thanos-io/thanos/pkg/info"
	"github.com/thanos-io/thanos/pkg/info/infopb"
	"github.com/thanos-io/thanos/pkg/logging"
	meta "github.com/thanos-io/thanos/pkg/metadata"
	thanosmodel "github.com/thanos-io/thanos/pkg/model"
	"github.com/thanos-io/thanos/pkg/objstore/client"
	"github.com/thanos-io/thanos/pkg/prober"
	"github.com/thanos-io/thanos/pkg/promclient"
	"github.com/thanos-io/thanos/pkg/reloader"
	"github.com/thanos-io/thanos/pkg/rules"
	"github.com/thanos-io/thanos/pkg/runutil"
	grpcserver "github.com/thanos-io/thanos/pkg/server/grpc"
	httpserver "github.com/thanos-io/thanos/pkg/server/http"
	"github.com/thanos-io/thanos/pkg/shipper"
	"github.com/thanos-io/thanos/pkg/store"
	"github.com/thanos-io/thanos/pkg/store/labelpb"
	"github.com/thanos-io/thanos/pkg/targets"
	"github.com/thanos-io/thanos/pkg/tls"
)

// 【1、Sidecar 注册与初始化】
// registerSidecar 注册 sidecar 命令到 CLI 应用
func registerSidecar(app *extkingpin.App) {
	// 注册命令,并设置帮助信息
	// 通过extkingpin.App类型的app参数注册一个新的命令。命令的名称由component.Sidecar.String()提供,描述为"Sidecar for Prometheus server."。extkingpin是对kingpin库的扩展,用于构建命令行接口。
	cmd := app.Command(component.Sidecar.String(), "Sidecar for Prometheus server.")
	// 创建一个sidecarConfig类型的实例conf,用于存储sidecar进程的配置信息。sidecarConfig结构体定义了一些字段来存储配置数据。
	conf := &sidecarConfig{}
	// 调用conf的registerFlag方法,注册所有命令行参数到 sidecarConfig。
	conf.registerFlag(cmd)

	// 设置命令的执行逻辑,即在执行sidecar命令时调用的函数。这里通过Setup方法注册了一个匿名函数,该函数的参数包括一个 run.Group实例、日志记录器logger、Prometheus注册表reg、OpenTracing 追踪器、一个信号通道和一个布尔值。
	// 这个匿名函数主要负责配置 reloader 的创建和sidecar服务的启动。
	cmd.Setup(func(g *run.Group, logger log.Logger, reg *prometheus.Registry, tracer opentracing.Tracer, _ <-chan struct{}, _ bool) error {
		// 解析请求日志配置
		// 【问】 为什么要解析 gRPC 日志配置?
		// 【答】 为了定制 gRPC 请求的日志格式(如添加跟踪标签),用于调试和监控 gRPC 调用。
		tagOpts, grpcLogOpts, err := logging.ParsegRPCOptions("", conf.reqLogConfig)
		if err != nil {
			return errors.Wrap(err, "error while parsing config for request logging")
		}

		// 【关键逻辑】创建 reloader 实例,【目的】:监控 Prometheus 配置文件变化,触发动态重载。
		// 参数说明:
		// - ReloadURL: Prometheus 的 /-/reload 端点
		// - CfgFile: 监控的 Prometheus 配置文件路径
		// - WatchedDirs: 需要监控的规则文件目录
		rl := reloader.New(log.With(logger, "component", "reloader"),
			extprom.WrapRegistererWithPrefix("thanos_sidecar_", reg),
			&reloader.Options{
				// 配置重载URL
				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,
			})

		//【启动 thanos-sidecar 核心逻辑】调用runSidecar函数来启动sidecar进程。这个函数负责根据提供的配置和资源启动sidecar的实际工作。
		return runSidecar(g, logger, reg, tracer, rl, component.Sidecar, *conf, grpcLogOpts, tagOpts)
	})
}

// 【2、thanos-sidecar 核心运行逻辑】
// runSidecar 函数用于启动 sidecar 服务,与 Prometheus 进行交互,并处理数据上传。
func runSidecar(
	g *run.Group, // g: *run.Group 类型的组对象,用于管理并发执行的任务;
	logger log.Logger, // logger: log.Logger 类型的日志记录器;
	reg *prometheus.Registry, // reg: *prometheus.Registry 类型的 Prometheus 注册中心。
	tracer opentracing.Tracer, // tracer: opentracing.Tracer 类型的追踪器。
	reloader *reloader.Reloader, // reloader: *reloader.Reloader 类型的重载器,用于动态加载配置。
	comp component.Component, // comp: component.Component 类型的组件对象。
	conf sidecarConfig, // conf: sidecarConfig 类型的 sidecar 配置对象。
	grpcLogOpts []grpc_logging.Option, // grpcLogOpts: []grpc_logging.Option 类型的 gRPC 日志选项。
	tagOpts []tags.Option, // tagOpts: []tags.Option 类型的标签选项。
) error {
	// 获取 Prometheus 的 http 客户端配置
	// 链式调用方式在 Go 语言中很常见:获取 Prometheus 的 http 客户端配置,并解析为 httpconfig.ClientConfig 结构体。
	httpConfContentYaml, err := conf.prometheus.httpClient.Content()
	if err != nil {
		return errors.Wrap(err, "getting http client config") // 错误处理,如果获取 http 客户端配置失败则返回错误。
	}
	httpClientConfig, err := httpconfig.NewClientConfigFromYAML(httpConfContentYaml) // 解析 Prometheus 的 http 客户端配置,并返回一个 httpconfig.ClientConfig 结构体。
	if err != nil {
		return errors.Wrap(err, "parsing http config YAML") // 错误处理,如果解析 http 配置 YAML 文件失败则返回错误。
	}

	// 【问】 为什么要单独创建 HTTP Client?
	// 【答】 使用与 Prometheus 相同的 TLS/认证配置,确保 Sidecar 能安全访问 Prometheus API。
	httpClient, err := httpconfig.NewHTTPClient(*httpClientConfig, "thanos-sidecar")
	if err != nil {
		return errors.Wrap(err, "Improper http client config")
	}
	// 设置 reloader 的 http 客户端
	// 问1:thanos sidecar 动态加载配置文件的必要性是什么?
	// 答1:1、允许在不重启 sidecar 的情况下更新配置文件,例如修改对象存储的访问凭证调整数据上传策略等;2、通过重新加载配置文件,可以动态调整 sidecar 的行为和性能参数,例如增加并发上传数、修改数据压缩级别等。
	// 问2:thanos sidecar 如何与 Prometheus 进行交互?
	// 答2:thanos sidecar 通过 reloader.Reloader 对象与 Prometheus 进行交互,通过 reloader.Reloader 对象的 Watch 方法, continuously watch the Prometheus configuration file for changes, and reload the configuration when changes are detected.
	// 问3:为什么需要 reloader.SetHttpClient 方法?
	// 答3:reloader组件负责监控配置文件的变化,并在检测到变化时重新加载配置。为了实现这一点,reloader需要能够通知Prometheus(或其他需要知道配置变化的组件)配置已更新。
	// 在Thanos中,reloader通过HTTP请求通知Prometheus配置已更改。这是Prometheus重新加载配置的标准机制之一(Prometheus支持通过HTTP请求触发配置重载)。因此,reloader.SetHttpClient 方法允许 reloader 使用 Prometheus 的 http 客户端来发送这些 HTTP 请求。因此,reloader需要一个HTTP客户端来发送这些通知请求。通过reloader.SetHttpClient(*httpClient),我们为reloader提供了一个配置好的HTTP客户端,它可以使用与Sidecar相同的认证和TLS设置来与Prometheus通信。
	reloader.SetHttpClient(*httpClient)

	// 【关键结构体】 promMetadata 定义
	var m = &promMetadata{
		promURL: conf.prometheus.url, // 连接的 Prometheus 实例地址

		// Start out with the full time range. The shipper will constrain it later.
		// TODO(fabxc): minimum timestamp is never adjusted if shipping is disabled.

		// 暴露给查询层(如 Thanos Querier),告知当前可查询的时间范围。通过 UpdateTimestamps 方法,从 Shipper 模块获取已上传块的时间范围,并更新 mint 和 maxt。
		mint: conf.limitMinTime.PrometheusTimestamp(), // (最小时间戳),初始化为 --min-time 配置
		maxt: math.MaxInt64,                           // (最大时间戳):初始化为 math.MaxInt64(表示无上限)。

		limitMinTime: conf.limitMinTime,                                                     // 查询时过滤早于此时间的数据,在 UpdateTimestamps 中强制 mint = max(实际mint, limitMinTime),但 不删除已上传的旧数据。
		client:       promclient.NewWithTracingClient(logger, httpClient, "thanos-sidecar"), // 创建 Prometheus 客户端,用于与 Prometheus 进行交互,并启用追踪
	}

	confContentYaml, err := conf.objStore.Content() // 这里的 objStore.Content() 方法用于获取对象存储的配置内容,并将其转换为 YAML 格式,通常包括访问凭证、桶名称等关键参数
	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。
		uploads = false                                                                               // 设置 uploads 变量为 false,表示禁用上传功能。
	}

	// 【3、探针与服务启动逻辑】
	// 初始化双模式探针系统,用于检查服务的健康状态
	grpcProbe := prober.NewGRPC()   // 创建 GRPC 探针,用于检查 gRPC 服务是否健康 (即 gRPC 服务是否正常运行)
	httpProbe := prober.NewHTTP()   // 创建 HTTP 探针,用于检查 HTTP 服务是否健康 (即 HTTP 服务是否正常运行)
	statusProber := prober.Combine( // 合并多个探针为一个,以便同时检查 gRPC 和 HTTP 服务是否健康 (即同时检查 gRPC 和 HTTP 服务是否正常运行)
		httpProbe,
		grpcProbe,
		prober.NewInstrumentation(comp, logger, extprom.WrapRegistererWithPrefix("thanos_", reg)), // 添加额外的监控指标,以便跟踪服务的健康状态和性能表现S
	)

	// 启动 HTTP 服务,用于提供健康检查和指标收集等功能
	// 【问】 为什么需要 GracePeriod?
	// 【答】 优雅关闭等待时间,确保正在处理的请求完成,防止数据丢失或中断。
	srv := httpserver.New(logger, reg, comp, httpProbe,
		httpserver.WithListen(conf.http.bindAddress),                     // 设置 HTTP 服务的监听地址
		httpserver.WithGracePeriod(time.Duration(conf.http.gracePeriod)), // 设置优雅关闭的等待时间,以便在服务停止时允许正在处理的请求完成
		httpserver.WithTLSConfig(conf.http.tlsConfig),                    // 设置 HTTP 服务的 TLS 配置,以便启用 HTTPS
	)

	// 启动 gRPC 服务,用于提供数据上传等功能
	// Add 方法添加了一个新的并发任务,该任务会在服务启动时执行。
	g.Add(func() error {
		statusProber.Healthy()      // 通过调用statusProber.Healthy()方法将服务标记为健康状态。这通常用于监控和健康检查,表明服务已准备好接收请求。
		return srv.ListenAndServe() // srv.ListenAndServe()是gRPC服务的核心启动方法。它会使服务监听指定的端口,并处理传入的请求。如果服务成功启动,该方法将阻塞当前goroutine,直到服务停止。如果启动过程中发生错误,它将返回一个错误。
	}, func(err error) { // 如果srv.ListenAndServe()返回错误,将执行这个错误处理函数。
		statusProber.NotReady(err)         // 首先将服务标记为 NotReady 状态,这通常意味着服务遇到了问题;
		defer statusProber.NotHealthy(err) // 用defer关键字确保在函数返回之前,将服务标记为不健康状态。这是为了确保无论后续操作如何,服务的健康状态都能被正确更新。

		srv.Shutdown(err) // 最后,调用srv.Shutdown(err)方法来优雅地关闭gRPC服务。这个方法会停止监听新的连接,并等待当前正在处理的请求完成。
	})

	// Setup all the concurrent groups.
	// 启动并发组,以便同时执行多个任务。
	{
		promUp := promauto.With(reg).NewGauge(prometheus.GaugeOpts{ // 创建一个新的指标,用于跟踪 sidecar 是否能够成功连接到 Prometheus。
			Name: "thanos_sidecar_prometheus_up",                                         // 指标名称,用于标识该指标。
			Help: "Boolean indicator whether the sidecar can reach its Prometheus peer.", // 指标的帮助信息,用于描述该指标的作用。
		})

		ctx, cancel := context.WithCancel(context.Background()) // 创建一个新的上下文,并返回一个取消函数。这个上下文将在服务关闭时被取消。

		// 【关键流程】 Prometheus元数据校验环节
		// 【问】为什么要在服务启动时校验 Prometheus 的配置?
		// 【答】在服务启动时校验 Prometheus 的配置是为了确保 sidecar 能够正确连接到 Prometheus,并且和thanos数据兼容。这样可以避免由于配置不一致导致的数据上传失败或数据异常
		g.Add(func() error {
			// Only check Prometheus's flags when upload is enabled.
			// 如果上传功能被禁用,则跳过 Prometheus 的配置校验。
			if uploads {
				// Check prometheus's flags to ensure same sidecar flags.
				if err := validatePrometheus(ctx, m.client, logger, conf.shipper.ignoreBlockSize, m); err != nil { // 校验 Prometheus 的配置,确保它与 sidecar 的配置一致。如果存在不一致,则返回错误。
					return errors.Wrap(err, "validate Prometheus flags") // 如果校验失败,则返回错误。
				}
			}

			// We retry infinitely until we reach and fetch BuildVersion from our Prometheus.
			// 无限重试,直到成功从 Prometheus 获取构建版本。这确保了 sidecar 能够与正在运行的 Prometheus 实例通信,并获取其版本信息。
			err := runutil.Retry(2*time.Second, ctx.Done(), func() error {
				if err := m.BuildVersion(ctx); err != nil {
					level.Warn(logger).Log(
						"msg", "failed to fetch prometheus version. Is Prometheus running? Retrying",
						"err", err,
					)
					return err
				}

				level.Info(logger).Log(
					"msg", "successfully loaded prometheus version",
				)
				return nil
			})
			if err != nil {
				return errors.Wrap(err, "failed to get prometheus version")
			}

			// Blocking query of external labels before joining as a Source Peer into gossip.
			// We retry infinitely until we reach and fetch labels from our Prometheus.
			// 无限重试,直到成功从 Prometheus 获取外部标签。这确保了 sidecar 能够获取到正确的外部标签信息,以便正确地处理和路由数据。
			err = runutil.Retry(2*time.Second, ctx.Done(), func() error {
				if err := m.UpdateLabels(ctx); err != nil {
					level.Warn(logger).Log(
						"msg", "failed to fetch initial external labels. Is Prometheus running? Retrying",
						"err", err,
					)
					promUp.Set(0)
					statusProber.NotReady(err)
					return err
				}

				level.Info(logger).Log(
					"msg", "successfully loaded prometheus external labels",
					"external_labels", m.Labels().String(),
				)
				promUp.Set(1)
				statusProber.Ready()
				return nil
			})
			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.") // 如果没有配置外部标签,则返回错误。这通常是因为 Prometheus 服务器没有正确配置外部标签,这些标签对于唯一标识和路由数据至关重要。
			}

			// Periodically query the Prometheus config. We use this as a heartbeat as well as for updating
			// the external labels we apply.
			// 定期查询 Prometheus 的配置。我们使用这个作为心跳,以及更新我们应用的外部标签。
			return runutil.Repeat(30*time.Second, ctx.Done(), func() error {
				iterCtx, iterCancel := context.WithTimeout(context.Background(), 5*time.Second)
				defer iterCancel()

				if err := m.UpdateLabels(iterCtx); err != nil {
					level.Warn(logger).Log("msg", "heartbeat failed", "err", err)
					promUp.Set(0)
					statusProber.NotReady(err)
				} else {
					promUp.Set(1)
					statusProber.Ready()
				}

				return nil
			})
		}, func(error) {
			cancel()
		})
	}
	{
		ctx, cancel := context.WithCancel(context.Background())
		g.Add(func() error {
			return reloader.Watch(ctx)
		}, func(error) {
			cancel()
		})
	}
	{
		// 【问】 为什么要创建一个新的 Prometheus 客户端?
		// 【答】 创建一个新的 Prometheus 客户端,用于与主 Prometheus 服务进行通信。
		// 这个客户端将使用特定的用户代理字符串和 HTTP 配置来确保正确的请求头被发送到服务器。这个客户端将用于执行各种查询和操作,例如获取 Prometheus 的版本信息、更新外部标签等。
		c := promclient.NewWithTracingClient(logger, httpClient, httpconfig.ThanosUserAgent)

		// 【关键服务】暴露多种 gRPC 端点,查询端点。
		promStore, err := store.NewPrometheusStore(logger, reg, c, conf.prometheus.url, component.Sidecar, m.Labels, m.Timestamps, m.Version)
		if err != nil {
			return errors.Wrap(err, "create Prometheus store") // 如果创建 Prometheus 数据存储失败,则返回错误。
		}

		tlsCfg, err := tls.NewServerConfig(log.With(logger, "protocol", "gRPC"), // 配置 gRPC 服务器使用的 TLS 配置。这将确保在客户端和服务器之间建立安全的连接。
			conf.grpc.tlsSrvCert, conf.grpc.tlsSrvKey, conf.grpc.tlsSrvClientCA)
		if err != nil {
			return errors.Wrap(err, "setup gRPC server") // 如果配置 gRPC 服务器失败,则返回错误。
		}

		// 【关键服务】暴露多种 gRPC 端点,示例数据端点。
		exemplarSrv := exemplars.NewPrometheus(conf.prometheus.url, c, m.Labels)

		// 【关键服务】暴露多种 gRPC 端点,元数据端点。
		infoSrv := info.NewInfoServer(
			component.Sidecar.String(), // 组件名称,用于标识服务。
			info.WithLabelSetFunc(func() []labelpb.ZLabelSet { // 返回 Prometheus 数据存储的标签集。这将允许客户端了解存储的标签集,这对于路由和查询数据至关重要。
				return promStore.LabelSet() // 获取 Prometheus 数据存储的标签集。这将允许客户端了解存储的标签集,这对于路由和查询数据至关重要。
			}),
			info.WithStoreInfoFunc(func() *infopb.StoreInfo {
				if httpProbe.IsReady() {
					mint, maxt := promStore.Timestamps()
					return &infopb.StoreInfo{
						MinTime: mint,
						MaxTime: maxt,
					}
				}
				return nil
			}),
			info.WithExemplarsInfoFunc(),      // 配置 exemplarSrv,使其能够提供有关 Prometheus 实例的示例数据信息。这将允许客户端查询特定时间范围内的样本值和标签集。
			info.WithRulesInfoFunc(),          // 配置规则端点,使其能够提供有关 Prometheus 实例的记录规则信息。这将允许客户端查询和评估记录规则的状态和结果。
			info.WithTargetsInfoFunc(),        // 配置目标端点,使其能够提供有关 Prometheus 实例的目标信息。这将允许客户端查询和监控目标的健康状态、标签集等。
			info.WithMetricMetadataInfoFunc(), // 配置元数据端点,使其能够提供有关 Prometheus 实例的指标元数据信息。这将允许客户端查询和了解特定指标的定义、标签集等。
		)

		// 【问】 为什么需要 TLS 配置?
		// 【答】 保护 gRPC 通信安全,防止中间人攻击,确保数据机密性和完整性。
		// 这将确保在客户端和服务器之间建立安全的连接,防止中间人攻击,并保护数据的机密性和完整性。
		s := grpcserver.New(logger, reg, tracer, grpcLogOpts, tagOpts, comp, grpcProbe,
			grpcserver.WithServer(store.RegisterStoreServer(promStore)),
			grpcserver.WithServer(rules.RegisterRulesServer(rules.NewPrometheus(conf.prometheus.url, c, m.Labels))),
			grpcserver.WithServer(targets.RegisterTargetsServer(targets.NewPrometheus(conf.prometheus.url, c, m.Labels))),
			grpcserver.WithServer(meta.RegisterMetadataServer(meta.NewPrometheus(conf.prometheus.url, c))),
			grpcserver.WithServer(exemplars.RegisterExemplarsServer(exemplarSrv)),
			grpcserver.WithServer(info.RegisterInfoServer(infoSrv)),
			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)
		})
	}

	// 【5、数据上传逻辑(Shipper)】
	if uploads {
		// The background shipper continuously scans the data directory and uploads
		// new blocks to Google Cloud Storage or an S3-compatible storage service.
		// 初始化对象存储客户端,用于上传数据块到对象存储服务。这包括配置客户端以连接到对象存储服务,并初始化一个用于上传数据块的桶(bucket)。
		bkt, err := client.NewBucket(logger, confContentYaml, reg, component.Sidecar.String())
		if err != nil {
			return err
		}

		// Ensure we close up everything properly.
		// 确保在出现错误时正确关闭所有资源。这包括对象存储客户端和任何其他相关资源,以避免潜在的资源泄露或悬挂连接等问题。
		defer func() {
			if err != nil {
				runutil.CloseWithLogOnErr(logger, bkt, "bucket client") // 关闭对象存储客户端。
			}
		}()

		// 检查 wal directory is accessible.
		if err := promclient.IsWALDirAccessible(conf.tsdb.path); err != nil {
			level.Error(logger).Log("err", err)
		}

		ctx, cancel := context.WithCancel(context.Background())
		// 【关键模块】定时查询 Prometheus 的配置。我们使用这个作为心跳,以及更新我们应用的外部标签。
		g.Add(func() error {
			defer runutil.CloseWithLogOnErr(logger, bkt, "bucket client")

			promReadyTimeout := conf.prometheus.readyTimeout                   // Prometheus 准备超时时间。这用于等待 Prometheus 服务变得可用,以便我们可以获取外部标签并开始上传数据块。
			extLabelsCtx, cancel := context.WithTimeout(ctx, promReadyTimeout) // 创建上下文,用于等待 Prometheus 服务变得可用。这将确保在尝试获取外部标签之前,Prometheus 服务已经准备好并可访问。
			defer cancel()                                                     // 取消等待 Prometheus 服务变得可用的上下文。这将确保在退出函数时不会出现悬挂的 goroutine 或资源泄露等问题。

			if err := runutil.Retry(2*time.Second, extLabelsCtx.Done(), func() error {
				if len(m.Labels()) == 0 {
					return errors.New("not uploading as no external labels are configured yet - is Prometheus healthy/reachable?") // 如果 Prometheus 服务不可用,则返回错误,并提示用户检查 Prometheus 服务是否可用。这个错误在 sidecar 启动时候,是不是很常见?
				}
				return nil
			}); err != nil {
				return errors.Wrapf(err, "aborting as no external labels found after waiting %s", promReadyTimeout) // 如果等待 Prometheus 服务变得可用超时,则返回错误。这将确保在尝试获取外部标签之前,Prometheus 服务已经准备好并可访问。如果在指定的时间内无法找到外部标签,这通常意味着配置有问题或服务不可用。在这种情况下,我们记录一条警告日志,并取消上传操作。
			}

			// 【关键模块】Shipper 定时同步本地到对象存储。该模块定期检查本地数据目录中的新块,并将它们上传到配置的对象存储服务。
			s := shipper.New(
				logger,                                   // 日志记录器
				reg,                                      // Prometheus 注册器
				conf.tsdb.path,                           // 本地 TSDB 数据路径
				bkt,                                      // 对象存储客户端,用于上传数据块到配置的对象存储服务。这将允许我们将本地 TSDB 数据目录中的新块上传到远程存储中,以便进行长期保存和查询。这将允许我们将本地 TSDB 数据目录中的新块上传到远程存储中,以便进行长期保存和查询。
				m.Labels,                                 // 外部标签函数,用于获取 Prometheus 的外部标签。这将允许我们为上传的数据块添加额外的元数据,以便在查询时能够正确地路由和过滤数据。
				metadata.SidecarSource,                   // 标识数据来源
				conf.shipper.uploadCompacted,             // 是否上传压缩块
				conf.shipper.allowOutOfOrderUpload,       // 是否允许无序上传
				metadata.HashFunc(conf.shipper.hashFunc) // 哈希函数,用于计算数据块的哈希值。这将允许我们验证上传的数据块是否完整且未被篡改。
			)

			return runutil.Repeat(30*time.Second, ctx.Done(), func() error {
				if uploaded, err := s.Sync(ctx); err != nil {
					level.Warn(logger).Log("err", err, "uploaded", uploaded)
				}

				minTime, _, err := s.Timestamps()
				if err != nil {
					level.Warn(logger).Log("msg", "reading timestamps failed", "err", err)
					return nil
				}
				m.UpdateTimestamps(minTime, math.MaxInt64)
				return nil
			})
		}, func(error) {
			cancel()
		})
	}

	level.Info(logger).Log("msg", "starting sidecar") // 记录一条信息日志,表示 sidecar 已启动。这将允许用户知道 sidecar 服务已经成功启动并正在运行中。
	return nil
}

// validatePrometheus 验证Prometheus配置是否正确
//
// 参数:
//     ctx: 上下文对象
//     client: Prometheus客户端
//     logger: 日志记录器
//     ignoreBlockSize: 是否忽略块大小配置
//     m: Prometheus元数据
//
// 返回值:
//     如果验证失败,返回错误对象;否则返回nil
// validatePrometheus的函数,其目的是验证Prometheus服务器的配置,特别是与TSDB(Time Series Database)相关的配置。这个函数主要用于确保Prometheus的某些设置符合特定的要求,这对于数据的一致性和完整性非常重要,特别是在与Thanos等系统集成时。
func validatePrometheus(
	ctx context.Context,  // 用于控制函数执行的上下文,允许取消操作或设置超时。
	client *promclient.Client, // Prometheus客户端,用于与Prometheus服务器通信。
	logger log.Logger, // 日志记录器,用于记录警告或错误信息。
	ignoreBlockSize bool, // 一个标志,指示是否忽略块大小(即TSDB最小和最大块持续时间)的不同。
	m *promMetadata	//  包含Prometheus相关元数据的结构体指针,这里主要用到了promURL字段。
	) error {	// 如果验证失败,返回一个错误。
	var (
		flagErr error
		flags   promclient.Flags
	)

	// 使用runutil.Retry函数尝试两次(每次间隔2秒)从Prometheus获取配置标志。如果因为网络问题或Prometheus暂时不可用导致失败,这可以提供一定的容错能力。
	if err := runutil.Retry(2*time.Second, ctx.Done(), func() error {
		// 获取Prometheus的配置标志,例如TSDB的最小和最大块持续时间。如果找不到这些标志(可能是因为Prometheus版本较旧),则记录警告并返回错误。
		if flags, flagErr = client.ConfiguredFlags(ctx, m.promURL); flagErr != nil && flagErr != promclient.ErrFlagEndpointNotFound {
			level.Warn(logger).Log("msg", "failed to get Prometheus flags. Is Prometheus running? Retrying", "err", flagErr)
			return errors.Wrapf(flagErr, "fetch Prometheus flags")
		}
		return nil
	}); err != nil { // 如果最终失败(即重试后仍然失败),返回错误。
		return errors.Wrapf(err, "fetch Prometheus flags")
	}

	// 如果flagErr不为nil但等于ErrFlagEndpointNotFound(意味着Prometheus版本较旧,不支持获取这些标志),记录警告信息但不返回错误。
	if flagErr != nil {
		level.Warn(logger).Log("msg", "failed to check Prometheus flags, due to potentially older Prometheus. No extra validation is done.", "err", flagErr)
		return nil
	}

	// Check if compaction is disabled.
	// 检查TSDB的MinTime和MaxTime:
	// 如果TSDBMinTime不等于TSDBMaxTime,表示禁用了压缩(因为最小和最大块持续时间不同)。如果ignoreBlockSize为false,则返回错误,提示用户需要禁用压缩(即设置最小和最大块持续时间相等)。
	// 如果使用了ignoreBlockSize标志,记录警告信息,提醒用户如果上传2小时的块失败且Prometheus发生压缩,则该块可能会从Thanos存储桶中丢失。
	if flags.TSDBMinTime != flags.TSDBMaxTime {
		if !ignoreBlockSize {
			return errors.Errorf("found that TSDB Max time is %s and Min time is %s. "+
				"Compaction needs to be disabled (storage.tsdb.min-block-duration = storage.tsdb.max-block-duration)", flags.TSDBMaxTime, flags.TSDBMinTime)
		}
		level.Warn(logger).Log("msg", "flag to ignore Prometheus min/max block duration flags differing is being used. If the upload of a 2h block fails and a Prometheus compaction happens that block may be missing from your Thanos bucket storage.")
	}
	// Check if block time is 2h.
	// 检查块时间是否为2小时:如果TSDB的MinTime(即块时间)不是2小时,记录警告信息,建议用户只使用2小时的块时间。
	if flags.TSDBMinTime != model.Duration(2*time.Hour) {
		level.Warn(logger).Log("msg", "found that TSDB block time is not 2h. Only 2h block time is recommended.", "block-time", flags.TSDBMinTime)
	}

	return nil
}

type promMetadata struct {
	promURL *url.URL

	mtx         sync.Mutex	// 用于同步的互斥锁
	mint        int64	// 最小时间戳
	maxt        int64	// 最大时间戳
	labels      labels.Labels	// 外部标签集合,用于标识 Prometheus 的实例和配置
	promVersion string // Prometheus 版本信息

	limitMinTime thanosmodel.TimeOrDurationValue // 限制最小时间戳,用于确保上传的块不会太旧

	client *promclient.Client // Prometheus 客户端,用于与 Prometheus API 进行交互
}

// promMetadata 结构体方法
func (s *promMetadata) UpdateLabels(ctx context.Context) error {
	// 从 Prometheus 获取最新 external_labels
	elset, err := s.client.ExternalLabels(ctx, s.promURL)
	if err != nil {
		return err
	}

	// 加锁确保线程安全更新
	s.mtx.Lock()
	defer s.mtx.Unlock() // 解锁,允许其他线程访问

	// 线程安全更新
	s.labels = elset
	return nil
}

func (s *promMetadata) UpdateTimestamps(mint, maxt int64) {
	s.mtx.Lock()	// 加锁确保线程安全更新
	defer s.mtx.Unlock() // 解锁,允许其他线程访问

	// [关键逻辑] 强制 mint 不低于 limitMinTime
	if mint < s.limitMinTime.PrometheusTimestamp() {
		mint = s.limitMinTime.PrometheusTimestamp()
	}

	s.mint = mint
	s.maxt = maxt
}

func (s *promMetadata) Labels() labels.Labels {
	// 加锁
	s.mtx.Lock()
	defer s.mtx.Unlock() // 延迟解锁

	// 返回标签
	return s.labels
}

func (s *promMetadata) Timestamps() (mint, maxt int64) {
	// 加锁
	s.mtx.Lock()
	defer s.mtx.Unlock()

	// 返回最小和最大时间戳
	return s.mint, s.maxt
}

func (s *promMetadata) BuildVersion(ctx context.Context) error {
	// 调用客户端的BuildVersion方法获取版本信息
	ver, err := s.client.BuildVersion(ctx, s.promURL)
	if err != nil {
		// 如果发生错误,返回错误
		return err
	}

	// 加锁,确保线程安全
	s.mtx.Lock()
	defer s.mtx.Unlock()

	// 更新Prometheus版本信息
	s.promVersion = ver
	return nil
}

func (s *promMetadata) Version() string {
	// 加锁
	s.mtx.Lock()
	defer s.mtx.Unlock() // 解锁

	// 返回版本信息
	return s.promVersion
}

type sidecarConfig struct {
	http         httpConfig // HTTP 配置结构体,包含HTTP服务器和客户端的配置信息
	grpc         grpcConfig // gRPC 配置结构体,包含gRPC服务器和客户端的配置信息
	prometheus   prometheusConfig // Prometheus 配置结构体,包含与Prometheus交互的配置信息
	tsdb         tsdbConfig // TSDB 配置结构体,包含TSDB的配置信息
	reloader     reloaderConfig // Reloader 配置结构体,包含Reloader的配置信息
	reqLogConfig *extflag.PathOrContent // 请求日志配置结构体,包含请求日志的配置信息
	objStore     extflag.PathOrContent // 对象存储配置结构体,包含与对象存储交互的配置信息
	shipper      shipperConfig // Shipper 配置结构体,包含与Shipper交互的配置信息
	limitMinTime thanosmodel.TimeOrDurationValue // 限制最小时间戳
}

func (sc *sidecarConfig) registerFlag(cmd extkingpin.FlagClause) {
	// 注册HTTP相关标志
	sc.http.registerFlag(cmd)
	// 注册gRPC相关标志
	sc.grpc.registerFlag(cmd)
	// 注册Prometheus相关标志
	sc.prometheus.registerFlag(cmd)
	// 注册TSDB相关标志
	sc.tsdb.registerFlag(cmd)
	// 注册Reloader相关标志
	sc.reloader.registerFlag(cmd)
	// 注册请求日志记录标志
	sc.reqLogConfig = extkingpin.RegisterRequestLoggingFlags(cmd)
	// 注册通用对象存储标志
	sc.objStore = *extkingpin.RegisterCommonObjStoreFlags(cmd, "", false)
	// 注册Shipper相关标志
	sc.shipper.registerFlag(cmd)
	// 注册时间范围限制标志
	cmd.Flag("min-time", "Start of time range limit to serve. Thanos sidecar will serve only metrics, which happened later than this value. Option can be a constant time in RFC3339 format or time duration relative to current time, such as -1d or 2h45m. Valid duration units are ms, s, m, h, d, w, y.").
		Default("0000-01-01T00:00:00Z").SetValue(&sc.limitMinTime)
}

 

  

 

posted @ 2025-01-24 15:30  左扬  阅读(49)  评论(0)    收藏  举报