mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
495 lines
15 KiB
Go
495 lines
15 KiB
Go
package provider
|
|
|
|
import (
|
|
"container/heap"
|
|
"context"
|
|
"flag"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
"github.com/google/uuid"
|
|
"github.com/grafana/tempo/modules/backendscheduler/work"
|
|
"github.com/grafana/tempo/modules/backendscheduler/work/tenantselector"
|
|
"github.com/grafana/tempo/modules/overrides"
|
|
"github.com/grafana/tempo/modules/storage"
|
|
"github.com/grafana/tempo/pkg/tempopb"
|
|
"github.com/grafana/tempo/pkg/util"
|
|
"github.com/grafana/tempo/tempodb"
|
|
"github.com/grafana/tempo/tempodb/backend"
|
|
"github.com/grafana/tempo/tempodb/blockselector"
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
var tracer = otel.Tracer("modules/backendscheduler/provider/compaction")
|
|
|
|
type CompactionConfig struct {
|
|
MeasureInterval time.Duration `yaml:"measure_interval"`
|
|
Compactor tempodb.CompactorConfig `yaml:"compaction"`
|
|
MaxJobsPerTenant int `yaml:"max_jobs_per_tenant"`
|
|
MinInputBlocks int `yaml:"min_input_blocks"`
|
|
MaxInputBlocks int `yaml:"max_input_blocks"`
|
|
MaxCompactionLevel int `yaml:"max_compaction_level"`
|
|
MinCycleInterval time.Duration `yaml:"min_cycle_interval"`
|
|
}
|
|
|
|
func (cfg *CompactionConfig) RegisterFlagsAndApplyDefaults(prefix string, f *flag.FlagSet) {
|
|
f.DurationVar(&cfg.MeasureInterval, prefix+"backend-scheduler.compaction-provider.measure-interval", time.Minute, "Interval at which to metric tenant blocklist")
|
|
f.IntVar(&cfg.MaxJobsPerTenant, prefix+"backend-scheduler.max-jobs-per-tenant", 1000, "Maximum number of jobs to run per tenant before moving on to the next tenant")
|
|
|
|
// Compaction
|
|
f.IntVar(&cfg.MinInputBlocks, prefix+".min-input-blocks", blockselector.DefaultMinInputBlocks, "Minimum number of blocks to compact in a single job.")
|
|
f.IntVar(&cfg.MaxInputBlocks, prefix+".max-input-blocks", blockselector.DefaultMaxInputBlocks, "Maximum number of blocks to compact in a single job.")
|
|
f.IntVar(&cfg.MaxCompactionLevel, prefix+".max-compaction-level", blockselector.DefaultMaxCompactionLevel, "Maximum compaction level to include in compaction jobs. 0 means no limit.")
|
|
|
|
// Tenant prioritization
|
|
f.DurationVar(&cfg.MinCycleInterval, prefix+".min-cycle-interval", 30*time.Second, "Minimum time between tenant prioritization cycles to prevent excessive CPU usage when no work is available.")
|
|
|
|
cfg.Compactor = tempodb.CompactorConfig{}
|
|
cfg.Compactor.RegisterFlagsAndApplyDefaults(util.PrefixConfig(prefix, "compaction"), f)
|
|
}
|
|
|
|
type CompactionProvider struct {
|
|
cfg CompactionConfig
|
|
logger log.Logger
|
|
|
|
// Dependencies needed for compaction job selection
|
|
store storage.Store
|
|
overrides overrides.Interface
|
|
|
|
// Scheduler calls required for this provider
|
|
sched Scheduler
|
|
|
|
// Dependencies needed for tenant selection
|
|
curPriority *tenantselector.PriorityQueue
|
|
curTenant *tenantselector.Item
|
|
curSelector blockselector.CompactionBlockSelector
|
|
lastPrioritizeTime time.Time
|
|
|
|
// Recent jobs cache for duplicate block ID prevention.
|
|
outstandingJobs map[string][]backend.UUID
|
|
outstandingJobsMtx sync.Mutex
|
|
}
|
|
|
|
func NewCompactionProvider(
|
|
cfg CompactionConfig,
|
|
logger log.Logger,
|
|
store storage.Store,
|
|
overrides overrides.Interface,
|
|
scheduler Scheduler,
|
|
) *CompactionProvider {
|
|
return &CompactionProvider{
|
|
cfg: cfg,
|
|
logger: logger,
|
|
store: store,
|
|
overrides: overrides,
|
|
curPriority: tenantselector.NewPriorityQueue(),
|
|
sched: scheduler,
|
|
outstandingJobs: make(map[string][]backend.UUID),
|
|
}
|
|
}
|
|
|
|
func (p *CompactionProvider) Start(ctx context.Context) <-chan *work.Job {
|
|
jobs := make(chan *work.Job, 1)
|
|
|
|
go func() {
|
|
defer close(jobs)
|
|
|
|
level.Info(p.logger).Log("msg", "compaction provider started")
|
|
|
|
var (
|
|
job *work.Job
|
|
curTenantJobCount int
|
|
span trace.Span
|
|
loopCtx context.Context
|
|
spanStarted bool
|
|
drained bool // Signal that we have drained the current work and should wait for the next poll
|
|
)
|
|
|
|
reset := func() {
|
|
metricTenantReset.WithLabelValues(p.curTenant.Value()).Inc()
|
|
span.AddEvent("tenant reset", trace.WithAttributes(
|
|
attribute.String("tenant_id", p.curTenant.Value()),
|
|
attribute.Int("job_count", curTenantJobCount),
|
|
))
|
|
p.curSelector = nil
|
|
p.curTenant = nil
|
|
curTenantJobCount = 0
|
|
span.End()
|
|
spanStarted = false
|
|
}
|
|
|
|
level.Info(p.logger).Log("msg", "compaction provider waiting for poll notification")
|
|
<-p.store.PollNotification(ctx)
|
|
|
|
for {
|
|
if ctx.Err() != nil {
|
|
level.Info(p.logger).Log("msg", "compaction provider stopping")
|
|
return
|
|
}
|
|
|
|
if !spanStarted {
|
|
loopCtx, span = tracer.Start(ctx, "compactionProviderLoop")
|
|
spanStarted = true
|
|
}
|
|
|
|
if p.curSelector == nil {
|
|
if !p.prepareNextTenant(loopCtx, drained) {
|
|
level.Info(p.logger).Log("msg", "received empty tenant")
|
|
// If we don't have a tenant with enough blocks, we signal drained to wait for next poll.
|
|
drained = true
|
|
metricEmptyTenantCycle.Inc()
|
|
span.AddEvent("no tenant selected")
|
|
} else {
|
|
// A tenant with enough blocks was selected, reset the drained state.
|
|
drained = false
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if curTenantJobCount >= p.cfg.MaxJobsPerTenant {
|
|
level.Info(p.logger).Log("msg", "max jobs per tenant reached, skipping to next tenant")
|
|
span.AddEvent("max jobs per tenant reached")
|
|
reset()
|
|
continue
|
|
}
|
|
|
|
job = p.createJob(loopCtx)
|
|
if job == nil {
|
|
level.Info(p.logger).Log("msg", "tenant exhausted, skipping to next tenant")
|
|
span.AddEvent("tenant exhausted")
|
|
// we don't have a job, reset the curTenant and try again
|
|
metricTenantEmptyJob.Inc()
|
|
reset()
|
|
continue
|
|
}
|
|
|
|
// Job successfully created, add to recent jobs cache before we send it.
|
|
p.addToRecentJobs(ctx, job)
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
level.Info(p.logger).Log("msg", "compaction provider stopping")
|
|
span.AddEvent("context done")
|
|
span.End()
|
|
return
|
|
case jobs <- job:
|
|
metricJobsCreated.WithLabelValues(p.curTenant.Value()).Inc()
|
|
curTenantJobCount++
|
|
span.AddEvent("job created", trace.WithAttributes(
|
|
attribute.String("job_id", job.ID),
|
|
attribute.String("tenant_id", p.curTenant.Value()),
|
|
))
|
|
span.End()
|
|
spanStarted = false
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Measure the tenants to get their current compaction status in a separate
|
|
// goroutine to avoid blocking the main job loop.
|
|
go func() {
|
|
measureTicker := time.NewTicker(p.cfg.MeasureInterval)
|
|
defer measureTicker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
level.Info(p.logger).Log("msg", "compaction provider measure ticker stopping")
|
|
return
|
|
case <-measureTicker.C:
|
|
p.measureTenants()
|
|
}
|
|
}
|
|
}()
|
|
|
|
return jobs
|
|
}
|
|
|
|
func (p *CompactionProvider) prepareNextTenant(ctx context.Context, drained bool) bool {
|
|
_, span := tracer.Start(ctx, "prepareNextTenant")
|
|
defer span.End()
|
|
|
|
if p.curPriority.Len() == 0 {
|
|
// Rate limit calls to prioritizeTenants to prevent excessive CPU usage
|
|
// when cycling through tenants with no available work.
|
|
//
|
|
// We only expect new work for tenants after a the next blocklist poll. If
|
|
// we have been drained, wait for the next poll.
|
|
|
|
if drained {
|
|
level.Debug(p.logger).Log("msg", "rate limiting tenant prioritization; waiting for next poll")
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-p.store.PollNotification(ctx):
|
|
// We waited for the poll, but we may have been cancelled in the meantime.
|
|
if ctx.Err() != nil {
|
|
return false
|
|
}
|
|
|
|
// Continue to prioritizeTenants
|
|
}
|
|
}
|
|
|
|
if elapsed := time.Since(p.lastPrioritizeTime); elapsed < p.cfg.MinCycleInterval {
|
|
level.Debug(p.logger).Log("msg", "rate limiting tenant prioritization; waiting")
|
|
time.Sleep(p.cfg.MinCycleInterval - elapsed)
|
|
|
|
// Continue to prioritizeTenants
|
|
}
|
|
|
|
p.prioritizeTenants(ctx)
|
|
p.lastPrioritizeTime = time.Now()
|
|
if p.curPriority.Len() == 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
p.curTenant = heap.Pop(p.curPriority).(*tenantselector.Item)
|
|
if p.curTenant == nil {
|
|
span.AddEvent("no more tenants to compact")
|
|
return false
|
|
}
|
|
|
|
level.Info(p.logger).Log("msg", "new tenant selected", "tenant_id", p.curTenant.Value())
|
|
|
|
p.curSelector, _ = p.newBlockSelector(p.curTenant.Value())
|
|
return true
|
|
}
|
|
|
|
func (p *CompactionProvider) createJob(ctx context.Context) *work.Job {
|
|
_, span := tracer.Start(ctx, "createJob")
|
|
defer span.End()
|
|
|
|
span.SetAttributes(attribute.String("tenant_id", p.curTenant.Value()))
|
|
|
|
input, ok := p.getNextBlockIDs(ctx)
|
|
if !ok {
|
|
span.AddEvent("not-enough-input-blocks", trace.WithAttributes(
|
|
attribute.Int("input_blocks", len(input)),
|
|
))
|
|
|
|
span.SetStatus(codes.Error, "not enough input blocks for compaction")
|
|
return nil
|
|
}
|
|
|
|
span.AddEvent("input blocks selected", trace.WithAttributes(
|
|
attribute.Int("input_blocks", len(input)),
|
|
attribute.StringSlice("input_block_ids", input),
|
|
))
|
|
span.SetStatus(codes.Ok, "compaction job created")
|
|
return &work.Job{
|
|
ID: uuid.New().String(),
|
|
Type: tempopb.JobType_JOB_TYPE_COMPACTION,
|
|
JobDetail: tempopb.JobDetail{
|
|
Tenant: p.curTenant.Value(),
|
|
Compaction: &tempopb.CompactionDetail{Input: input},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (p *CompactionProvider) getNextBlockIDs(_ context.Context) ([]string, bool) {
|
|
ids := make([]string, 0, p.cfg.MaxInputBlocks)
|
|
|
|
toBeCompacted, _ := p.curSelector.BlocksToCompact()
|
|
|
|
if len(toBeCompacted) == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
for _, b := range toBeCompacted {
|
|
ids = append(ids, b.BlockID.String())
|
|
}
|
|
|
|
return ids, len(ids) >= p.cfg.MinInputBlocks
|
|
}
|
|
|
|
// prioritizeTenants prioritizes tenants based on the number of outstanding blocks.
|
|
func (p *CompactionProvider) prioritizeTenants(ctx context.Context) {
|
|
tenants := []tenantselector.Tenant{}
|
|
|
|
_, span := tracer.Start(ctx, "prioritizeTenants")
|
|
defer span.End()
|
|
|
|
p.curPriority = tenantselector.NewPriorityQueue() // wipe and restart
|
|
|
|
var (
|
|
blocklistLen int
|
|
blockSelector blockselector.CompactionBlockSelector
|
|
outstandingBlocks int
|
|
toBeCompacted []*backend.BlockMeta
|
|
)
|
|
|
|
for _, tenantID := range p.store.Tenants() {
|
|
if p.overrides.CompactionDisabled(tenantID) {
|
|
continue
|
|
}
|
|
|
|
outstandingBlocks = 0
|
|
clear(toBeCompacted)
|
|
|
|
blockSelector, blocklistLen = p.newBlockSelector(tenantID)
|
|
|
|
// Measure the outstanding blocks
|
|
for {
|
|
toBeCompacted, _ = blockSelector.BlocksToCompact()
|
|
if len(toBeCompacted) == 0 {
|
|
span.AddEvent("no-more-blocks-to-compact", trace.WithAttributes(
|
|
attribute.String("tenant_id", tenantID),
|
|
attribute.Int("outstanding_blocks", len(toBeCompacted)),
|
|
))
|
|
break
|
|
}
|
|
|
|
outstandingBlocks += len(toBeCompacted)
|
|
|
|
span.AddEvent("found-blocks-to-compact", trace.WithAttributes(
|
|
attribute.String("tenant_id", tenantID),
|
|
attribute.Int("outstanding_blocks", len(toBeCompacted)),
|
|
))
|
|
}
|
|
|
|
tenants = append(tenants, tenantselector.Tenant{
|
|
ID: tenantID,
|
|
BlocklistLength: blocklistLen,
|
|
OutstandingBlocklistLength: outstandingBlocks,
|
|
})
|
|
}
|
|
|
|
var (
|
|
ts = tenantselector.NewBlockListWeightedTenantSelector(tenants)
|
|
item *tenantselector.Item
|
|
priority int
|
|
)
|
|
|
|
for _, tenant := range tenants {
|
|
priority = ts.PriorityForTenant(tenant.ID)
|
|
|
|
if priority >= p.cfg.MinInputBlocks {
|
|
item = tenantselector.NewItem(tenant.ID, priority)
|
|
heap.Push(p.curPriority, item)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *CompactionProvider) measureTenants() {
|
|
_, span := tracer.Start(context.Background(), "measureTenants")
|
|
defer span.End()
|
|
|
|
owns := func(_ string) bool {
|
|
return true
|
|
}
|
|
|
|
var blockSelector blockselector.CompactionBlockSelector
|
|
for _, tenant := range p.store.Tenants() {
|
|
blockSelector, _ = p.newBlockSelector(tenant)
|
|
tempodb.MeasureOutstandingBlocks(tenant, blockSelector, owns)
|
|
}
|
|
}
|
|
|
|
func (p *CompactionProvider) newBlockSelector(tenantID string) (blockselector.CompactionBlockSelector, int) {
|
|
var (
|
|
fullBlocklist = p.store.BlockMetas(tenantID)
|
|
window = p.overrides.MaxCompactionRange(tenantID)
|
|
blocklist = make([]*backend.BlockMeta, 0, len(fullBlocklist))
|
|
)
|
|
|
|
// Query the work for the jobs and build up a list of UUIDs which we can match against and skip on the selector.
|
|
var (
|
|
inProgressBlockIDs = make(map[backend.UUID]struct{})
|
|
bid backend.UUID
|
|
err error
|
|
)
|
|
|
|
for _, job := range p.sched.ListJobs() {
|
|
// Clean up recent jobs cache when we find a job which has been persisted
|
|
p.outstandingJobsMtx.Lock()
|
|
delete(p.outstandingJobs, job.ID)
|
|
p.outstandingJobsMtx.Unlock()
|
|
|
|
if job.Tenant() != tenantID {
|
|
continue
|
|
}
|
|
|
|
if job.GetType() == tempopb.JobType_JOB_TYPE_UNSPECIFIED {
|
|
continue
|
|
}
|
|
|
|
// NOTE: We check the compaction job input, but not the output. This is to
|
|
// allow the output of one job to become the input of another.
|
|
for _, blockID := range job.GetCompactionInput() {
|
|
bid, err = backend.ParseUUID(blockID)
|
|
if err != nil {
|
|
level.Error(p.logger).Log("msg", "failed to parse block ID", "block_id", blockID, "err", err)
|
|
continue
|
|
}
|
|
inProgressBlockIDs[bid] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Also include blocks from recent jobs cache to prevent duplicate job creation
|
|
p.outstandingJobsMtx.Lock()
|
|
for _, recentBlockIDs := range p.outstandingJobs {
|
|
for _, blockID := range recentBlockIDs {
|
|
inProgressBlockIDs[blockID] = struct{}{}
|
|
}
|
|
}
|
|
p.outstandingJobsMtx.Unlock()
|
|
|
|
for _, block := range fullBlocklist {
|
|
if _, ok := inProgressBlockIDs[block.BlockID]; !ok {
|
|
// Include blocks that are not already in the input list from another job or recent jobs
|
|
blocklist = append(blocklist, block)
|
|
}
|
|
}
|
|
|
|
if window == 0 {
|
|
window = p.cfg.Compactor.MaxCompactionRange
|
|
}
|
|
|
|
return blockselector.NewTimeWindowBlockSelector(
|
|
blocklist,
|
|
window,
|
|
p.cfg.Compactor.MaxCompactionObjects,
|
|
p.cfg.Compactor.MaxBlockBytes,
|
|
p.cfg.MinInputBlocks,
|
|
p.cfg.MaxInputBlocks,
|
|
p.cfg.MaxCompactionLevel,
|
|
), len(blocklist)
|
|
}
|
|
|
|
// addToRecentJobs adds a job to the recent jobs cache
|
|
func (p *CompactionProvider) addToRecentJobs(ctx context.Context, job *work.Job) {
|
|
_, span := tracer.Start(ctx, "addToRecentJobs")
|
|
defer span.End()
|
|
|
|
if job.Type != tempopb.JobType_JOB_TYPE_COMPACTION || job.JobDetail.Compaction == nil {
|
|
return
|
|
}
|
|
|
|
// Don't cache jobs with empty input
|
|
if len(job.JobDetail.Compaction.Input) == 0 {
|
|
return
|
|
}
|
|
|
|
// Copy the input block IDs
|
|
blockIDs := make([]backend.UUID, len(job.JobDetail.Compaction.Input))
|
|
for i, blockID := range job.JobDetail.Compaction.Input {
|
|
bid, err := backend.ParseUUID(blockID)
|
|
if err != nil {
|
|
level.Error(p.logger).Log("msg", "failed to parse block ID", "block_id", blockID, "err", err)
|
|
return
|
|
}
|
|
blockIDs[i] = bid
|
|
}
|
|
|
|
p.outstandingJobsMtx.Lock()
|
|
p.outstandingJobs[job.ID] = blockIDs
|
|
p.outstandingJobsMtx.Unlock()
|
|
}
|