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

     1  package segmentwriter
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"time"
     9  
    10  	"github.com/go-kit/log"
    11  	"github.com/go-kit/log/level"
    12  	"github.com/google/uuid"
    13  	"github.com/grafana/dskit/grpcclient"
    14  	"github.com/grafana/dskit/multierror"
    15  	"github.com/grafana/dskit/ring"
    16  	"github.com/grafana/dskit/services"
    17  	"github.com/prometheus/client_golang/prometheus"
    18  	"google.golang.org/grpc/codes"
    19  	"google.golang.org/grpc/status"
    20  
    21  	segmentwriterv1 "github.com/grafana/pyroscope/api/gen/proto/go/segmentwriter/v1"
    22  	metastoreclient "github.com/grafana/pyroscope/pkg/metastore/client"
    23  	"github.com/grafana/pyroscope/pkg/model/relabel"
    24  	phlareobj "github.com/grafana/pyroscope/pkg/objstore"
    25  	"github.com/grafana/pyroscope/pkg/pprof"
    26  	"github.com/grafana/pyroscope/pkg/segmentwriter/memdb"
    27  	"github.com/grafana/pyroscope/pkg/tenant"
    28  	"github.com/grafana/pyroscope/pkg/util"
    29  	"github.com/grafana/pyroscope/pkg/util/health"
    30  	"github.com/grafana/pyroscope/pkg/validation"
    31  )
    32  
    33  const (
    34  	RingName = "segment-writer"
    35  	RingKey  = "segment-writer-ring"
    36  
    37  	minFlushConcurrency         = 8
    38  	defaultSegmentDuration      = 500 * time.Millisecond
    39  	defaultHedgedRequestMaxRate = 2  // 2 hedged requests per second
    40  	defaultHedgedRequestBurst   = 10 // allow bursts of 10 hedged requests
    41  )
    42  
    43  type Config struct {
    44  	GRPCClientConfig         grpcclient.Config     `yaml:"grpc_client_config" doc:"description=Configures the gRPC client used to communicate with the segment writer."`
    45  	LifecyclerConfig         ring.LifecyclerConfig `yaml:"lifecycler,omitempty"`
    46  	SegmentDuration          time.Duration         `yaml:"segment_duration,omitempty" category:"advanced"`
    47  	FlushConcurrency         uint                  `yaml:"flush_concurrency,omitempty" category:"advanced"`
    48  	UploadTimeout            time.Duration         `yaml:"upload-timeout,omitempty" category:"advanced"`
    49  	UploadMaxRetries         int                   `yaml:"upload-retry_max_retries,omitempty" category:"advanced"`
    50  	UploadMinBackoff         time.Duration         `yaml:"upload-retry_min_period,omitempty" category:"advanced"`
    51  	UploadMaxBackoff         time.Duration         `yaml:"upload-retry_max_period,omitempty" category:"advanced"`
    52  	UploadHedgeAfter         time.Duration         `yaml:"upload-hedge_upload_after,omitempty" category:"advanced"`
    53  	UploadHedgeRateMax       float64               `yaml:"upload-hedge_rate_max,omitempty" category:"advanced"`
    54  	UploadHedgeRateBurst     uint                  `yaml:"upload-hedge_rate_burst,omitempty" category:"advanced"`
    55  	MetadataDLQEnabled       bool                  `yaml:"metadata_dlq_enabled,omitempty" category:"advanced"`
    56  	MetadataUpdateTimeout    time.Duration         `yaml:"metadata_update_timeout,omitempty" category:"advanced"`
    57  	BucketHealthCheckEnabled bool                  `yaml:"bucket_health_check_enabled,omitempty" category:"advanced"`
    58  	BucketHealthCheckTimeout time.Duration         `yaml:"bucket_health_check_timeout,omitempty" category:"advanced"`
    59  }
    60  
    61  func (cfg *Config) Validate() error {
    62  	// TODO(kolesnikovae): implement.
    63  	if err := cfg.LifecyclerConfig.Validate(); err != nil {
    64  		return err
    65  	}
    66  	return cfg.GRPCClientConfig.Validate()
    67  }
    68  
    69  func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
    70  	const prefix = "segment-writer"
    71  	cfg.LifecyclerConfig.RegisterFlagsWithPrefix(prefix+".", f, util.Logger)
    72  	cfg.GRPCClientConfig.RegisterFlagsWithPrefix(prefix+".grpc-client-config", f)
    73  	f.DurationVar(&cfg.SegmentDuration, prefix+".segment-duration", defaultSegmentDuration, "Timeout when flushing segments to bucket.")
    74  	f.UintVar(&cfg.FlushConcurrency, prefix+".flush-concurrency", 0, "Number of concurrent flushes. Defaults to the number of CPUs, but not less than 8.")
    75  	f.DurationVar(&cfg.UploadTimeout, prefix+".upload-timeout", 2*time.Second, "Timeout for upload requests, including retries.")
    76  	f.IntVar(&cfg.UploadMaxRetries, prefix+".upload-max-retries", 3, "Number of times to backoff and retry before failing.")
    77  	f.DurationVar(&cfg.UploadMinBackoff, prefix+".upload-retry-min-period", 50*time.Millisecond, "Minimum delay when backing off.")
    78  	f.DurationVar(&cfg.UploadMaxBackoff, prefix+".upload-retry-max-period", defaultSegmentDuration, "Maximum delay when backing off.")
    79  	f.DurationVar(&cfg.UploadHedgeAfter, prefix+".upload-hedge-after", defaultSegmentDuration, "Time after which to hedge the upload request.")
    80  	f.Float64Var(&cfg.UploadHedgeRateMax, prefix+".upload-hedge-rate-max", defaultHedgedRequestMaxRate, "Maximum number of hedged requests per second. 0 disables rate limiting.")
    81  	f.UintVar(&cfg.UploadHedgeRateBurst, prefix+".upload-hedge-rate-burst", defaultHedgedRequestBurst, "Maximum number of hedged requests in a burst.")
    82  	f.BoolVar(&cfg.MetadataDLQEnabled, prefix+".metadata-dlq-enabled", true, "Enables dead letter queue (DLQ) for metadata. If the metadata update fails, it will be stored and updated asynchronously.")
    83  	f.DurationVar(&cfg.MetadataUpdateTimeout, prefix+".metadata-update-timeout", 2*time.Second, "Timeout for metadata update requests.")
    84  	f.BoolVar(&cfg.BucketHealthCheckEnabled, prefix+".bucket-health-check-enabled", true, "Enables bucket health check on startup. This both validates credentials and warms up the connection to reduce latency for the first write.")
    85  	f.DurationVar(&cfg.BucketHealthCheckTimeout, prefix+".bucket-health-check-timeout", 10*time.Second, "Timeout for bucket health check operations.")
    86  }
    87  
    88  type Limits interface {
    89  	IngestionRelabelingRules(tenantID string) []*relabel.Config
    90  	DistributorUsageGroups(tenantID string) *validation.UsageGroupConfig
    91  }
    92  
    93  type SegmentWriterService struct {
    94  	services.Service
    95  	segmentwriterv1.UnimplementedSegmentWriterServiceServer
    96  
    97  	config Config
    98  	logger log.Logger
    99  	reg    prometheus.Registerer
   100  	health health.Service
   101  
   102  	requests           util.InflightRequests
   103  	lifecycler         *ring.Lifecycler
   104  	subservices        *services.Manager
   105  	subservicesWatcher *services.FailureWatcher
   106  
   107  	storageBucket phlareobj.Bucket
   108  	segmentWriter *segmentsWriter
   109  }
   110  
   111  func New(
   112  	reg prometheus.Registerer,
   113  	logger log.Logger,
   114  	config Config,
   115  	limits Limits,
   116  	health health.Service,
   117  	storageBucket phlareobj.Bucket,
   118  	metastoreClient *metastoreclient.Client,
   119  ) (*SegmentWriterService, error) {
   120  	i := &SegmentWriterService{
   121  		config:        config,
   122  		logger:        logger,
   123  		reg:           reg,
   124  		health:        health,
   125  		storageBucket: storageBucket,
   126  	}
   127  
   128  	// The lifecycler is only used for discovery: it maintains the state of the
   129  	// instance in the ring and nothing more. Flush is managed explicitly at
   130  	// shutdown, and data/state transfer is not required.
   131  	var err error
   132  	i.lifecycler, err = ring.NewLifecycler(
   133  		config.LifecyclerConfig,
   134  		noOpTransferFlush{},
   135  		RingName,
   136  		RingKey,
   137  		false,
   138  		i.logger, prometheus.WrapRegistererWithPrefix("pyroscope_segment_writer_", i.reg))
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	i.subservices, err = services.NewManager(i.lifecycler)
   144  	if err != nil {
   145  		return nil, fmt.Errorf("services manager: %w", err)
   146  	}
   147  	if storageBucket == nil {
   148  		return nil, errors.New("storage bucket is required for segment writer")
   149  	}
   150  	if metastoreClient == nil {
   151  		return nil, errors.New("metastore client is required for segment writer")
   152  	}
   153  	metrics := newSegmentMetrics(i.reg)
   154  	headMetrics := memdb.NewHeadMetricsWithPrefix(reg, "pyroscope_segment_writer")
   155  	i.segmentWriter = newSegmentWriter(i.logger, metrics, headMetrics, config, limits, storageBucket, metastoreClient)
   156  	i.subservicesWatcher = services.NewFailureWatcher()
   157  	i.subservicesWatcher.WatchManager(i.subservices)
   158  	i.Service = services.NewBasicService(i.starting, i.running, i.stopping)
   159  	return i, nil
   160  }
   161  
   162  // performBucketHealthCheck performs a lightweight bucket operation to warm up the connection
   163  // and detect any object storage issues early. This serves the dual purpose of validating
   164  // bucket accessibility and reducing latency for the first actual write operation.
   165  func (i *SegmentWriterService) performBucketHealthCheck(ctx context.Context) error {
   166  	if !i.config.BucketHealthCheckEnabled {
   167  		return nil
   168  	}
   169  
   170  	level.Debug(i.logger).Log("msg", "starting bucket health check", "timeout", i.config.BucketHealthCheckTimeout.String())
   171  
   172  	healthCheckCtx, cancel := context.WithTimeout(ctx, i.config.BucketHealthCheckTimeout)
   173  	defer cancel()
   174  
   175  	err := i.storageBucket.Iter(healthCheckCtx, "", func(string) error {
   176  		// We only care about connectivity, not the actual contents
   177  		// Return an error to stop iteration after first item (if any)
   178  		return errors.New("stop iteration")
   179  	})
   180  
   181  	// Ignore the "stop iteration" error we intentionally return
   182  	// and any "object not found" type errors as they indicate the bucket is accessible
   183  	if err == nil || i.storageBucket.IsObjNotFoundErr(err) || err.Error() == "stop iteration" {
   184  		level.Debug(i.logger).Log("msg", "bucket health check succeeded")
   185  		return nil
   186  	}
   187  
   188  	level.Warn(i.logger).Log("msg", "bucket health check failed", "err", err)
   189  	return nil // Don't fail startup, just warn
   190  }
   191  
   192  func (i *SegmentWriterService) starting(ctx context.Context) error {
   193  	// Perform bucket health check before ring registration to warm up the connection
   194  	// and avoid slow first requests affecting p99 latency
   195  	// On error, will emit a warning but continue startup
   196  	_ = i.performBucketHealthCheck(ctx)
   197  
   198  	if err := services.StartManagerAndAwaitHealthy(ctx, i.subservices); err != nil {
   199  		return err
   200  	}
   201  	// The instance is ready to handle incoming requests.
   202  	// We do not have to wait for the lifecycler: its readiness check
   203  	// is only used to limit the number of instances that can be coming
   204  	// or going at any one time, by only returning true if all instances
   205  	// are active.
   206  	i.requests.Open()
   207  	i.health.SetServing()
   208  	return nil
   209  }
   210  
   211  func (i *SegmentWriterService) running(ctx context.Context) error {
   212  	select {
   213  	case <-ctx.Done():
   214  		return nil
   215  	case err := <-i.subservicesWatcher.Chan(): // handle lifecycler errors
   216  		return fmt.Errorf("lifecycler failed: %w", err)
   217  	}
   218  }
   219  
   220  func (i *SegmentWriterService) stopping(_ error) error {
   221  	i.health.SetNotServing()
   222  	errs := multierror.New()
   223  	errs.Add(services.StopManagerAndAwaitStopped(context.Background(), i.subservices))
   224  	time.Sleep(i.config.LifecyclerConfig.MinReadyDuration)
   225  	i.requests.Drain()
   226  	i.segmentWriter.stop()
   227  	return errs.Err()
   228  }
   229  
   230  func (i *SegmentWriterService) Push(ctx context.Context, req *segmentwriterv1.PushRequest) (*segmentwriterv1.PushResponse, error) {
   231  	if !i.requests.Add() {
   232  		return nil, status.Error(codes.Unavailable, "service is unavailable")
   233  	} else {
   234  		defer func() {
   235  			i.requests.Done()
   236  		}()
   237  	}
   238  
   239  	if req.TenantId == "" {
   240  		return nil, status.Error(codes.InvalidArgument, tenant.ErrNoTenantID.Error())
   241  	}
   242  	var id uuid.UUID
   243  	if err := id.UnmarshalBinary(req.ProfileId); err != nil {
   244  		return nil, status.Error(codes.InvalidArgument, err.Error())
   245  	}
   246  	p, err := pprof.RawFromBytes(req.Profile)
   247  	if err != nil {
   248  		return nil, status.Error(codes.InvalidArgument, err.Error())
   249  	}
   250  
   251  	wait := i.segmentWriter.ingest(shardKey(req.Shard), func(segment segmentIngest) {
   252  		segment.ingest(req.TenantId, p.Profile, id, req.Labels, req.Annotations)
   253  	})
   254  
   255  	flushStarted := time.Now()
   256  	defer func() {
   257  		i.segmentWriter.metrics.segmentFlushWaitDuration.
   258  			WithLabelValues(req.TenantId).
   259  			Observe(time.Since(flushStarted).Seconds())
   260  	}()
   261  	if err = wait.waitFlushed(ctx); err == nil {
   262  		return &segmentwriterv1.PushResponse{}, nil
   263  	}
   264  
   265  	switch {
   266  	case errors.Is(err, context.Canceled):
   267  		return nil, status.FromContextError(err).Err()
   268  
   269  	case errors.Is(err, context.DeadlineExceeded):
   270  		i.segmentWriter.metrics.segmentFlushTimeouts.WithLabelValues(req.TenantId).Inc()
   271  		level.Error(i.logger).Log("msg", "flush timeout", "err", err)
   272  		return nil, status.FromContextError(err).Err()
   273  
   274  	default:
   275  		level.Error(i.logger).Log("msg", "flush err", "err", err)
   276  		return nil, status.Error(codes.Unknown, err.Error())
   277  	}
   278  }
   279  
   280  // CheckReady is used to indicate when the ingesters are ready for
   281  // the addition removal of another ingester. Returns 204 when the ingester is
   282  // ready, 500 otherwise.
   283  func (i *SegmentWriterService) CheckReady(ctx context.Context) error {
   284  	if s := i.State(); s != services.Running && s != services.Stopping {
   285  		return fmt.Errorf("ingester not ready: %v", s)
   286  	}
   287  	return i.lifecycler.CheckReady(ctx)
   288  }
   289  
   290  type noOpTransferFlush struct{}
   291  
   292  func (noOpTransferFlush) Flush()                            {}
   293  func (noOpTransferFlush) TransferOut(context.Context) error { return ring.ErrTransferDisabled }