Thanos源码专题【左扬精讲】——Thanos Sidecar 组件(release-0.26)源码阅读和分析(第一章——概览介绍)

Thanos Sidecar 组件(release-0.26)源码阅读和分析(第一章——概览介绍)

https://github.com/thanos-io/thanos/blob/release-0.26

一、整体架构

  Thanos Sidecar 作为 Prometheus 的边车容器运行,代码位于 https://github.com/thanos-io/thanos/blob/v0.26.0/cmd/thanos/sidecar.go,其核心模块如下:

    1. 健康状态管理
      1. https://github.com/thanos-io/thanos/blob/v0.26.0/cmd/thanos/sidecar.go
      2. https://github.com/thanos-io/thanos/blob/v0.26.0/pkg/promclient/promclient.go
      3. https://github.com/thanos-io/thanos/blob/v0.26.0/pkg/prober/prober.go
    2. 查询代理
      1. 提供 StoreAPI 接口,允许 Querier 查询 Prometheus 的实时数据(未上传的 head chunk)
      2. 提供 HTTPAPI 查询接口
      3. 提供 gRPC 查询接口
    3. 实时监控 block 上传
      1. 持续监听 Prometheus 本地存储目录(/prometheus/data)
      2. 使用 Watch 机制检测新生成的 TSDB 块(目录格式为<ulid>)
    4. 对象存储
      1. 将生成的 block 上传到对象存储(如,s3、oss、cos等)
      2. 在 metadata 中添加 Thanos 扩展信息(external labels,downsample等)
// 主函数初始化流程
func main() {
    // 1. 配置解析
    cfg := parseFlags() 
    
    // 2. Prometheus 健康检查系统
    prober := prober.NewHTTP()
    statusProber := prober.Probe()
    
    // 3. 对象存储客户端初始化
    bucketClient, err := client.NewBucket(...)
    
    // 4. 块上传控制器
    uploader := block.NewUploader(...)
    
    // 5. StoreAPI 服务初始化
    store := store.NewPrometheusStore(...)
    
    // 6. gRPC 服务器启动
    gRPCServer := grpc.NewServer(...)
    storepb.RegisterStoreServer(gRPCServer, store)
}

二、核心功能源码分析

2.1、健康状态管理

2.1.1、cmd/sidecar模块

在 https://github.com/thanos-io/thanos/blob/v0.26.0/cmd/thanos/sidecar.go 中,Sidecar 的主要实现

func runSidecar(
    g *run.Group,
    logger log.Logger,
    reg *prometheus.Registry,
    tracer opentracing.Tracer,
    conf *sidecarConfig,
    comp component.Component,
) error {
    // 创建 Prometheus 客户端
    promClient, err := promclient.NewClient(logger, conf.prometheus.client)
    if err != nil {
        return errors.Wrap(err, "create prometheus client")
    }

    // 创建探针
    statusProber := prober.NewProber(comp, logger, reg)
    httpProbe := statusProber.NewProbe("http")
    grpcProbe := statusProber.NewProbe("grpc")

    // 启动 HTTP 服务
    {
        srv := httpserver.New(logger, reg, comp, httpProbe,
            httpserver.WithListen(conf.http.bindAddress),
            httpserver.WithGracePeriod(time.Duration(conf.http.gracePeriod)))

        g.Add(func() error {
            statusProber.Ready()
            return srv.ListenAndServe()
        }, func(err error) {
            statusProber.NotReady(err)
            srv.Shutdown(err)
        })
    }

    // 启动 gRPC 服务
    {
        s := grpcserver.New(logger, reg, tracer, comp, grpcProbe,
            grpcserver.WithListen(conf.grpc.bindAddress),
            grpcserver.WithGracePeriod(time.Duration(conf.grpc.gracePeriod)))

        g.Add(func() error {
            statusProber.Ready()
            return s.ListenAndServe()
        }, func(err error) {
            statusProber.NotReady(err)
            s.Shutdown(err)
        })
    }
}  

