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

     1  package ingester
     2  
     3  import (
     4  	"context"
     5  	"flag"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"sync"
    11  	"time"
    12  
    13  	"connectrpc.com/connect"
    14  	"github.com/go-kit/log"
    15  	"github.com/go-kit/log/level"
    16  	"github.com/google/uuid"
    17  	"github.com/grafana/dskit/multierror"
    18  	"github.com/grafana/dskit/ring"
    19  	"github.com/grafana/dskit/services"
    20  	"github.com/oklog/ulid/v2"
    21  	"github.com/pkg/errors"
    22  	"github.com/prometheus/client_golang/prometheus"
    23  
    24  	profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    25  	ingesterv1 "github.com/grafana/pyroscope/api/gen/proto/go/ingester/v1"
    26  	pushv1 "github.com/grafana/pyroscope/api/gen/proto/go/push/v1"
    27  	phlareobj "github.com/grafana/pyroscope/pkg/objstore"
    28  	phlareobjclient "github.com/grafana/pyroscope/pkg/objstore/client"
    29  	"github.com/grafana/pyroscope/pkg/phlaredb"
    30  	"github.com/grafana/pyroscope/pkg/pprof"
    31  	phlarecontext "github.com/grafana/pyroscope/pkg/pyroscope/context"
    32  	"github.com/grafana/pyroscope/pkg/tenant"
    33  	"github.com/grafana/pyroscope/pkg/usagestats"
    34  	"github.com/grafana/pyroscope/pkg/util"
    35  	"github.com/grafana/pyroscope/pkg/validation"
    36  )
    37  
    38  var activeTenantsStats = usagestats.NewInt("ingester_active_tenants")
    39  
    40  type Config struct {
    41  	LifecyclerConfig ring.LifecyclerConfig `yaml:"lifecycler,omitempty"`
    42  }
    43  
    44  // RegisterFlags registers the flags.
    45  func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
    46  	cfg.LifecyclerConfig.RegisterFlags(f, util.Logger)
    47  }
    48  
    49  func (cfg *Config) Validate() error {
    50  	return nil
    51  }
    52  
    53  type Ingester struct {
    54  	services.Service
    55  
    56  	cfg       Config
    57  	dbConfig  phlaredb.Config
    58  	logger    log.Logger
    59  	phlarectx context.Context
    60  
    61  	lifecycler         *ring.Lifecycler
    62  	subservices        *services.Manager
    63  	subservicesWatcher *services.FailureWatcher
    64  
    65  	localBucket   phlareobj.Bucket
    66  	storageBucket phlareobj.Bucket
    67  
    68  	instances    map[string]*instance
    69  	instancesMtx sync.RWMutex
    70  
    71  	limits              Limits
    72  	reg                 prometheus.Registerer
    73  	usageGroupEvaluator *validation.UsageGroupEvaluator
    74  }
    75  
    76  type ingesterFlusherCompat struct {
    77  	*Ingester
    78  }
    79  
    80  func (i *ingesterFlusherCompat) Flush() {
    81  	_, err := i.Ingester.Flush(context.TODO(), connect.NewRequest(&ingesterv1.FlushRequest{}))
    82  	if err != nil {
    83  		level.Error(i.Ingester.logger).Log("msg", "flush failed", "err", err)
    84  	}
    85  }
    86  
    87  func New(phlarectx context.Context, cfg Config, dbConfig phlaredb.Config, storageBucket phlareobj.Bucket, limits Limits, queryStoreAfter time.Duration) (*Ingester, error) {
    88  	i := &Ingester{
    89  		cfg:           cfg,
    90  		phlarectx:     phlarectx,
    91  		logger:        phlarecontext.Logger(phlarectx),
    92  		reg:           phlarecontext.Registry(phlarectx),
    93  		instances:     map[string]*instance{},
    94  		dbConfig:      dbConfig,
    95  		storageBucket: storageBucket,
    96  		limits:        limits,
    97  	}
    98  
    99  	// initialise the local bucket client
   100  	var (
   101  		localBucketCfg phlareobjclient.Config
   102  		err            error
   103  	)
   104  	localBucketCfg.Backend = phlareobjclient.Filesystem
   105  	localBucketCfg.Filesystem.Directory = dbConfig.DataPath
   106  	i.localBucket, err = phlareobjclient.NewBucket(phlarectx, localBucketCfg, "local")
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	i.usageGroupEvaluator = validation.NewUsageGroupEvaluator(i.logger)
   112  
   113  	i.lifecycler, err = ring.NewLifecycler(
   114  		cfg.LifecyclerConfig,
   115  		&ingesterFlusherCompat{i},
   116  		"ingester",
   117  		"ring",
   118  		true,
   119  		i.logger, prometheus.WrapRegistererWithPrefix("pyroscope_", i.reg))
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	retentionPolicy := defaultRetentionPolicy()
   125  
   126  	if dbConfig.EnforcementInterval > 0 {
   127  		retentionPolicy.EnforcementInterval = dbConfig.EnforcementInterval
   128  	}
   129  	if dbConfig.MinFreeDisk > 0 {
   130  		retentionPolicy.MinFreeDisk = dbConfig.MinFreeDisk
   131  	}
   132  	if dbConfig.MinDiskAvailablePercentage > 0 {
   133  		retentionPolicy.MinDiskAvailablePercentage = dbConfig.MinDiskAvailablePercentage
   134  	}
   135  	if queryStoreAfter > 0 {
   136  		retentionPolicy.Expiry = queryStoreAfter
   137  	}
   138  
   139  	if dbConfig.DisableEnforcement {
   140  		i.subservices, err = services.NewManager(i.lifecycler)
   141  	} else {
   142  		dc := newDiskCleaner(phlarecontext.Logger(phlarectx), i, retentionPolicy, dbConfig)
   143  		i.subservices, err = services.NewManager(i.lifecycler, dc)
   144  	}
   145  	if err != nil {
   146  		return nil, errors.Wrap(err, "services manager")
   147  	}
   148  	i.subservicesWatcher = services.NewFailureWatcher()
   149  	i.subservicesWatcher.WatchManager(i.subservices)
   150  	i.Service = services.NewBasicService(i.starting, i.running, i.stopping)
   151  	return i, nil
   152  }
   153  
   154  func (i *Ingester) starting(ctx context.Context) error {
   155  	return services.StartManagerAndAwaitHealthy(ctx, i.subservices)
   156  }
   157  
   158  func (i *Ingester) running(ctx context.Context) error {
   159  	select {
   160  	case <-ctx.Done():
   161  		return nil
   162  	case err := <-i.subservicesWatcher.Chan(): // handle lifecycler errors
   163  		return fmt.Errorf("lifecycler failed: %w", err)
   164  	}
   165  }
   166  
   167  func (i *Ingester) stopping(_ error) error {
   168  	errs := multierror.New()
   169  	errs.Add(services.StopManagerAndAwaitStopped(context.Background(), i.subservices))
   170  	// stop all instances
   171  	i.instancesMtx.RLock()
   172  	defer i.instancesMtx.RUnlock()
   173  	for _, inst := range i.instances {
   174  		errs.Add(inst.Stop())
   175  	}
   176  	return errs.Err()
   177  }
   178  
   179  func (i *Ingester) getOrCreateInstance(tenantID string) (*instance, error) {
   180  	inst, ok := i.getInstanceByID(tenantID)
   181  	if ok {
   182  		return inst, nil
   183  	}
   184  
   185  	i.instancesMtx.Lock()
   186  	defer i.instancesMtx.Unlock()
   187  	inst, ok = i.instances[tenantID]
   188  	if !ok {
   189  		var err error
   190  
   191  		inst, err = newInstance(i.phlarectx, i.dbConfig, tenantID, i.localBucket, i.storageBucket, NewLimiter(tenantID, i.limits, i.lifecycler, i.cfg.LifecyclerConfig.RingConfig.ReplicationFactor))
   192  		if err != nil {
   193  			return nil, err
   194  		}
   195  		i.instances[tenantID] = inst
   196  		activeTenantsStats.Set(int64(len(i.instances)))
   197  	}
   198  	return inst, nil
   199  }
   200  
   201  func (i *Ingester) hasLocalBlocks(tenantID string) (bool, error) {
   202  	entries, err := os.ReadDir(filepath.Join(i.dbConfig.DataPath, tenantID, "local"))
   203  	if err != nil {
   204  		if os.IsNotExist(err) {
   205  			return false, nil
   206  		}
   207  		return false, err
   208  	}
   209  	for _, entry := range entries {
   210  		if entry.IsDir() {
   211  			return true, nil
   212  		}
   213  	}
   214  	return false, nil
   215  }
   216  
   217  func (i *Ingester) getOrOpenInstance(tenantID string) (*instance, error) {
   218  	inst, ok := i.getInstanceByID(tenantID)
   219  	if ok {
   220  		return inst, nil
   221  	}
   222  
   223  	i.instancesMtx.Lock()
   224  	defer i.instancesMtx.Unlock()
   225  	inst, ok = i.instances[tenantID]
   226  	if ok {
   227  		return inst, nil
   228  	}
   229  
   230  	hasLocalBlocks, err := i.hasLocalBlocks(tenantID)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	if !hasLocalBlocks {
   235  		return nil, nil
   236  	}
   237  
   238  	limiter := NewLimiter(tenantID, i.limits, i.lifecycler, i.cfg.LifecyclerConfig.RingConfig.ReplicationFactor)
   239  	inst, err = newInstance(i.phlarectx, i.dbConfig, tenantID, i.localBucket, i.storageBucket, limiter)
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	i.instances[tenantID] = inst
   245  	activeTenantsStats.Set(int64(len(i.instances)))
   246  	return inst, nil
   247  }
   248  
   249  func (i *Ingester) getInstanceByID(id string) (*instance, bool) {
   250  	i.instancesMtx.RLock()
   251  	defer i.instancesMtx.RUnlock()
   252  
   253  	inst, ok := i.instances[id]
   254  	return inst, ok
   255  }
   256  
   257  func push[T any](ctx context.Context, i *Ingester, f func(*instance) (T, error)) (T, error) {
   258  	var res T
   259  	tenantID, err := tenant.ExtractTenantIDFromContext(ctx)
   260  	if err != nil {
   261  		return res, connect.NewError(connect.CodeInvalidArgument, err)
   262  	}
   263  	instance, err := i.getOrCreateInstance(tenantID)
   264  	if err != nil {
   265  		return res, connect.NewError(connect.CodeInternal, err)
   266  	}
   267  	return f(instance)
   268  }
   269  
   270  // forInstanceUnary executes the given function for the instance with the given tenant ID in the context.
   271  func forInstanceUnary[T any](ctx context.Context, i *Ingester, f func(*instance) (*connect.Response[T], error)) (*connect.Response[T], error) {
   272  	tenantID, err := tenant.ExtractTenantIDFromContext(ctx)
   273  	if err != nil {
   274  		return connect.NewResponse[T](new(T)), connect.NewError(connect.CodeInvalidArgument, err)
   275  	}
   276  	instance, err := i.getOrOpenInstance(tenantID)
   277  	if err != nil {
   278  		return nil, connect.NewError(connect.CodeInternal, err)
   279  	}
   280  	if instance != nil {
   281  		return f(instance)
   282  	}
   283  	return connect.NewResponse[T](new(T)), nil
   284  }
   285  
   286  func forInstanceStream[Req, Resp any](ctx context.Context, i *Ingester, stream *connect.BidiStream[Req, Resp], f func(*instance) error) error {
   287  	tenantID, err := tenant.ExtractTenantIDFromContext(ctx)
   288  	if err != nil {
   289  		return connect.NewError(connect.CodeInvalidArgument, err)
   290  	}
   291  	instance, err := i.getOrOpenInstance(tenantID)
   292  	if err != nil {
   293  		return connect.NewError(connect.CodeInternal, err)
   294  	}
   295  	if instance != nil {
   296  		return f(instance)
   297  	}
   298  	// The client blocks awaiting the response.
   299  	if _, err = stream.Receive(); err != nil {
   300  		if errors.Is(err, io.EOF) {
   301  			return connect.NewError(connect.CodeCanceled, errors.New("client closed stream"))
   302  		}
   303  		return err
   304  	}
   305  	if err = stream.Send(new(Resp)); err != nil {
   306  		return err
   307  	}
   308  	return stream.Send(new(Resp))
   309  }
   310  
   311  func (i *Ingester) evictBlock(tenantID string, b ulid.ULID, fn func() error) (err error) {
   312  	// We lock instances map for writes to ensure that no new instances are
   313  	// created during the procedure. Otherwise, during initialization, the
   314  	// new PhlareDB instance may try to load a block that has already been
   315  	// deleted, or is being deleted.
   316  	i.instancesMtx.RLock()
   317  	defer i.instancesMtx.RUnlock()
   318  	// The map only contains PhlareDB instances that has been initialized since
   319  	// the process start, therefore there is no guarantee that we will find the
   320  	// discovered candidate block there. If it is the case, we have to ensure that
   321  	// the block won't be accessed, before and during deleting it from the disk.
   322  	var evicted bool
   323  	if tenantInstance, ok := i.instances[tenantID]; ok {
   324  		if evicted, err = tenantInstance.Evict(b, fn); err != nil {
   325  			return fmt.Errorf("failed to evict block %s/%s: %w", tenantID, b, err)
   326  		}
   327  	}
   328  	// If the instance is not found, or the querier is not aware of the block,
   329  	// and thus the callback has not been invoked, do it now.
   330  	if !evicted {
   331  		return fn()
   332  	}
   333  	return nil
   334  }
   335  
   336  func (i *Ingester) Push(ctx context.Context, req *connect.Request[pushv1.PushRequest]) (*connect.Response[pushv1.PushResponse], error) {
   337  	return push(ctx, i, func(instance *instance) (*connect.Response[pushv1.PushResponse], error) {
   338  		usageGroups := i.limits.DistributorUsageGroups(instance.tenantID)
   339  
   340  		for _, series := range req.Msg.Series {
   341  			groups := i.usageGroupEvaluator.GetMatch(instance.tenantID, usageGroups, series.Labels)
   342  
   343  			for _, sample := range series.Samples {
   344  				err := pprof.FromBytes(sample.RawProfile, func(p *profilev1.Profile, size int) error {
   345  					id, err := uuid.Parse(sample.ID)
   346  					if err != nil {
   347  						return err
   348  					}
   349  					if err = instance.Ingest(ctx, p, id, series.Annotations, series.Labels...); err != nil {
   350  						reason := validation.ReasonOf(err)
   351  						if reason != validation.Unknown {
   352  							validation.DiscardedProfiles.WithLabelValues(string(reason), instance.tenantID).Add(float64(1))
   353  							validation.DiscardedBytes.WithLabelValues(string(reason), instance.tenantID).Add(float64(size))
   354  							groups.CountDiscardedBytes(string(reason), int64(size))
   355  
   356  							switch validation.ReasonOf(err) {
   357  							case validation.SeriesLimit:
   358  								return connect.NewError(connect.CodeResourceExhausted, err)
   359  							}
   360  						}
   361  					}
   362  					return err
   363  				})
   364  				if err != nil {
   365  					return nil, err
   366  				}
   367  			}
   368  		}
   369  		return connect.NewResponse(&pushv1.PushResponse{}), nil
   370  	})
   371  }
   372  
   373  func (i *Ingester) Flush(ctx context.Context, req *connect.Request[ingesterv1.FlushRequest]) (*connect.Response[ingesterv1.FlushResponse], error) {
   374  	i.instancesMtx.RLock()
   375  	defer i.instancesMtx.RUnlock()
   376  	for _, inst := range i.instances {
   377  		if err := inst.Flush(ctx, true, "api"); err != nil {
   378  			return nil, err
   379  		}
   380  	}
   381  
   382  	return connect.NewResponse(&ingesterv1.FlushResponse{}), nil
   383  }
   384  
   385  func (i *Ingester) TransferOut(ctx context.Context) error {
   386  	return ring.ErrTransferDisabled
   387  }
   388  
   389  // CheckReady is used to indicate to k8s when the ingesters are ready for
   390  // the addition removal of another ingester. Returns 204 when the ingester is
   391  // ready, 500 otherwise.
   392  func (i *Ingester) CheckReady(ctx context.Context) error {
   393  	if s := i.State(); s != services.Running && s != services.Stopping {
   394  		return fmt.Errorf("ingester not ready: %v", s)
   395  	}
   396  	return i.lifecycler.CheckReady(ctx)
   397  }