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等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 主函数初始化流程
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 的主要实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 探针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 的健康检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 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:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// 查询接口的实现流程
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 服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// 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 上传功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
// 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 @   左扬  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
历史上的今天:
2024-01-22 《PMBOK指南第六版》第4章 项目整合管理 -> 监控项目工作
levels of contents
点击右上角即可分享
微信分享提示