2.1.2、pkg/prober/prober.go - 探针的核心实现

在 https://github.com/thanos-io/thanos/blob/v0.26.0/pkg/prober/prober.go 中实现了 Probe 探针:

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

package prober

// Prober represents health and readiness status of given component.
//
// From Kubernetes documentation https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/ :
//
//   liveness: Many applications running for long periods of time eventually transition to broken states,
//   (healthy) and cannot recover except by being restarted.
//             Kubernetes provides liveness probes to detect and remedy such situations.
//
//   readiness: Sometimes, applications are temporarily unable to serve traffic.
//   (ready)    For example, an application might need to load large data or configuration files during startup,
//              or depend on external services after startup. In such cases, you don’t want to kill the application,
//              but you don’t want to send it requests either. Kubernetes provides readiness probes to detect
//              and mitigate these situations. A pod with containers reporting that they are not ready
//              does not receive traffic through Kubernetes Services.
type Probe interface {
	Healthy()	// 标记组件为健康
	NotHealthy(err error)	// 标记组件为不健康
	Ready()	// 标记组件为就绪
	NotReady(err error)	// 标记组件为不就绪
}

2.1.3、Prometheus 探活实现部分

在 https://github.com/thanos-io/thanos/blob/release-0.26/pkg/promclient/promclient.go 中实现了对 Prometheus 的健康检查:

// 68行-73行
// Client represents a Prometheus API client.
type Client struct {
	HTTPClient
	userAgent string
	logger    log.Logger
}

……

// 654行-693行,用于实现健康检查
// 函数签名和用途,这是一个 Client 结构体的方法,主要是发送 GET 请求并处理 2XX 响应,支持 tracing,并将 HTTP 错误转为 gRPC 错误码
func (c *Client) get2xxResultWithGRPCErrors(ctx context.Context, spanName string, u *url.URL, data interface{}) error {
	span, ctx := tracing.StartSpan(ctx, spanName) // 创建 tracing span 用于监控请求
	defer span.Finish() // 关闭 span

	body, code, err := c.req2xx(ctx, u, http.MethodGet) // 发送 GET 请求
	if err != nil {
		if code, exists := statusToCode[code]; exists && code != 0 {
			return status.Error(code, err.Error()) // 如果请求失败,将 HTTP 状态码映射为对应的 gRPC 状态码
		}
		return status.Error(codes.Internal, err.Error()) // 如果映射 gRPC 错误码映射失败,则返回 Internal{} 以支持不同类型的响应数据
	}
     
	if code == http.StatusNoContent { // 处理空响应,我理解是为了应对 http 204 这种
		return nil
	}

	var m struct {  // 解析 response
		Data   interface{} `json:"data"`
		Status string      `json:"status"`
		Error  string      `json:"error"`
	}

	if err = json.Unmarshal(body, &m); err != nil {
		return status.Error(codes.Internal, err.Error())
	}

	if m.Status != SUCCESS {  // 对 response.status 进行状态检查
		code, exists := statusToCode[code]
		if !exists {  // 如果不是成功,则将错误码映射成相应的 gRPC 错误
			return status.Error(codes.Internal, m.Error)
		}
		return status.Error(code, m.Error)
	}

	if err = json.Unmarshal(body, &data); err != nil {
		return status.Error(codes.Internal, err.Error())
	}

	return nil
}

2.2、查询代理

2.2.1、HTTP API 封装 

在 https://github.com/thanos-io/thanos/blob/v0.26.0/pkg/api/query/v1.go 中封装了 Prometheus V1 的 HTTP API: 

