github.com/grafana/pyroscope@v1.18.0/pkg/storegateway/bucket.go (about)

     1  package storegateway
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"path"
     7  	"path/filepath"
     8  	"sort"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/go-kit/log"
    13  	"github.com/go-kit/log/level"
    14  	"github.com/oklog/ulid/v2"
    15  	"github.com/pkg/errors"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	"github.com/prometheus/common/model"
    18  
    19  	phlareobj "github.com/grafana/pyroscope/pkg/objstore"
    20  	"github.com/grafana/pyroscope/pkg/phlaredb"
    21  	"github.com/grafana/pyroscope/pkg/phlaredb/block"
    22  )
    23  
    24  // TODO move this to a config.
    25  const blockSyncConcurrency = 100
    26  
    27  type BucketStoreStats struct {
    28  	// BlocksLoaded is the number of blocks currently loaded in the bucket store.
    29  	BlocksLoaded int
    30  }
    31  
    32  type BucketStore struct {
    33  	bucket  phlareobj.Bucket
    34  	fetcher block.MetadataFetcher
    35  
    36  	tenantID, syncDir string
    37  
    38  	logger log.Logger
    39  
    40  	blocksMx sync.RWMutex
    41  	blocks   map[ulid.ULID]*Block
    42  	blockSet *bucketBlockSet
    43  
    44  	metrics *Metrics
    45  	stats   BucketStoreStats
    46  }
    47  
    48  func NewBucketStore(bucket phlareobj.Bucket, fetcher block.MetadataFetcher, tenantID string, syncDir string, logger log.Logger, reg prometheus.Registerer) (*BucketStore, error) {
    49  	s := &BucketStore{
    50  		fetcher:  fetcher,
    51  		bucket:   phlareobj.NewTenantBucketClient(tenantID, bucket, nil),
    52  		tenantID: tenantID,
    53  		syncDir:  syncDir,
    54  		logger:   log.With(logger, "tenant", tenantID),
    55  		blockSet: newBucketBlockSet(),
    56  		blocks:   map[ulid.ULID]*Block{},
    57  		metrics: NewBucketStoreMetrics(prometheus.WrapRegistererWith(
    58  			prometheus.Labels{"tenant": tenantID},
    59  			reg,
    60  		)),
    61  	}
    62  
    63  	if err := os.MkdirAll(syncDir, 0o750); err != nil {
    64  		return nil, errors.Wrap(err, "create dir")
    65  	}
    66  
    67  	return s, nil
    68  }
    69  
    70  func (b *BucketStore) InitialSync(ctx context.Context) error {
    71  	if err := b.SyncBlocks(ctx); err != nil {
    72  		return errors.Wrap(err, "sync block")
    73  	}
    74  
    75  	fis, err := os.ReadDir(b.syncDir)
    76  	if err != nil {
    77  		return errors.Wrap(err, "read dir")
    78  	}
    79  	names := make([]string, 0, len(fis))
    80  	for _, fi := range fis {
    81  		names = append(names, fi.Name())
    82  	}
    83  	for _, n := range names {
    84  		id, ok := block.IsBlockDir(n)
    85  		if !ok {
    86  			continue
    87  		}
    88  		if b := b.getBlock(id); b != nil {
    89  			continue
    90  		}
    91  
    92  		// No such block loaded, remove the local dir.
    93  		if err := os.RemoveAll(path.Join(b.syncDir, id.String())); err != nil {
    94  			level.Warn(b.logger).Log("msg", "failed to remove block which is not needed", "err", err)
    95  		}
    96  	}
    97  	return nil
    98  }
    99  
   100  func (s *BucketStore) getBlock(id ulid.ULID) *Block {
   101  	s.blocksMx.RLock()
   102  	defer s.blocksMx.RUnlock()
   103  	return s.blocks[id]
   104  }
   105  
   106  func (s *BucketStore) SyncBlocks(ctx context.Context) error {
   107  	// TODO sounds like we should get the meta this is just a list of ids
   108  	metas, _, metaFetchErr := s.fetcher.Fetch(ctx)
   109  	// For partial view allow adding new blocks at least.
   110  	if metaFetchErr != nil && metas == nil {
   111  		return metaFetchErr
   112  	}
   113  
   114  	var wg sync.WaitGroup
   115  	blockc := make(chan *block.Meta)
   116  
   117  	for i := 0; i < blockSyncConcurrency; i++ {
   118  		wg.Add(1)
   119  		go func() {
   120  			for meta := range blockc {
   121  				if err := s.addBlock(ctx, meta); err != nil {
   122  					continue
   123  				}
   124  			}
   125  			wg.Done()
   126  		}()
   127  	}
   128  
   129  	for id, meta := range metas {
   130  		if b := s.getBlock(id); b != nil {
   131  			continue
   132  		}
   133  		select {
   134  		case <-ctx.Done():
   135  		case blockc <- meta:
   136  		}
   137  	}
   138  
   139  	close(blockc)
   140  	wg.Wait()
   141  
   142  	if metaFetchErr != nil {
   143  		return metaFetchErr
   144  	}
   145  
   146  	// Drop all blocks that are no longer present in the bucket.
   147  	for id := range s.blocks {
   148  		if _, ok := metas[id]; ok {
   149  			continue
   150  		}
   151  		if err := s.removeBlock(id); err != nil {
   152  			level.Warn(s.logger).Log("msg", "drop of outdated block failed", "block", id, "err", err)
   153  		}
   154  		level.Info(s.logger).Log("msg", "dropped outdated block", "block", id)
   155  	}
   156  	s.stats.BlocksLoaded = len(s.blocks)
   157  
   158  	return nil
   159  }
   160  
   161  func (bs *BucketStore) addBlock(ctx context.Context, meta *block.Meta) (err error) {
   162  	level.Debug(bs.logger).Log("msg", "loading new block", "id", meta.ULID)
   163  
   164  	dir := bs.localPath(meta.ULID.String())
   165  	start := time.Now()
   166  	defer func() {
   167  		if err != nil {
   168  			bs.metrics.blockLoadFailures.Inc()
   169  			if err2 := os.RemoveAll(dir); err2 != nil {
   170  				level.Warn(bs.logger).Log("msg", "failed to remove block we cannot load", "err", err2)
   171  			}
   172  			level.Warn(bs.logger).Log("msg", "loading block failed", "elapsed", time.Since(start), "id", meta.ULID, "err", err)
   173  		} else {
   174  			level.Info(bs.logger).Log("msg", "loaded new block", "elapsed", time.Since(start), "id", meta.ULID)
   175  		}
   176  	}()
   177  
   178  	bs.metrics.blockLoads.Inc()
   179  
   180  	b, err := func() (*Block, error) {
   181  		bs.blocksMx.Lock()
   182  		defer bs.blocksMx.Unlock()
   183  		ctx = phlaredb.ContextWithBlockMetrics(ctx, bs.metrics.blockMetrics)
   184  		b, err := bs.createBlock(ctx, meta)
   185  		if err != nil {
   186  			return nil, errors.Wrap(err, "load block from disk")
   187  		}
   188  		bs.blockSet.add(b)
   189  		bs.blocks[meta.ULID] = b
   190  		return b, nil
   191  	}()
   192  	if err != nil {
   193  		return err
   194  	}
   195  	// Load the block into memory if it's within the last 24 hours.
   196  	// Todo make this configurable
   197  	if phlaredb.InRange(b, model.Now().Add(-24*time.Hour), model.Now()) {
   198  		level.Debug(bs.logger).Log("msg", "opening block",
   199  			"id", meta.ULID.String(),
   200  			"min", b.meta.MinTime.Time().Format(time.RFC3339),
   201  			"max", b.meta.MaxTime.Time().Format(time.RFC3339),
   202  		)
   203  
   204  		start := time.Now()
   205  		defer func() {
   206  			level.Info(bs.logger).Log("msg", "block opened", "duration", time.Since(start), "id", meta.ULID.String())
   207  		}()
   208  		if err := b.Open(ctx); err != nil {
   209  			level.Error(bs.logger).Log("msg", "open block", "err", err)
   210  		}
   211  	}
   212  	return nil
   213  }
   214  
   215  func (b *BucketStore) Stats() BucketStoreStats {
   216  	return b.stats
   217  }
   218  
   219  func (s *BucketStore) removeBlock(id ulid.ULID) (returnErr error) {
   220  	defer func() {
   221  		if returnErr != nil {
   222  			s.metrics.blockDropFailures.Inc()
   223  		}
   224  	}()
   225  
   226  	s.blocksMx.Lock()
   227  	b, ok := s.blocks[id]
   228  	if ok {
   229  		s.blockSet.remove(id)
   230  		delete(s.blocks, id)
   231  	}
   232  	s.blocksMx.Unlock()
   233  
   234  	if !ok {
   235  		return nil
   236  	}
   237  
   238  	// // The block has already been removed from BucketStore, so we track it as removed
   239  	// // even if releasing its resources could fail below.
   240  	s.metrics.blockDrops.Inc()
   241  
   242  	if err := b.Close(); err != nil {
   243  		return errors.Wrap(err, "close block")
   244  	}
   245  	if err := os.RemoveAll(s.localPath(id.String())); err != nil {
   246  		return errors.Wrap(err, "delete block")
   247  	}
   248  	return nil
   249  }
   250  
   251  func (s *BucketStore) localPath(id string) string {
   252  	return filepath.Join(s.syncDir, id)
   253  }
   254  
   255  // RemoveBlocksAndClose remove all blocks from local disk and releases all resources associated with the BucketStore.
   256  func (s *BucketStore) RemoveBlocksAndClose() error {
   257  	if err := os.RemoveAll(s.syncDir); err != nil {
   258  		return errors.Wrap(err, "delete block")
   259  	}
   260  	return nil
   261  }
   262  
   263  // bucketBlockSet holds all blocks.
   264  type bucketBlockSet struct {
   265  	mtx    sync.RWMutex
   266  	blocks []*Block // Blocks sorted by mint, then maxt.
   267  }
   268  
   269  // newBucketBlockSet initializes a new set with the known downsampling windows hard-configured.
   270  // (Mimir only supports no-downsampling)
   271  // The set currently does not support arbitrary ranges.
   272  func newBucketBlockSet() *bucketBlockSet {
   273  	return &bucketBlockSet{}
   274  }
   275  
   276  func (s *bucketBlockSet) add(b *Block) {
   277  	s.mtx.Lock()
   278  	defer s.mtx.Unlock()
   279  
   280  	s.blocks = append(s.blocks, b)
   281  
   282  	// Always sort blocks by min time, then max time.
   283  	sort.Slice(s.blocks, func(j, k int) bool {
   284  		if s.blocks[j].meta.MinTime == s.blocks[k].meta.MinTime {
   285  			return s.blocks[j].meta.MaxTime < s.blocks[k].meta.MaxTime
   286  		}
   287  		return s.blocks[j].meta.MinTime < s.blocks[k].meta.MinTime
   288  	})
   289  }
   290  
   291  func (s *bucketBlockSet) remove(id ulid.ULID) {
   292  	s.mtx.Lock()
   293  	defer s.mtx.Unlock()
   294  
   295  	for i, b := range s.blocks {
   296  		if b.meta.ULID != id {
   297  			continue
   298  		}
   299  		s.blocks = append(s.blocks[:i], s.blocks[i+1:]...)
   300  		return
   301  	}
   302  }
   303  
   304  // getFor returns a time-ordered list of blocks that cover date between mint and maxt.
   305  // It supports overlapping blocks.
   306  //
   307  // NOTE: s.blocks are expected to be sorted in minTime order.
   308  func (s *bucketBlockSet) getFor(mint, maxt model.Time) (bs []*Block) {
   309  	if mint > maxt {
   310  		return nil
   311  	}
   312  
   313  	s.mtx.RLock()
   314  	defer s.mtx.RUnlock()
   315  
   316  	// Fill the given interval with the blocks within the request mint and maxt.
   317  	for _, b := range s.blocks {
   318  		if b.meta.MaxTime <= mint {
   319  			continue
   320  		}
   321  		// NOTE: Block intervals are half-open: [b.MinTime, b.MaxTime).
   322  		if b.meta.MinTime > maxt {
   323  			break
   324  		}
   325  
   326  		bs = append(bs, b)
   327  	}
   328  
   329  	return bs
   330  }