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 }