// QueryAPI is an API used by Thanos Querier.
type QueryAPI struct {
	baseAPI         *api.BaseAPI
	logger          log.Logger
	gate            gate.Gate
	queryableCreate query.QueryableCreator
	// queryEngine returns appropriate promql.Engine for a query with a given step.
	queryEngine func(int64) *promql.Engine
	ruleGroups  rules.UnaryClient
	targets     targets.UnaryClient
	metadatas   metadata.UnaryClient
	exemplars   exemplars.UnaryClient

	enableAutodownsampling              bool
	enableQueryPartialResponse          bool
	enableRulePartialResponse           bool
	enableTargetPartialResponse         bool
	enableMetricMetadataPartialResponse bool
	enableExemplarPartialResponse       bool
	enableQueryPushdown                 bool
	disableCORS                         bool

	replicaLabels  []string
	endpointStatus func() []query.EndpointStatus

	defaultRangeQueryStep                  time.Duration
	defaultInstantQueryMaxSourceResolution time.Duration
	defaultMetadataTimeRange               time.Duration

	queryRangeHist prometheus.Histogram
}
// 查询接口的实现流程
func (qapi *QueryAPI) query(r *http.Request) (interface{}, []error, *api.ApiError) {
	// 解析查询时间参数,如果未指定则使用当前时间
	ts, err := parseTimeParam(r, "time", qapi.baseAPI.Now())
	if err != nil {
		return nil, nil, &api.ApiError{Typ: api.ErrorBadData, Err: err}
	}
 
	// 获取请求的上下文
	ctx := r.Context()
	
	// 自定义超时配置
	if to := r.FormValue("timeout"); to != "" {
		var cancel context.CancelFunc
		timeout, err := parseDuration(to)
		if err != nil {
			return nil, nil, &api.ApiError{Typ: api.ErrorBadData, Err: err}
		}
 
		// 设置上下文超时
		ctx, cancel = context.WithTimeout(ctx, timeout)
		defer cancel() // 确保在函数返回时取消上下文
	}
 
	// 解析去重参数
	enableDedup, apiErr := qapi.parseEnableDedupParam(r)
	if apiErr != nil {
		return nil, nil, apiErr
	}
 
	// 解析副本标签参数
	replicaLabels, apiErr := qapi.parseReplicaLabelsParam(r)
	if apiErr != nil {
		return nil, nil, apiErr
	}
 
	// 解析存储调试匹配器参数
	storeDebugMatchers, apiErr := qapi.parseStoreDebugMatchersParam(r)
	if apiErr != nil {
		return nil, nil, apiErr
	}
 
	// 解析部分响应参数
	enablePartialResponse, apiErr := qapi.parsePartialResponseParam(r, qapi.enableQueryPartialResponse)
	if apiErr != nil {
		return nil, nil, apiErr
	}
 
	// 解析下采样参数
	maxSourceResolution, apiErr := qapi.parseDownsamplingParamMillis(r, qapi.defaultInstantQueryMaxSourceResolution)
	if apiErr != nil {
		return nil, nil, apiErr
	}
 
	// 获取查询引擎
	qe := qapi.queryEngine(maxSourceResolution)
 
	// 开始 PromQL 追踪 span,因为我们无法控制 PromQL 代码
	span, ctx := tracing.StartSpan(ctx, "promql_instant_query")
	defer span.Finish() // 确保 span 在函数返回时完成
 
	// 创建新的即时查询
	qry, err := qe.NewInstantQuery(
		qapi.queryableCreate(enableDedup, replicaLabels, storeDebugMatchers, maxSourceResolution, enablePartialResponse, qapi.enableQueryPushdown, false),
		r.FormValue("query"),
		ts,
	)
	if err != nil {
		return nil, nil, &api.ApiError{Typ: api.ErrorBadData, Err: err}
	}
 
	// 查询限流器:检查是否允许开始查询
	tracing.DoInSpan(ctx, "query_gate_ismyturn", func(ctx context.Context) {
		err = qapi.gate.Start(ctx)
	})
	if err != nil {
		return nil, nil, &api.ApiError{Typ: api.ErrorExec, Err: err}
	}
	defer qapi.gate.Done() // 确保在函数返回时释放限流器
 
	// 执行查询
	res := qry.Exec(ctx)
	if res.Err != nil {
		// 根据错误类型返回不同的 API 错误
		switch res.Err.(type) {
		case promql.ErrQueryCanceled:
			return nil, nil, &api.ApiError{Typ: api.ErrorCanceled, Err: res.Err}
		case promql.ErrQueryTimeout:
			return nil, nil, &api.ApiError{Typ: api.ErrorTimeout, Err: res.Err}
		case promql.ErrStorage:
			return nil, nil, &api.ApiError{Typ: api.ErrorInternal, Err: res.Err}
		}
		return nil, nil, &api.ApiError{Typ: api.ErrorExec, Err: res.Err}
	}
 
	// 如果请求中包含 "stats" 参数,则返回查询统计信息
	var qs *stats.QueryStats
	if r.FormValue(Stats) != "" {
		qs = stats.NewQueryStats(qry.Stats())
	}
 
	// 返回查询结果、警告和错误信息(如果有)
	return &queryData{
		ResultType: res.Value.Type(),
		Result:     res.Value,
		Stats:      qs,
	}, res.Warnings, nil
}  

