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 }