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,其核心模块如下:
- 健康状态管理
- https://github.com/thanos-io/thanos/blob/v0.26.0/cmd/thanos/sidecar.go
- https://github.com/thanos-io/thanos/blob/v0.26.0/pkg/promclient/promclient.go
- https://github.com/thanos-io/thanos/blob/v0.26.0/pkg/prober/prober.go
- 查询代理
- 提供 StoreAPI 接口,允许 Querier 查询 Prometheus 的实时数据(未上传的 head chunk)
- 提供 HTTPAPI 查询接口
- 提供 gRPC 查询接口
- 实时监控 block 上传
- 持续监听 Prometheus 本地存储目录(/prometheus/data)
- 使用 Watch 机制检测新生成的 TSDB 块(目录格式为<ulid>)
- 对象存储
- 将生成的 block 上传到对象存储(如,s3、oss、cos等)
- 在 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
}

浙公网安备 33010602011771号