2.2.2、gRPC Server 实现

在 https://github.com/thanos-io/thanos/blob/v0.26.0/pkg/api/query/grpc.go 中实现了 gRPC 服务:

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

package v1

import (
    "context"
    "time"

    "github.com/prometheus/prometheus/promql" // PromQL查询引擎
    "github.com/thanos-io/thanos/pkg/api/query/querypb" // Thanos查询服务的protobuf定义
    "github.com/thanos-io/thanos/pkg/query" // Thanos查询相关的工具函数
    "github.com/thanos-io/thanos/pkg/store/labelpb" // 标签处理的工具
    "github.com/thanos-io/thanos/pkg/store/storepb/prompb" // Prometheus数据格式的protobuf定义
    "google.golang.org/grpc" // gRPC库
)

// 定义GRPCAPI结构体,包含处理查询所需的各种依赖和方法。
type GRPCAPI struct {
    now                         func() time.Time // 获取当前时间的函数
    queryableCreate             query.QueryableCreator // 创建Queryable实例的函数
    queryEngine                 func(int64) *promql.Engine // 根据最大分辨率创建PromQL引擎的函数
    defaultMaxResolutionSeconds time.Duration // 默认的最大分辨率时间
}

// NewGRPCAPI是GRPCAPI的构造函数,用于初始化一个GRPCAPI实例。
func NewGRPCAPI(now func() time.Time, creator query.QueryableCreator, queryEngine func(int64) *promql.Engine, defaultMaxResolutionSeconds time.Duration) *GRPCAPI {
    return &GRPCAPI{
        now:                         now,
        queryableCreate:             creator,
        queryEngine:                 queryEngine,
        defaultMaxResolutionSeconds: defaultMaxResolutionSeconds,
    }
}

// RegisterQueryServer是一个高阶函数,用于将查询服务器注册到gRPC服务器。
func RegisterQueryServer(queryServer querypb.QueryServer) func(*grpc.Server) {
    return func(s *grpc.Server) {
        querypb.RegisterQueryServer(s, queryServer) // 使用protobuf生成的注册函数注册服务器
    }
}

