github.com/grafana/pyroscope@v1.18.0/pkg/storegateway/bucket.go (about) 1 package storegateway 2 3 import ( 4 "context" 5 "os" 6 "path" 7 "path/filepath" 8 "sort" 9 "sync" 10 "time" 11 12 "github.com/go-kit/log" 13 "github.com/go-kit/log/level" 14 "github.com/oklog/ulid/v2" 15 "github.com/pkg/errors" 16 "github.com/prometheus/client_golang/prometheus" 17 "github.com/prometheus/common/model" 18 19 phlareobj "github.com/grafana/pyroscope/pkg/objstore" 20 "github.com/grafana/pyroscope/pkg/phlaredb" 21 "github.com/grafana/pyroscope/pkg/phlaredb/block" 22 ) 23 24 // TODO move this to a config. 25 const blockSyncConcurrency = 100 26 27 type BucketStoreStats struct { 28 // BlocksLoaded is the number of blocks currently loaded in the bucket store. 29 BlocksLoaded int 30 } 31 32 type BucketStore struct { 33 bucket phlareobj.Bucket 34 fetcher block.MetadataFetcher 35 36 tenantID, syncDir string 37 38 logger log.Logger 39 40 blocksMx sync.RWMutex 41 blocks map[ulid.ULID]*Block 42 blockSet *bucketBlockSet 43 44 metrics *Metrics 45 stats BucketStoreStats 46 } 47 48 func NewBucketStore(bucket phlareobj.Bucket, fetcher block.MetadataFetcher, tenantID string, syncDir string, logger log.Logger, reg prometheus.Registerer) (*BucketStore, error) { 49 s := &BucketStore{ 50 fetcher: fetcher, 51 bucket: phlareobj.NewTenantBucketClient(tenantID, bucket, nil), 52 tenantID: tenantID, 53 syncDir: syncDir, 54 logger: log.With(logger, "tenant", tenantID), 55 blockSet: newBucketBlockSet(), 56 blocks: map[ulid.ULID]*Block{}, 57 metrics: NewBucketStoreMetrics(prometheus.WrapRegistererWith( 58 prometheus.Labels{"tenant": tenantID}, 59 reg, 60 )), 61 } 62 63 if err := os.MkdirAll(syncDir, 0o750); err != nil { 64 return nil, errors.Wrap(err, "create dir") 65 } 66 67 return s, nil 68 } 69 70 func (b *BucketStore) InitialSync(ctx context.Context) error { 71 if err := b.SyncBlocks(ctx); err != nil { 72 return errors.Wrap(err, "sync block") 73 } 74 75 fis, err := os.ReadDir(b.syncDir) 76 if err != nil { 77 return errors.Wrap(err, "read dir") 78 } 79 names := make([]string, 0, len(fis)) 80 for _, fi := range fis { 81 names = append(names, fi.Name()) 82 } 83 for _, n := range names { 84 id, ok := block.IsBlockDir(n) 85 if !ok { 86 continue 87 } 88 if b := b.getBlock(id); b != nil { 89 continue 90 } 91 92 // No such block loaded, remove the local dir. 93 if err := os.RemoveAll(path.Join(b.syncDir, id.String())); err != nil { 94 level.Warn(b.logger).Log("msg", "failed to remove block which is not needed", "err", err) 95 } 96 } 97 return nil 98 } 99 100 func (s *BucketStore) getBlock(id ulid.ULID) *Block { 101 s.blocksMx.RLock() 102 defer s.blocksMx.RUnlock() 103 return s.blocks[id] 104 } 105 106 func (s *BucketStore) SyncBlocks(ctx context.Context) error { 107 // TODO sounds like we should get the meta this is just a list of ids 108 metas, _, metaFetchErr := s.fetcher.Fetch(ctx) 109 // For partial view allow adding new blocks at least. 110 if metaFetchErr != nil && metas == nil { 111 return metaFetchErr 112 } 113 114 var wg sync.WaitGroup 115 blockc := make(chan *block.Meta) 116 117 for i := 0; i < blockSyncConcurrency; i++ { 118 wg.Add(1) 119 go func() { 120 for meta := range blockc { 121 if err := s.addBlock(ctx, meta); err != nil { 122 continue 123 } 124 } 125 wg.Done() 126 }() 127 } 128 129 for id, meta := range metas { 130 if b := s.getBlock(id); b != nil { 131 continue 132 } 133 select { 134 case <-ctx.Done(): 135 case blockc <- meta: 136 } 137 } 138 139 close(blockc) 140 wg.Wait() 141 142 if metaFetchErr != nil { 143 return metaFetchErr 144 } 145 146 // Drop all blocks that are no longer present in the bucket. 147 for id := range s.blocks { 148 if _, ok := metas[id]; ok { 149 continue 150 } 151 if err := s.removeBlock(id); err != nil { 152 level.Warn(s.logger).Log("msg", "drop of outdated block failed", "block", id, "err", err) 153 } 154 level.Info(s.logger).Log("msg", "dropped outdated block", "block", id) 155 } 156 s.stats.BlocksLoaded = len(s.blocks) 157 158 return nil 159 } 160 161 func (bs *BucketStore) addBlock(ctx context.Context, meta *block.Meta) (err error) { 162 level.Debug(bs.logger).Log("msg", "loading new block", "id", meta.ULID) 163 164 dir := bs.localPath(meta.ULID.String()) 165 start := time.Now() 166 defer func() { 167 if err != nil { 168 bs.metrics.blockLoadFailures.Inc() 169 if err2 := os.RemoveAll(dir); err2 != nil { 170 level.Warn(bs.logger).Log("msg", "failed to remove block we cannot load", "err", err2) 171 } 172 level.Warn(bs.logger).Log("msg", "loading block failed", "elapsed", time.Since(start), "id", meta.ULID, "err", err) 173 } else { 174 level.Info(bs.logger).Log("msg", "loaded new block", "elapsed", time.Since(start), "id", meta.ULID) 175 } 176 }() 177 178 bs.metrics.blockLoads.Inc() 179 180 b, err := func() (*Block, error) { 181 bs.blocksMx.Lock() 182 defer bs.blocksMx.Unlock() 183 ctx = phlaredb.ContextWithBlockMetrics(ctx, bs.metrics.blockMetrics) 184 b, err := bs.createBlock(ctx, meta) 185 if err != nil { 186 return nil, errors.Wrap(err, "load block from disk") 187 } 188 bs.blockSet.add(b) 189 bs.blocks[meta.ULID] = b 190 return b, nil 191 }() 192 if err != nil { 193 return err 194 } 195 // Load the block into memory if it's within the last 24 hours. 196 // Todo make this configurable 197 if phlaredb.InRange(b, model.Now().Add(-24*time.Hour), model.Now()) { 198 level.Debug(bs.logger).Log("msg", "opening block", 199 "id", meta.ULID.String(), 200 "min", b.meta.MinTime.Time().Format(time.RFC3339), 201 "max", b.meta.MaxTime.Time().Format(time.RFC3339), 202 ) 203 204 start := time.Now() 205 defer func() { 206 level.Info(bs.logger).Log("msg", "block opened", "duration", time.Since(start), "id", meta.ULID.String()) 207 }() 208 if err := b.Open(ctx); err != nil { 209 level.Error(bs.logger).Log("msg", "open block", "err", err) 210 } 211 } 212 return nil 213 } 214 215 func (b *BucketStore) Stats() BucketStoreStats { 216 return b.stats 217 } 218 219 func (s *BucketStore) removeBlock(id ulid.ULID) (returnErr error) { 220 defer func() { 221 if returnErr != nil { 222 s.metrics.blockDropFailures.Inc() 223 } 224 }() 225 226 s.blocksMx.Lock() 227 b, ok := s.blocks[id] 228 if ok { 229 s.blockSet.remove(id) 230 delete(s.blocks, id) 231 } 232 s.blocksMx.Unlock() 233 234 if !ok { 235 return nil 236 } 237 238 // // The block has already been removed from BucketStore, so we track it as removed 239 // // even if releasing its resources could fail below. 240 s.metrics.blockDrops.Inc() 241 242 if err := b.Close(); err != nil { 243 return errors.Wrap(err, "close block") 244 } 245 if err := os.RemoveAll(s.localPath(id.String())); err != nil { 246 return errors.Wrap(err, "delete block") 247 } 248 return nil 249 } 250 251 func (s *BucketStore) localPath(id string) string { 252 return filepath.Join(s.syncDir, id) 253 } 254 255 // RemoveBlocksAndClose remove all blocks from local disk and releases all resources associated with the BucketStore. 256 func (s *BucketStore) RemoveBlocksAndClose() error { 257 if err := os.RemoveAll(s.syncDir); err != nil { 258 return errors.Wrap(err, "delete block") 259 } 260 return nil 261 } 262 263 // bucketBlockSet holds all blocks. 264 type bucketBlockSet struct { 265 mtx sync.RWMutex 266 blocks []*Block // Blocks sorted by mint, then maxt. 267 } 268 269 // newBucketBlockSet initializes a new set with the known downsampling windows hard-configured. 270 // (Mimir only supports no-downsampling) 271 // The set currently does not support arbitrary ranges. 272 func newBucketBlockSet() *bucketBlockSet { 273 return &bucketBlockSet{} 274 } 275 276 func (s *bucketBlockSet) add(b *Block) { 277 s.mtx.Lock() 278 defer s.mtx.Unlock() 279 280 s.blocks = append(s.blocks, b) 281 282 // Always sort blocks by min time, then max time. 283 sort.Slice(s.blocks, func(j, k int) bool { 284 if s.blocks[j].meta.MinTime == s.blocks[k].meta.MinTime { 285 return s.blocks[j].meta.MaxTime < s.blocks[k].meta.MaxTime 286 } 287 return s.blocks[j].meta.MinTime < s.blocks[k].meta.MinTime 288 }) 289 } 290 291 func (s *bucketBlockSet) remove(id ulid.ULID) { 292 s.mtx.Lock() 293 defer s.mtx.Unlock() 294 295 for i, b := range s.blocks { 296 if b.meta.ULID != id { 297 continue 298 } 299 s.blocks = append(s.blocks[:i], s.blocks[i+1:]...) 300 return 301 } 302 } 303 304 // getFor returns a time-ordered list of blocks that cover date between mint and maxt. 305 // It supports overlapping blocks. 306 // 307 // NOTE: s.blocks are expected to be sorted in minTime order. 308 func (s *bucketBlockSet) getFor(mint, maxt model.Time) (bs []*Block) { 309 if mint > maxt { 310 return nil 311 } 312 313 s.mtx.RLock() 314 defer s.mtx.RUnlock() 315 316 // Fill the given interval with the blocks within the request mint and maxt. 317 for _, b := range s.blocks { 318 if b.meta.MaxTime <= mint { 319 continue 320 } 321 // NOTE: Block intervals are half-open: [b.MinTime, b.MaxTime). 322 if b.meta.MinTime > maxt { 323 break 324 } 325 326 bs = append(bs, b) 327 } 328 329 return bs 330 }