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

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  // Package block contains common functionality for interacting with TSDB blocks
     5  // in the context of Thanos.
     6  package block
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"encoding/json"
    12  	"io"
    13  	"os"
    14  	"path"
    15  	"path/filepath"
    16  	"sort"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/go-kit/log"
    21  	"github.com/go-kit/log/level"
    22  	"github.com/oklog/ulid"
    23  	"github.com/pkg/errors"
    24  	"github.com/prometheus/client_golang/prometheus"
    25  	"github.com/thanos-io/objstore"
    26  
    27  	"github.com/thanos-io/thanos/pkg/block/metadata"
    28  	"github.com/thanos-io/thanos/pkg/runutil"
    29  )
    30  
    31  const (
    32  	// MetaFilename is the known JSON filename for meta information.
    33  	MetaFilename = "meta.json"
    34  	// IndexFilename is the known index file for block index.
    35  	IndexFilename = "index"
    36  	// IndexHeaderFilename is the canonical name for binary index header file that stores essential information.
    37  	IndexHeaderFilename = "index-header"
    38  	// ChunksDirname is the known dir name for chunks with compressed samples.
    39  	ChunksDirname = "chunks"
    40  
    41  	// DebugMetas is a directory for debug meta files that happen in the past. Useful for debugging.
    42  	DebugMetas = "debug/metas"
    43  )
    44  
    45  // Download downloads directory that is mean to be block directory. If any of the files
    46  // have a hash calculated in the meta file and it matches with what is in the destination path then
    47  // we do not download it. We always re-download the meta file.
    48  func Download(ctx context.Context, logger log.Logger, bucket objstore.Bucket, id ulid.ULID, dst string, options ...objstore.DownloadOption) error {
    49  	if err := os.MkdirAll(dst, 0750); err != nil {
    50  		return errors.Wrap(err, "create dir")
    51  	}
    52  
    53  	if err := objstore.DownloadFile(ctx, logger, bucket, path.Join(id.String(), MetaFilename), path.Join(dst, MetaFilename)); err != nil {
    54  		return err
    55  	}
    56  	m, err := metadata.ReadFromDir(dst)
    57  	if err != nil {
    58  		return errors.Wrapf(err, "reading meta from %s", dst)
    59  	}
    60  
    61  	ignoredPaths := []string{MetaFilename}
    62  	for _, fl := range m.Thanos.Files {
    63  		if fl.Hash == nil || fl.Hash.Func == metadata.NoneFunc || fl.RelPath == "" {
    64  			continue
    65  		}
    66  		actualHash, err := metadata.CalculateHash(filepath.Join(dst, fl.RelPath), fl.Hash.Func, logger)
    67  		if err != nil {
    68  			level.Info(logger).Log("msg", "failed to calculate hash when downloading; re-downloading", "relPath", fl.RelPath, "err", err)
    69  			continue
    70  		}
    71  
    72  		if fl.Hash.Equal(&actualHash) {
    73  			ignoredPaths = append(ignoredPaths, fl.RelPath)
    74  		}
    75  	}
    76  
    77  	if err := objstore.DownloadDir(ctx, logger, bucket, id.String(), id.String(), dst, append(options, objstore.WithDownloadIgnoredPaths(ignoredPaths...))...); err != nil {
    78  		return err
    79  	}
    80  
    81  	chunksDir := filepath.Join(dst, ChunksDirname)
    82  	_, err = os.Stat(chunksDir)
    83  	if os.IsNotExist(err) {
    84  		// This can happen if block is empty. We cannot easily upload empty directory, so create one here.
    85  		return os.Mkdir(chunksDir, os.ModePerm)
    86  	}
    87  
    88  	if err != nil {
    89  		return errors.Wrapf(err, "stat %s", chunksDir)
    90  	}
    91  
    92  	return nil
    93  }
    94  
    95  // Upload uploads a TSDB block to the object storage. It verifies basic
    96  // features of Thanos block.
    97  func Upload(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc, options ...objstore.UploadOption) error {
    98  	return upload(ctx, logger, bkt, bdir, hf, true, options...)
    99  }
   100  
   101  // UploadPromBlock uploads a TSDB block to the object storage. It assumes
   102  // the block is used in Prometheus so it doesn't check Thanos external labels.
   103  func UploadPromBlock(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc, options ...objstore.UploadOption) error {
   104  	return upload(ctx, logger, bkt, bdir, hf, false, options...)
   105  }
   106  
   107  // upload uploads block from given block dir that ends with block id.
   108  // It makes sure cleanup is done on error to avoid partial block uploads.
   109  // TODO(bplotka): Ensure bucket operations have reasonable backoff retries.
   110  // NOTE: Upload updates `meta.Thanos.File` section.
   111  func upload(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc, checkExternalLabels bool, options ...objstore.UploadOption) error {
   112  	df, err := os.Stat(bdir)
   113  	if err != nil {
   114  		return err
   115  	}
   116  	if !df.IsDir() {
   117  		return errors.Errorf("%s is not a directory", bdir)
   118  	}
   119  
   120  	// Verify dir.
   121  	id, err := ulid.Parse(df.Name())
   122  	if err != nil {
   123  		return errors.Wrap(err, "not a block dir")
   124  	}
   125  
   126  	meta, err := metadata.ReadFromDir(bdir)
   127  	if err != nil {
   128  		// No meta or broken meta file.
   129  		return errors.Wrap(err, "read meta")
   130  	}
   131  
   132  	if checkExternalLabels {
   133  		if meta.Thanos.Labels == nil || len(meta.Thanos.Labels) == 0 {
   134  			return errors.New("empty external labels are not allowed for Thanos block.")
   135  		}
   136  	}
   137  
   138  	metaEncoded := strings.Builder{}
   139  	meta.Thanos.Files, err = GatherFileStats(bdir, hf, logger)
   140  	if err != nil {
   141  		return errors.Wrap(err, "gather meta file stats")
   142  	}
   143  
   144  	if err := meta.Write(&metaEncoded); err != nil {
   145  		return errors.Wrap(err, "encode meta file")
   146  	}
   147  
   148  	if err := objstore.UploadDir(ctx, logger, bkt, filepath.Join(bdir, ChunksDirname), path.Join(id.String(), ChunksDirname), options...); err != nil {
   149  		return cleanUp(logger, bkt, id, errors.Wrap(err, "upload chunks"))
   150  	}
   151  
   152  	if err := objstore.UploadFile(ctx, logger, bkt, filepath.Join(bdir, IndexFilename), path.Join(id.String(), IndexFilename)); err != nil {
   153  		return cleanUp(logger, bkt, id, errors.Wrap(err, "upload index"))
   154  	}
   155  
   156  	// Meta.json always need to be uploaded as a last item. This will allow to assume block directories without meta file to be pending uploads.
   157  	if err := bkt.Upload(ctx, path.Join(id.String(), MetaFilename), strings.NewReader(metaEncoded.String())); err != nil {
   158  		// Don't call cleanUp here. Despite getting error, meta.json may have been uploaded in certain cases,
   159  		// and even though cleanUp will not see it yet, meta.json may appear in the bucket later.
   160  		// (Eg. S3 is known to behave this way when it returns 503 "SlowDown" error).
   161  		// If meta.json is not uploaded, this will produce partial blocks, but such blocks will be cleaned later.
   162  		return errors.Wrap(err, "upload meta file")
   163  	}
   164  
   165  	return nil
   166  }
   167  
   168  func cleanUp(logger log.Logger, bkt objstore.Bucket, id ulid.ULID, err error) error {
   169  	// Cleanup the dir with an uncancelable context.
   170  	cleanErr := Delete(context.Background(), logger, bkt, id)
   171  	if cleanErr != nil {
   172  		return errors.Wrapf(err, "failed to clean block after upload issue. Partial block in system. Err: %s", err.Error())
   173  	}
   174  	return err
   175  }
   176  
   177  // MarkForDeletion creates a file which stores information about when the block was marked for deletion.
   178  func MarkForDeletion(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID, details string, markedForDeletion prometheus.Counter) error {
   179  	deletionMarkFile := path.Join(id.String(), metadata.DeletionMarkFilename)
   180  	deletionMarkExists, err := bkt.Exists(ctx, deletionMarkFile)
   181  	if err != nil {
   182  		return errors.Wrapf(err, "check exists %s in bucket", deletionMarkFile)
   183  	}
   184  	if deletionMarkExists {
   185  		level.Warn(logger).Log("msg", "requested to mark for deletion, but file already exists; this should not happen; investigate", "err", errors.Errorf("file %s already exists in bucket", deletionMarkFile))
   186  		return nil
   187  	}
   188  
   189  	deletionMark, err := json.Marshal(metadata.DeletionMark{
   190  		ID:           id,
   191  		DeletionTime: time.Now().Unix(),
   192  		Version:      metadata.DeletionMarkVersion1,
   193  		Details:      details,
   194  	})
   195  	if err != nil {
   196  		return errors.Wrap(err, "json encode deletion mark")
   197  	}
   198  
   199  	if err := bkt.Upload(ctx, deletionMarkFile, bytes.NewBuffer(deletionMark)); err != nil {
   200  		return errors.Wrapf(err, "upload file %s to bucket", deletionMarkFile)
   201  	}
   202  	markedForDeletion.Inc()
   203  	level.Info(logger).Log("msg", "block has been marked for deletion", "block", id)
   204  	return nil
   205  }
   206  
   207  // Delete removes directory that is meant to be block directory.
   208  // NOTE: Always prefer this method for deleting blocks.
   209  //   - We have to delete block's files in the certain order (meta.json first and deletion-mark.json last)
   210  //     to ensure we don't end up with malformed partial blocks. Thanos system handles well partial blocks
   211  //     only if they don't have meta.json. If meta.json is present Thanos assumes valid block.
   212  //   - This avoids deleting empty dir (whole bucket) by mistake.
   213  func Delete(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID) error {
   214  	metaFile := path.Join(id.String(), MetaFilename)
   215  	deletionMarkFile := path.Join(id.String(), metadata.DeletionMarkFilename)
   216  
   217  	// Delete block meta file.
   218  	ok, err := bkt.Exists(ctx, metaFile)
   219  	if err != nil {
   220  		return errors.Wrapf(err, "stat %s", metaFile)
   221  	}
   222  
   223  	if ok {
   224  		if err := bkt.Delete(ctx, metaFile); err != nil {
   225  			return errors.Wrapf(err, "delete %s", metaFile)
   226  		}
   227  		level.Debug(logger).Log("msg", "deleted file", "file", metaFile, "bucket", bkt.Name())
   228  	}
   229  
   230  	// Delete the block objects, but skip:
   231  	// - The metaFile as we just deleted. This is required for eventual object storages (list after write).
   232  	// - The deletionMarkFile as we'll delete it at last.
   233  	err = deleteDirRec(ctx, logger, bkt, id.String(), func(name string) bool {
   234  		return name == metaFile || name == deletionMarkFile
   235  	})
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	// Delete block deletion mark.
   241  	ok, err = bkt.Exists(ctx, deletionMarkFile)
   242  	if err != nil {
   243  		return errors.Wrapf(err, "stat %s", deletionMarkFile)
   244  	}
   245  
   246  	if ok {
   247  		if err := bkt.Delete(ctx, deletionMarkFile); err != nil {
   248  			return errors.Wrapf(err, "delete %s", deletionMarkFile)
   249  		}
   250  		level.Debug(logger).Log("msg", "deleted file", "file", deletionMarkFile, "bucket", bkt.Name())
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  // deleteDirRec removes all objects prefixed with dir from the bucket. It skips objects that return true for the passed keep function.
   257  // NOTE: For objects removal use `block.Delete` strictly.
   258  func deleteDirRec(ctx context.Context, logger log.Logger, bkt objstore.Bucket, dir string, keep func(name string) bool) error {
   259  	return bkt.Iter(ctx, dir, func(name string) error {
   260  		// If we hit a directory, call DeleteDir recursively.
   261  		if strings.HasSuffix(name, objstore.DirDelim) {
   262  			return deleteDirRec(ctx, logger, bkt, name, keep)
   263  		}
   264  		if keep(name) {
   265  			return nil
   266  		}
   267  		if err := bkt.Delete(ctx, name); err != nil {
   268  			return err
   269  		}
   270  		level.Debug(logger).Log("msg", "deleted file", "file", name, "bucket", bkt.Name())
   271  		return nil
   272  	})
   273  }
   274  
   275  // DownloadMeta downloads only meta file from bucket by block ID.
   276  // TODO(bwplotka): Differentiate between network error & partial upload.
   277  func DownloadMeta(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID) (metadata.Meta, error) {
   278  	rc, err := bkt.Get(ctx, path.Join(id.String(), MetaFilename))
   279  	if err != nil {
   280  		return metadata.Meta{}, errors.Wrapf(err, "meta.json bkt get for %s", id.String())
   281  	}
   282  	defer runutil.CloseWithLogOnErr(logger, rc, "download meta bucket client")
   283  
   284  	var m metadata.Meta
   285  
   286  	obj, err := io.ReadAll(rc)
   287  	if err != nil {
   288  		return metadata.Meta{}, errors.Wrapf(err, "read meta.json for block %s", id.String())
   289  	}
   290  
   291  	if err = json.Unmarshal(obj, &m); err != nil {
   292  		return metadata.Meta{}, errors.Wrapf(err, "unmarshal meta.json for block %s", id.String())
   293  	}
   294  
   295  	return m, nil
   296  }
   297  
   298  func IsBlockMetaFile(path string) bool {
   299  	return filepath.Base(path) == MetaFilename
   300  }
   301  
   302  func IsBlockDir(path string) (id ulid.ULID, ok bool) {
   303  	id, err := ulid.Parse(filepath.Base(path))
   304  	return id, err == nil
   305  }
   306  
   307  // GetSegmentFiles returns list of segment files for given block. Paths are relative to the chunks directory.
   308  // In case of errors, nil is returned.
   309  func GetSegmentFiles(blockDir string) []string {
   310  	files, err := os.ReadDir(filepath.Join(blockDir, ChunksDirname))
   311  	if err != nil {
   312  		return nil
   313  	}
   314  
   315  	// ReadDir returns files in sorted order already.
   316  	var result []string
   317  	for _, f := range files {
   318  		result = append(result, f.Name())
   319  	}
   320  	return result
   321  }
   322  
   323  // GatherFileStats returns metadata.File entry for files inside TSDB block (index, chunks, meta.json).
   324  func GatherFileStats(blockDir string, hf metadata.HashFunc, logger log.Logger) (res []metadata.File, _ error) {
   325  	files, err := os.ReadDir(filepath.Join(blockDir, ChunksDirname))
   326  	if err != nil {
   327  		return nil, errors.Wrapf(err, "read dir %v", filepath.Join(blockDir, ChunksDirname))
   328  	}
   329  	for _, f := range files {
   330  		fi, err := f.Info()
   331  		if err != nil {
   332  			return nil, errors.Wrapf(err, "getting file info %v", filepath.Join(ChunksDirname, f.Name()))
   333  		}
   334  
   335  		mf := metadata.File{
   336  			RelPath:   filepath.Join(ChunksDirname, f.Name()),
   337  			SizeBytes: fi.Size(),
   338  		}
   339  		if hf != metadata.NoneFunc && !f.IsDir() {
   340  			h, err := metadata.CalculateHash(filepath.Join(blockDir, ChunksDirname, f.Name()), hf, logger)
   341  			if err != nil {
   342  				return nil, errors.Wrapf(err, "calculate hash %v", filepath.Join(ChunksDirname, f.Name()))
   343  			}
   344  			mf.Hash = &h
   345  		}
   346  		res = append(res, mf)
   347  	}
   348  
   349  	indexFile, err := os.Stat(filepath.Join(blockDir, IndexFilename))
   350  	if err != nil {
   351  		return nil, errors.Wrapf(err, "stat %v", filepath.Join(blockDir, IndexFilename))
   352  	}
   353  	mf := metadata.File{
   354  		RelPath:   indexFile.Name(),
   355  		SizeBytes: indexFile.Size(),
   356  	}
   357  	if hf != metadata.NoneFunc {
   358  		h, err := metadata.CalculateHash(filepath.Join(blockDir, IndexFilename), hf, logger)
   359  		if err != nil {
   360  			return nil, errors.Wrapf(err, "calculate hash %v", indexFile.Name())
   361  		}
   362  		mf.Hash = &h
   363  	}
   364  	res = append(res, mf)
   365  
   366  	metaFile, err := os.Stat(filepath.Join(blockDir, MetaFilename))
   367  	if err != nil {
   368  		return nil, errors.Wrapf(err, "stat %v", filepath.Join(blockDir, MetaFilename))
   369  	}
   370  	res = append(res, metadata.File{RelPath: metaFile.Name()})
   371  
   372  	sort.Slice(res, func(i, j int) bool {
   373  		return strings.Compare(res[i].RelPath, res[j].RelPath) < 0
   374  	})
   375  	return res, err
   376  }
   377  
   378  // MarkForNoCompact creates a file which marks block to be not compacted.
   379  func MarkForNoCompact(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID, reason metadata.NoCompactReason, details string, markedForNoCompact prometheus.Counter) error {
   380  	m := path.Join(id.String(), metadata.NoCompactMarkFilename)
   381  	noCompactMarkExists, err := bkt.Exists(ctx, m)
   382  	if err != nil {
   383  		return errors.Wrapf(err, "check exists %s in bucket", m)
   384  	}
   385  	if noCompactMarkExists {
   386  		level.Warn(logger).Log("msg", "requested to mark for no compaction, but file already exists; this should not happen; investigate", "err", errors.Errorf("file %s already exists in bucket", m))
   387  		return nil
   388  	}
   389  
   390  	noCompactMark, err := json.Marshal(metadata.NoCompactMark{
   391  		ID:      id,
   392  		Version: metadata.NoCompactMarkVersion1,
   393  
   394  		NoCompactTime: time.Now().Unix(),
   395  		Reason:        reason,
   396  		Details:       details,
   397  	})
   398  	if err != nil {
   399  		return errors.Wrap(err, "json encode no compact mark")
   400  	}
   401  
   402  	if err := bkt.Upload(ctx, m, bytes.NewBuffer(noCompactMark)); err != nil {
   403  		return errors.Wrapf(err, "upload file %s to bucket", m)
   404  	}
   405  	markedForNoCompact.Inc()
   406  	level.Info(logger).Log("msg", "block has been marked for no compaction", "block", id)
   407  	return nil
   408  }
   409  
   410  // MarkForNoDownsample creates a file which marks block to be not downsampled.
   411  func MarkForNoDownsample(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID, reason metadata.NoDownsampleReason, details string, markedForNoDownsample prometheus.Counter) error {
   412  	m := path.Join(id.String(), metadata.NoDownsampleMarkFilename)
   413  	noDownsampleMarkExists, err := bkt.Exists(ctx, m)
   414  	if err != nil {
   415  		return errors.Wrapf(err, "check exists %s in bucket", m)
   416  	}
   417  	if noDownsampleMarkExists {
   418  		level.Warn(logger).Log("msg", "requested to mark for no deletion, but file already exists; this should not happen; investigate", "err", errors.Errorf("file %s already exists in bucket", m))
   419  		return nil
   420  	}
   421  	noDownsampleMark, err := json.Marshal(metadata.NoDownsampleMark{
   422  		ID:      id,
   423  		Version: metadata.NoDownsampleMarkVersion1,
   424  
   425  		NoDownsampleTime: time.Now().Unix(),
   426  		Reason:           reason,
   427  		Details:          details,
   428  	})
   429  	if err != nil {
   430  		return errors.Wrap(err, "json encode no downsample mark")
   431  	}
   432  
   433  	if err := bkt.Upload(ctx, m, bytes.NewBuffer(noDownsampleMark)); err != nil {
   434  		return errors.Wrapf(err, "upload file %s to bucket", m)
   435  	}
   436  	markedForNoDownsample.Inc()
   437  	level.Info(logger).Log("msg", "block has been marked for no downsample", "block", id)
   438  	return nil
   439  }
   440  
   441  // RemoveMark removes the file which marked the block for deletion, no-downsample or no-compact.
   442  func RemoveMark(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID, removeMark prometheus.Counter, markedFilename string) error {
   443  	markedFile := path.Join(id.String(), markedFilename)
   444  	markedFileExists, err := bkt.Exists(ctx, markedFile)
   445  	if err != nil {
   446  		return errors.Wrapf(err, "check if %s file exists in bucket", markedFile)
   447  	}
   448  	if !markedFileExists {
   449  		level.Warn(logger).Log("msg", "requested to remove the mark, but file does not exist", "err", errors.Errorf("file %s does not exist in bucket", markedFile))
   450  		return nil
   451  	}
   452  	if err := bkt.Delete(ctx, markedFile); err != nil {
   453  		return errors.Wrapf(err, "delete file %s from bucket", markedFile)
   454  	}
   455  	removeMark.Inc()
   456  	level.Info(logger).Log("msg", "mark has been removed from the block", "block", id)
   457  	return nil
   458  }