// Query是GRPCAPI的方法,用于处理即时查询请求。
func (g *GRPCAPI) Query(request *querypb.QueryRequest, server querypb.Query_QueryServer) error {
    ctx := context.Background() // 创建一个 Context,常用于顶层或根 Context 使用
    var ts time.Time
    if request.TimeSeconds == 0 {
        ts = g.now() // 如果请求中没有指定时间,使用当前时间
    } else {
        ts = time.Unix(request.TimeSeconds, 0) // 否则,使用请求中的时间
    }

    if request.TimeoutSeconds != 0 {
        var cancel context.CancelFunc
        timeout := time.Duration(request.TimeoutSeconds) * time.Second // 根据请求设置超时
        ctx, cancel = context.WithTimeout(ctx, timeout) // 创建一个带超时的上下文
        defer cancel() // 确保函数退出时取消上下文,防止资源泄露
    }

    maxResolution := request.MaxResolutionSeconds
    if request.MaxResolutionSeconds == 0 {
        maxResolution = g.defaultMaxResolutionSeconds.Milliseconds() / 1000 // 使用默认的最大分辨率
    }

    storeMatchers, err := querypb.StoreMatchersToLabelMatchers(request.StoreMatchers) // 将请求中的存储匹配器转换为标签匹配器
    if err != nil {
        return err // 如果转换失败,返回错误
    }

    qe := g.queryEngine(request.MaxResolutionSeconds) // 根据最大分辨率创建查询引擎
    queryable := g.queryableCreate( // 创建Queryable实例
        request.EnableDedup,
        request.ReplicaLabels,
        storeMatchers,
        maxResolution,
        request.EnablePartialResponse,
        request.EnableQueryPushdown,
        false,
    )
    qry, err := qe.NewInstantQuery(queryable, request.Query, ts) // 创建即时查询
    if err != nil {
        return err // 如果创建查询失败,返回错误
    }

    result := qry.Exec(ctx) // 执行查询
    if err := server.Send(querypb.NewQueryWarningsResponse(result.Warnings)); err != nil {
        return nil // 如果发送警告失败,返回nil(这里应该返回err,可能是代码的一个bug)
    }

    // 根据查询结果的类型处理结果
    switch vector := result.Value.(type) {
    case promql.Scalar: // 标量结果
        series := &prompb.TimeSeries{
            Samples: []prompb.Sample{{Value: vector.V, Timestamp: vector.T}},
        }
        if err := server.Send(querypb.NewQueryResponse(series)); err != nil {
            return err // 如果发送结果失败,返回错误
        }
    case promql.Vector: // 向量结果
        for _, sample := range vector {
            series := &prompb.TimeSeries{
                Labels:  labelpb.ZLabelsFromPromLabels(sample.Metric),
                Samples: prompb.SamplesFromPromqlPoints([]promql.Point{sample.Point}),
            }
            if err := server.Send(querypb.NewQueryResponse(series)); err != nil {
                return err // 如果发送结果失败,返回错误
            }
        }

        return nil // 查询成功完成
    }

    return nil // 如果没有匹配的类型,也返回nil(通常不会到达这里)
}

// QueryRange是GRPCAPI的方法,用于处理范围查询请求。
func (g *GRPCAPI) QueryRange(request *querypb.QueryRangeRequest, srv querypb.Query_QueryRangeServer) error {
    ctx := context.Background() // 创建一个背景上下文
    if request.TimeoutSeconds != 0 {
        var cancel context.CancelFunc
        ctx, cancel = context.WithTimeout(ctx, time.Duration(request.TimeoutSeconds)*time.Second) // 设置超时
        defer cancel() // 确保函数退出时取消上下文
    }

    maxResolution := request.MaxResolutionSeconds
    if request.MaxResolutionSeconds == 0 {
        maxResolution = g.defaultMaxResolutionSeconds.Milliseconds() / 1000 // 使用默认的最大分辨率
    }

    storeMatchers, err := querypb.StoreMatchersToLabelMatchers(request.StoreMatchers) // 转换存储匹配器
    if err != nil {
        return err // 如果转换失败,返回错误
    }

    qe := g.queryEngine(request.MaxResolutionSeconds) // 创建查询引擎
    queryable := g.queryableCreate( // 创建Queryable实例
        request.EnableDedup,
        request.ReplicaLabels,
        storeMatchers,
        maxResolution,
        request.EnablePartialResponse,
        request.EnableQueryPushdown,
        false,
    )

    startTime := time.Unix(request.StartTimeSeconds, 0) // 开始时间
    endTime := time.Unix(request.EndTimeSeconds, 0) // 结束时间
    interval := time.Duration(request.IntervalSeconds) * time.Second // 查询间隔

    qry, err := qe.NewRangeQuery(queryable, request.Query, startTime, endTime, interval) // 创建范围查询
    if err != nil {
        return err // 如果创建查询失败,返回错误
    }

    result := qry.Exec(ctx) // 执行查询
    if err := srv.Send(querypb.NewQueryRangeWarningsResponse(result.Warnings)); err != nil {
        return err // 如果发送警告失败,返回错误
    }

    // 根据查询结果的类型处理结果
    switch matrix := result.Value.(type) {
    case promql.Matrix: // 矩阵结果
        for _, series := range matrix {
            series := &prompb.TimeSeries{
                Labels:  labelpb.ZLabelsFromPromLabels(series.Metric),
                Samples: prompb.SamplesFromPromqlPoints(series.Points),
            }
            if err := srv.Send(querypb.NewQueryRangeResponse(series)); err != nil {
                return err // 如果发送结果失败,返回错误
            }
        }

        return nil // 查询成功完成
    }

    return nil // 如果没有匹配的类型,也返回nil(通常不会到达这里)
}

