github.com/thanos-io/thanos@v0.32.5/pkg/replicate/scheme.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package replicate
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"path"
    12  	"sort"
    13  
    14  	"github.com/go-kit/log"
    15  	"github.com/go-kit/log/level"
    16  	"github.com/oklog/ulid"
    17  	"github.com/pkg/errors"
    18  	"github.com/prometheus/client_golang/prometheus"
    19  	"github.com/prometheus/client_golang/prometheus/promauto"
    20  	"github.com/prometheus/prometheus/model/labels"
    21  
    22  	"github.com/thanos-io/objstore"
    23  
    24  	thanosblock "github.com/thanos-io/thanos/pkg/block"
    25  	"github.com/thanos-io/thanos/pkg/block/metadata"
    26  	"github.com/thanos-io/thanos/pkg/compact"
    27  	"github.com/thanos-io/thanos/pkg/runutil"
    28  	"github.com/thanos-io/thanos/pkg/store/storepb"
    29  )
    30  
    31  // BlockFilter is block filter that filters out compacted and unselected blocks.
    32  type BlockFilter struct {
    33  	logger           log.Logger
    34  	labelSelector    labels.Selector
    35  	labelSelectorStr string
    36  	resolutionLevels map[compact.ResolutionLevel]struct{}
    37  	compactionLevels map[int]struct{}
    38  	blockIDs         []ulid.ULID
    39  }
    40  
    41  // NewBlockFilter returns block filter.
    42  func NewBlockFilter(
    43  	logger log.Logger,
    44  	labelSelector labels.Selector,
    45  	resolutionLevels []compact.ResolutionLevel,
    46  	compactionLevels []int,
    47  	blockIDs []ulid.ULID,
    48  ) *BlockFilter {
    49  	allowedResolutions := make(map[compact.ResolutionLevel]struct{})
    50  	for _, resolutionLevel := range resolutionLevels {
    51  		allowedResolutions[resolutionLevel] = struct{}{}
    52  	}
    53  	allowedCompactions := make(map[int]struct{})
    54  	for _, compactionLevel := range compactionLevels {
    55  		allowedCompactions[compactionLevel] = struct{}{}
    56  	}
    57  
    58  	return &BlockFilter{
    59  		labelSelector:    labelSelector,
    60  		labelSelectorStr: storepb.PromMatchersToString(labelSelector...),
    61  		logger:           logger,
    62  		resolutionLevels: allowedResolutions,
    63  		compactionLevels: allowedCompactions,
    64  		blockIDs:         blockIDs,
    65  	}
    66  }
    67  
    68  // Filter return true if block is non-compacted and matches selector.
    69  func (bf *BlockFilter) Filter(b *metadata.Meta) bool {
    70  	if len(b.Thanos.Labels) == 0 {
    71  		level.Error(bf.logger).Log("msg", "filtering block", "reason", "labels should not be empty")
    72  		return false
    73  	}
    74  
    75  	// If required block IDs are set, we only match required blocks and ignore others.
    76  	if len(bf.blockIDs) > 0 {
    77  		for _, id := range bf.blockIDs {
    78  			if b.ULID == id {
    79  				return true
    80  			}
    81  		}
    82  		return false
    83  	}
    84  
    85  	blockLabels := labels.FromMap(b.Thanos.Labels)
    86  
    87  	labelMatch := bf.labelSelector.Matches(blockLabels)
    88  	if !labelMatch {
    89  		level.Debug(bf.logger).Log("msg", "filtering block", "reason", "labels don't match", "block_labels", blockLabels.String(), "selector", bf.labelSelectorStr)
    90  		return false
    91  	}
    92  
    93  	gotResolution := compact.ResolutionLevel(b.Thanos.Downsample.Resolution)
    94  	if _, ok := bf.resolutionLevels[gotResolution]; !ok {
    95  		level.Info(bf.logger).Log("msg", "filtering block", "reason", "resolution doesn't match allowed resolutions", "got_resolution", gotResolution, "allowed_resolutions", fmt.Sprintf("%v", bf.resolutionLevels))
    96  		return false
    97  	}
    98  
    99  	gotCompactionLevel := b.BlockMeta.Compaction.Level
   100  	if _, ok := bf.compactionLevels[gotCompactionLevel]; !ok {
   101  		level.Info(bf.logger).Log("msg", "filtering block", "reason", "compaction level doesn't match allowed levels", "got_compaction_level", gotCompactionLevel, "allowed_compaction_levels", fmt.Sprintf("%v", bf.compactionLevels))
   102  		return false
   103  	}
   104  
   105  	return true
   106  }
   107  
   108  type blockFilterFunc func(b *metadata.Meta) bool
   109  
   110  // TODO: Add filters field.
   111  type replicationScheme struct {
   112  	fromBkt objstore.InstrumentedBucketReader
   113  	toBkt   objstore.Bucket
   114  
   115  	blockFilter blockFilterFunc
   116  	fetcher     thanosblock.MetadataFetcher
   117  
   118  	logger  log.Logger
   119  	metrics *replicationMetrics
   120  
   121  	reg prometheus.Registerer
   122  }
   123  
   124  type replicationMetrics struct {
   125  	blocksAlreadyReplicated prometheus.Counter
   126  	blocksReplicated        prometheus.Counter
   127  	objectsReplicated       prometheus.Counter
   128  }
   129  
   130  func newReplicationMetrics(reg prometheus.Registerer) *replicationMetrics {
   131  	m := &replicationMetrics{
   132  		blocksAlreadyReplicated: promauto.With(reg).NewCounter(prometheus.CounterOpts{
   133  			Name: "thanos_replicate_blocks_already_replicated_total",
   134  			Help: "Total number of blocks skipped due to already being replicated.",
   135  		}),
   136  		blocksReplicated: promauto.With(reg).NewCounter(prometheus.CounterOpts{
   137  			Name: "thanos_replicate_blocks_replicated_total",
   138  			Help: "Total number of blocks replicated.",
   139  		}),
   140  		objectsReplicated: promauto.With(reg).NewCounter(prometheus.CounterOpts{
   141  			Name: "thanos_replicate_objects_replicated_total",
   142  			Help: "Total number of objects replicated.",
   143  		}),
   144  	}
   145  	return m
   146  }
   147  
   148  func newReplicationScheme(
   149  	logger log.Logger,
   150  	metrics *replicationMetrics,
   151  	blockFilter blockFilterFunc,
   152  	fetcher thanosblock.MetadataFetcher,
   153  	from objstore.InstrumentedBucketReader,
   154  	to objstore.Bucket,
   155  	reg prometheus.Registerer,
   156  ) *replicationScheme {
   157  	if logger == nil {
   158  		logger = log.NewNopLogger()
   159  	}
   160  
   161  	return &replicationScheme{
   162  		logger:      logger,
   163  		blockFilter: blockFilter,
   164  		fetcher:     fetcher,
   165  		fromBkt:     from,
   166  		toBkt:       to,
   167  		metrics:     metrics,
   168  		reg:         reg,
   169  	}
   170  }
   171  
   172  func (rs *replicationScheme) execute(ctx context.Context) error {
   173  	availableBlocks := []*metadata.Meta{}
   174  
   175  	metas, partials, err := rs.fetcher.Fetch(ctx)
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	for id := range partials {
   181  		level.Info(rs.logger).Log("msg", "block meta not uploaded yet. Skipping.", "block_uuid", id.String())
   182  	}
   183  
   184  	for id, meta := range metas {
   185  		if rs.blockFilter(meta) {
   186  			level.Info(rs.logger).Log("msg", "adding block to be replicated", "block_uuid", id.String())
   187  			availableBlocks = append(availableBlocks, meta)
   188  		}
   189  	}
   190  
   191  	// In order to prevent races in compactions by the target environment, we
   192  	// need to replicate oldest start timestamp first.
   193  	sort.Slice(availableBlocks, func(i, j int) bool {
   194  		return availableBlocks[i].BlockMeta.MinTime < availableBlocks[j].BlockMeta.MinTime
   195  	})
   196  
   197  	for _, b := range availableBlocks {
   198  		if err := rs.ensureBlockIsReplicated(ctx, b.BlockMeta.ULID); err != nil {
   199  			return errors.Wrapf(err, "ensure block %v is replicated", b.BlockMeta.ULID.String())
   200  		}
   201  	}
   202  
   203  	return nil
   204  }
   205  
   206  // ensureBlockIsReplicated ensures that a block present in the origin bucket is
   207  // present in the target bucket.
   208  func (rs *replicationScheme) ensureBlockIsReplicated(ctx context.Context, id ulid.ULID) error {
   209  	blockID := id.String()
   210  	chunksDir := path.Join(blockID, thanosblock.ChunksDirname)
   211  	indexFile := path.Join(blockID, thanosblock.IndexFilename)
   212  	metaFile := path.Join(blockID, thanosblock.MetaFilename)
   213  
   214  	level.Debug(rs.logger).Log("msg", "ensuring block is replicated", "block_uuid", blockID)
   215  
   216  	originMetaFile, err := rs.fromBkt.ReaderWithExpectedErrs(rs.fromBkt.IsObjNotFoundErr).Get(ctx, metaFile)
   217  	if err != nil {
   218  		return errors.Wrap(err, "get meta file from origin bucket")
   219  	}
   220  
   221  	defer runutil.CloseWithLogOnErr(rs.logger, originMetaFile, "close original meta file")
   222  
   223  	targetMetaFile, err := rs.toBkt.Get(ctx, metaFile)
   224  
   225  	if targetMetaFile != nil {
   226  		defer runutil.CloseWithLogOnErr(rs.logger, targetMetaFile, "close target meta file")
   227  	}
   228  
   229  	if err != nil && !rs.toBkt.IsObjNotFoundErr(err) && err != io.EOF {
   230  		return errors.Wrap(err, "get meta file from target bucket")
   231  	}
   232  
   233  	// TODO(bwplotka): Allow injecting custom labels as shipper does.
   234  	originMetaFileContent, err := io.ReadAll(originMetaFile)
   235  	if err != nil {
   236  		return errors.Wrap(err, "read origin meta file")
   237  	}
   238  
   239  	if targetMetaFile != nil && !rs.toBkt.IsObjNotFoundErr(err) {
   240  		targetMetaFileContent, err := io.ReadAll(targetMetaFile)
   241  		if err != nil {
   242  			return errors.Wrap(err, "read target meta file")
   243  		}
   244  
   245  		if bytes.Equal(originMetaFileContent, targetMetaFileContent) {
   246  			// If the origin meta file content and target meta file content is
   247  			// equal, we know we have already successfully replicated
   248  			// previously.
   249  			level.Debug(rs.logger).Log("msg", "skipping block as already replicated", "block_uuid", blockID)
   250  			rs.metrics.blocksAlreadyReplicated.Inc()
   251  
   252  			return nil
   253  		}
   254  	}
   255  
   256  	if err := rs.fromBkt.Iter(ctx, chunksDir, func(objectName string) error {
   257  		err := rs.ensureObjectReplicated(ctx, objectName)
   258  		if err != nil {
   259  			return errors.Wrapf(err, "replicate object %v", objectName)
   260  		}
   261  
   262  		return nil
   263  	}); err != nil {
   264  		return err
   265  	}
   266  
   267  	if err := rs.ensureObjectReplicated(ctx, indexFile); err != nil {
   268  		return errors.Wrap(err, "replicate index file")
   269  	}
   270  
   271  	level.Debug(rs.logger).Log("msg", "replicating meta file", "object", metaFile)
   272  
   273  	if err := rs.toBkt.Upload(ctx, metaFile, bytes.NewBuffer(originMetaFileContent)); err != nil {
   274  		return errors.Wrap(err, "upload meta file")
   275  	}
   276  
   277  	rs.metrics.blocksReplicated.Inc()
   278  
   279  	return nil
   280  }
   281  
   282  // ensureBlockIsReplicated ensures that an object present in the origin bucket
   283  // is present in the target bucket.
   284  func (rs *replicationScheme) ensureObjectReplicated(ctx context.Context, objectName string) error {
   285  	level.Debug(rs.logger).Log("msg", "ensuring object is replicated", "object", objectName)
   286  
   287  	exists, err := rs.toBkt.Exists(ctx, objectName)
   288  	if err != nil {
   289  		return errors.Wrapf(err, "check if %v exists in target bucket", objectName)
   290  	}
   291  
   292  	// skip if already exists.
   293  	if exists {
   294  		level.Debug(rs.logger).Log("msg", "skipping object as already replicated", "object", objectName)
   295  		return nil
   296  	}
   297  
   298  	level.Debug(rs.logger).Log("msg", "object not present in target bucket, replicating", "object", objectName)
   299  
   300  	r, err := rs.fromBkt.Get(ctx, objectName)
   301  	if err != nil {
   302  		return errors.Wrapf(err, "get %v from origin bucket", objectName)
   303  	}
   304  
   305  	defer r.Close()
   306  
   307  	if err = rs.toBkt.Upload(ctx, objectName, r); err != nil {
   308  		return errors.Wrapf(err, "upload %v to target bucket", objectName)
   309  	}
   310  
   311  	level.Info(rs.logger).Log("msg", "object replicated", "object", objectName)
   312  	rs.metrics.objectsReplicated.Inc()
   313  
   314  	return nil
   315  }