2.3、Sidecar 数据上传实现

在 https://github.com/thanos-io/thanos/blob/v0.26.0/pkg/block/block.go 中实现了 block 上传功能:

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

// Package block 包含与TSDB块交互的通用功能
// 在Thanos的上下文中处理块相关操作
package block

import (
	"bytes"
	"context"
	"encoding/json"
	"io/ioutil"  // Go 1.16+ 推荐使用 io 和 os 包替代
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/go-kit/log"       // 结构化日志库
	"github.com/go-kit/log/level" // 日志级别控制
	"github.com/oklog/ulid"       // 唯一时序ID生成
	"github.com/pkg/errors"       // 增强错误处理
	"github.com/prometheus/client_golang/prometheus" // 指标采集

	"github.com/thanos-io/thanos/pkg/block/metadata" // block 元数据操作
	"github.com/thanos-io/thanos/pkg/objstore"       // 对象存储抽象层
	"github.com/thanos-io/thanos/pkg/runutil"        // 实用工具函数
)

// 常量定义区块相关文件名和目录结构
const (
	MetaFilename         = "meta.json"        // 元数据文件名
	IndexFilename        = "index"            // 索引文件名
	IndexHeaderFilename  = "index-header"     // 索引头文件名
	ChunksDirname        = "chunks"           // 块数据目录名
	DebugMetas           = "debug/metas"      // 调试元数据目录
)

// ========== 下载相关 ==========
// Download 从对象存储下载整个块到本地目录
// ctx: 上下文控制(超时/取消)
// logger: 结构化日志记录器
// bucket: 对象存储抽象接口
// id: 块的ULID标识
// dst: 本地目标路径
func Download(ctx context.Context, logger log.Logger, bucket objstore.Bucket, id ulid.ULID, dst string) error {
	// 创建目标目录(权限755)
	if err := os.MkdirAll(dst, 0750); err != nil {
		return errors.Wrap(err, "create dir") // 错误包装增加上下文
	}

	// 下载元数据文件(核心操作)
	if err := objstore.DownloadFile(ctx, logger, bucket, path.Join(id.String(), MetaFilename), path.Join(dst, MetaFilename)); err != nil {
		return err
	}

	// 读取本地元数据
	m, err := metadata.ReadFromDir(dst)
	if err != nil {
		return errors.Wrapf(err, "reading meta from %s", dst)
	}

	// 需要忽略的文件列表(已存在的正确文件)
	ignoredPaths := []string{MetaFilename}
	for _, fl := range m.Thanos.Files {
		// 跳过没有哈希或哈希类型为None的文件
		if fl.Hash == nil || fl.Hash.Func == metadata.NoneFunc || fl.RelPath == "" {
			continue
		}

		// 计算本地文件的哈希值
		actualHash, err := metadata.CalculateHash(
			filepath.Join(dst, fl.RelPath),
			fl.Hash.Func,
			logger,
		)
		if err != nil {
			// 哈希计算失败时重新下载
			level.Info(logger).Log("msg", "failed to calculate hash when downloading; re-downloading", "relPath", fl.RelPath, "err", err)
			continue
		}

		// 哈希匹配则加入忽略列表
		if fl.Hash.Equal(&actualHash) {
			ignoredPaths = append(ignoredPaths, fl.RelPath)
		}
	}

	// 下载剩余文件(排除已存在的)
	if err := objstore.DownloadDir(ctx, logger, bucket, id.String(), id.String(), dst, ignoredPaths...); err != nil {
		return err
	}

	// 确保chunks目录存在(处理空块情况)
	chunksDir := filepath.Join(dst, ChunksDirname)
	if _, err := os.Stat(chunksDir); os.IsNotExist(err) {
		return os.Mkdir(chunksDir, os.ModePerm)
	}
	return nil
}

// ========== 上传相关 ==========
// Upload 上传本地块到对象存储(校验Thanos外部标签)
func Upload(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc) error {
	return upload(ctx, logger, bkt, bdir, hf, true)
}

// UploadPromBlock 上传Prometheus原生块(不校验外部标签)
func UploadPromBlock(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc) error {
	return upload(ctx, logger, bkt, bdir, hf, false)
}

// 实际的上传实现
func upload(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc, checkExternalLabels bool) error {
	// 验证目录结构
	df, err := os.Stat(bdir)
	if err != nil {
		return err
	}
	if !df.IsDir() {
		return errors.Errorf("%s is not a directory", bdir)
	}

	// 解析ULID(验证是否为有效块目录)
	id, err := ulid.Parse(df.Name())
	if err != nil {
		return errors.Wrap(err, "not a block dir")
	}

	// 读取元数据
	meta, err := metadata.ReadFromDir(bdir)
	if err != nil {
		return errors.Wrap(err, "read meta")
	}

	// 校验外部标签(Thanos特有)
	if checkExternalLabels && (meta.Thanos.Labels == nil || len(meta.Thanos.Labels) == 0) {
		return errors.New("empty external labels are not allowed for Thanos block.")
	}

	// 收集文件统计信息(哈希值等)
	meta.Thanos.Files, err = gatherFileStats(bdir, hf, logger)
	if err != nil {
		return errors.Wrap(err, "gather meta file stats")
	}

	// 编码元数据到内存缓冲区
	var metaEncoded bytes.Buffer
	if err := meta.Write(&metaEncoded); err != nil {
		return errors.Wrap(err, "encode meta file")
	}

	// 上传chunks目录(并发控制由objstore内部处理)
	if err := objstore.UploadDir(ctx, logger, bkt, 
		filepath.Join(bdir, ChunksDirname), 
		path.Join(id.String(), ChunksDirname)); err != nil {
		return cleanUp(logger, bkt, id, errors.Wrap(err, "upload chunks"))
	}

	// 上传索引文件
	if err := objstore.UploadFile(ctx, logger, bkt, 
		filepath.Join(bdir, IndexFilename), 
		path.Join(id.String(), IndexFilename)); err != nil {
		return cleanUp(logger, bkt, id, errors.Wrap(err, "upload index"))
	}

	// 最后上传meta.json(确保原子性)
	if err := bkt.Upload(ctx, path.Join(id.String(), MetaFilename), &metaEncoded); err != nil {
		return errors.Wrap(err, "upload meta file")
	}

	return nil
}

// ========== 清理相关 ==========
// cleanUp 上传失败时的清理操作
func cleanUp(logger log.Logger, bkt objstore.Bucket, id ulid.ULID, origErr error) error {
	// 使用不可取消的上下文确保清理完成
	ctx := context.Background()
	if err := Delete(ctx, logger, bkt, id); err != nil {
		// 包装原始错误和清理错误
		return errors.Wrapf(origErr, 
			"failed to clean block after upload issue. Partial block in system. Err: %s", 
			err.Error())
	}
	return origErr
}
posted @ 2025-01-22 11:42  左扬  阅读(53)  评论(0)    收藏  举报