github.com/thanos-io/thanos@v0.32.5/pkg/block/block.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 // Package block contains common functionality for interacting with TSDB blocks 5 // in the context of Thanos. 6 package block 7 8 import ( 9 "bytes" 10 "context" 11 "encoding/json" 12 "io" 13 "os" 14 "path" 15 "path/filepath" 16 "sort" 17 "strings" 18 "time" 19 20 "github.com/go-kit/log" 21 "github.com/go-kit/log/level" 22 "github.com/oklog/ulid" 23 "github.com/pkg/errors" 24 "github.com/prometheus/client_golang/prometheus" 25 "github.com/thanos-io/objstore" 26 27 "github.com/thanos-io/thanos/pkg/block/metadata" 28 "github.com/thanos-io/thanos/pkg/runutil" 29 ) 30 31 const ( 32 // MetaFilename is the known JSON filename for meta information. 33 MetaFilename = "meta.json" 34 // IndexFilename is the known index file for block index. 35 IndexFilename = "index" 36 // IndexHeaderFilename is the canonical name for binary index header file that stores essential information. 37 IndexHeaderFilename = "index-header" 38 // ChunksDirname is the known dir name for chunks with compressed samples. 39 ChunksDirname = "chunks" 40 41 // DebugMetas is a directory for debug meta files that happen in the past. Useful for debugging. 42 DebugMetas = "debug/metas" 43 ) 44 45 // Download downloads directory that is mean to be block directory. If any of the files 46 // have a hash calculated in the meta file and it matches with what is in the destination path then 47 // we do not download it. We always re-download the meta file. 48 func Download(ctx context.Context, logger log.Logger, bucket objstore.Bucket, id ulid.ULID, dst string, options ...objstore.DownloadOption) error { 49 if err := os.MkdirAll(dst, 0750); err != nil { 50 return errors.Wrap(err, "create dir") 51 } 52 53 if err := objstore.DownloadFile(ctx, logger, bucket, path.Join(id.String(), MetaFilename), path.Join(dst, MetaFilename)); err != nil { 54 return err 55 } 56 m, err := metadata.ReadFromDir(dst) 57 if err != nil { 58 return errors.Wrapf(err, "reading meta from %s", dst) 59 } 60 61 ignoredPaths := []string{MetaFilename} 62 for _, fl := range m.Thanos.Files { 63 if fl.Hash == nil || fl.Hash.Func == metadata.NoneFunc || fl.RelPath == "" { 64 continue 65 } 66 actualHash, err := metadata.CalculateHash(filepath.Join(dst, fl.RelPath), fl.Hash.Func, logger) 67 if err != nil { 68 level.Info(logger).Log("msg", "failed to calculate hash when downloading; re-downloading", "relPath", fl.RelPath, "err", err) 69 continue 70 } 71 72 if fl.Hash.Equal(&actualHash) { 73 ignoredPaths = append(ignoredPaths, fl.RelPath) 74 } 75 } 76 77 if err := objstore.DownloadDir(ctx, logger, bucket, id.String(), id.String(), dst, append(options, objstore.WithDownloadIgnoredPaths(ignoredPaths...))...); err != nil { 78 return err 79 } 80 81 chunksDir := filepath.Join(dst, ChunksDirname) 82 _, err = os.Stat(chunksDir) 83 if os.IsNotExist(err) { 84 // This can happen if block is empty. We cannot easily upload empty directory, so create one here. 85 return os.Mkdir(chunksDir, os.ModePerm) 86 } 87 88 if err != nil { 89 return errors.Wrapf(err, "stat %s", chunksDir) 90 } 91 92 return nil 93 } 94 95 // Upload uploads a TSDB block to the object storage. It verifies basic 96 // features of Thanos block. 97 func Upload(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc, options ...objstore.UploadOption) error { 98 return upload(ctx, logger, bkt, bdir, hf, true, options...) 99 } 100 101 // UploadPromBlock uploads a TSDB block to the object storage. It assumes 102 // the block is used in Prometheus so it doesn't check Thanos external labels. 103 func UploadPromBlock(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc, options ...objstore.UploadOption) error { 104 return upload(ctx, logger, bkt, bdir, hf, false, options...) 105 } 106 107 // upload uploads block from given block dir that ends with block id. 108 // It makes sure cleanup is done on error to avoid partial block uploads. 109 // TODO(bplotka): Ensure bucket operations have reasonable backoff retries. 110 // NOTE: Upload updates `meta.Thanos.File` section. 111 func upload(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc, checkExternalLabels bool, options ...objstore.UploadOption) error { 112 df, err := os.Stat(bdir) 113 if err != nil { 114 return err 115 } 116 if !df.IsDir() { 117 return errors.Errorf("%s is not a directory", bdir) 118 } 119 120 // Verify dir. 121 id, err := ulid.Parse(df.Name()) 122 if err != nil { 123 return errors.Wrap(err, "not a block dir") 124 } 125 126 meta, err := metadata.ReadFromDir(bdir) 127 if err != nil { 128 // No meta or broken meta file. 129 return errors.Wrap(err, "read meta") 130 } 131 132 if checkExternalLabels { 133 if meta.Thanos.Labels == nil || len(meta.Thanos.Labels) == 0 { 134 return errors.New("empty external labels are not allowed for Thanos block.") 135 } 136 } 137 138 metaEncoded := strings.Builder{} 139 meta.Thanos.Files, err = GatherFileStats(bdir, hf, logger) 140 if err != nil { 141 return errors.Wrap(err, "gather meta file stats") 142 } 143 144 if err := meta.Write(&metaEncoded); err != nil { 145 return errors.Wrap(err, "encode meta file") 146 } 147 148 if err := objstore.UploadDir(ctx, logger, bkt, filepath.Join(bdir, ChunksDirname), path.Join(id.String(), ChunksDirname), options...); err != nil { 149 return cleanUp(logger, bkt, id, errors.Wrap(err, "upload chunks")) 150 } 151 152 if err := objstore.UploadFile(ctx, logger, bkt, filepath.Join(bdir, IndexFilename), path.Join(id.String(), IndexFilename)); err != nil { 153 return cleanUp(logger, bkt, id, errors.Wrap(err, "upload index")) 154 } 155 156 // Meta.json always need to be uploaded as a last item. This will allow to assume block directories without meta file to be pending uploads. 157 if err := bkt.Upload(ctx, path.Join(id.String(), MetaFilename), strings.NewReader(metaEncoded.String())); err != nil { 158 // Don't call cleanUp here. Despite getting error, meta.json may have been uploaded in certain cases, 159 // and even though cleanUp will not see it yet, meta.json may appear in the bucket later. 160 // (Eg. S3 is known to behave this way when it returns 503 "SlowDown" error). 161 // If meta.json is not uploaded, this will produce partial blocks, but such blocks will be cleaned later. 162 return errors.Wrap(err, "upload meta file") 163 } 164 165 return nil 166 } 167 168 func cleanUp(logger log.Logger, bkt objstore.Bucket, id ulid.ULID, err error) error { 169 // Cleanup the dir with an uncancelable context. 170 cleanErr := Delete(context.Background(), logger, bkt, id) 171 if cleanErr != nil { 172 return errors.Wrapf(err, "failed to clean block after upload issue. Partial block in system. Err: %s", err.Error()) 173 } 174 return err 175 } 176 177 // MarkForDeletion creates a file which stores information about when the block was marked for deletion. 178 func MarkForDeletion(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID, details string, markedForDeletion prometheus.Counter) error { 179 deletionMarkFile := path.Join(id.String(), metadata.DeletionMarkFilename) 180 deletionMarkExists, err := bkt.Exists(ctx, deletionMarkFile) 181 if err != nil { 182 return errors.Wrapf(err, "check exists %s in bucket", deletionMarkFile) 183 } 184 if deletionMarkExists { 185 level.Warn(logger).Log("msg", "requested to mark for deletion, but file already exists; this should not happen; investigate", "err", errors.Errorf("file %s already exists in bucket", deletionMarkFile)) 186 return nil 187 } 188 189 deletionMark, err := json.Marshal(metadata.DeletionMark{ 190 ID: id, 191 DeletionTime: time.Now().Unix(), 192 Version: metadata.DeletionMarkVersion1, 193 Details: details, 194 }) 195 if err != nil { 196 return errors.Wrap(err, "json encode deletion mark") 197 } 198 199 if err := bkt.Upload(ctx, deletionMarkFile, bytes.NewBuffer(deletionMark)); err != nil { 200 return errors.Wrapf(err, "upload file %s to bucket", deletionMarkFile) 201 } 202 markedForDeletion.Inc() 203 level.Info(logger).Log("msg", "block has been marked for deletion", "block", id) 204 return nil 205 } 206 207 // Delete removes directory that is meant to be block directory. 208 // NOTE: Always prefer this method for deleting blocks. 209 // - We have to delete block's files in the certain order (meta.json first and deletion-mark.json last) 210 // to ensure we don't end up with malformed partial blocks. Thanos system handles well partial blocks 211 // only if they don't have meta.json. If meta.json is present Thanos assumes valid block. 212 // - This avoids deleting empty dir (whole bucket) by mistake. 213 func Delete(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID) error { 214 metaFile := path.Join(id.String(), MetaFilename) 215 deletionMarkFile := path.Join(id.String(), metadata.DeletionMarkFilename) 216 217 // Delete block meta file. 218 ok, err := bkt.Exists(ctx, metaFile) 219 if err != nil { 220 return errors.Wrapf(err, "stat %s", metaFile) 221 } 222 223 if ok { 224 if err := bkt.Delete(ctx, metaFile); err != nil { 225 return errors.Wrapf(err, "delete %s", metaFile) 226 } 227 level.Debug(logger).Log("msg", "deleted file", "file", metaFile, "bucket", bkt.Name()) 228 } 229 230 // Delete the block objects, but skip: 231 // - The metaFile as we just deleted. This is required for eventual object storages (list after write). 232 // - The deletionMarkFile as we'll delete it at last. 233 err = deleteDirRec(ctx, logger, bkt, id.String(), func(name string) bool { 234 return name == metaFile || name == deletionMarkFile 235 }) 236 if err != nil { 237 return err 238 } 239 240 // Delete block deletion mark. 241 ok, err = bkt.Exists(ctx, deletionMarkFile) 242 if err != nil { 243 return errors.Wrapf(err, "stat %s", deletionMarkFile) 244 } 245 246 if ok { 247 if err := bkt.Delete(ctx, deletionMarkFile); err != nil { 248 return errors.Wrapf(err, "delete %s", deletionMarkFile) 249 } 250 level.Debug(logger).Log("msg", "deleted file", "file", deletionMarkFile, "bucket", bkt.Name()) 251 } 252 253 return nil 254 } 255 256 // deleteDirRec removes all objects prefixed with dir from the bucket. It skips objects that return true for the passed keep function. 257 // NOTE: For objects removal use `block.Delete` strictly. 258 func deleteDirRec(ctx context.Context, logger log.Logger, bkt objstore.Bucket, dir string, keep func(name string) bool) error { 259 return bkt.Iter(ctx, dir, func(name string) error { 260 // If we hit a directory, call DeleteDir recursively. 261 if strings.HasSuffix(name, objstore.DirDelim) { 262 return deleteDirRec(ctx, logger, bkt, name, keep) 263 } 264 if keep(name) { 265 return nil 266 } 267 if err := bkt.Delete(ctx, name); err != nil { 268 return err 269 } 270 level.Debug(logger).Log("msg", "deleted file", "file", name, "bucket", bkt.Name()) 271 return nil 272 }) 273 } 274 275 // DownloadMeta downloads only meta file from bucket by block ID. 276 // TODO(bwplotka): Differentiate between network error & partial upload. 277 func DownloadMeta(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID) (metadata.Meta, error) { 278 rc, err := bkt.Get(ctx, path.Join(id.String(), MetaFilename)) 279 if err != nil { 280 return metadata.Meta{}, errors.Wrapf(err, "meta.json bkt get for %s", id.String()) 281 } 282 defer runutil.CloseWithLogOnErr(logger, rc, "download meta bucket client") 283 284 var m metadata.Meta 285 286 obj, err := io.ReadAll(rc) 287 if err != nil { 288 return metadata.Meta{}, errors.Wrapf(err, "read meta.json for block %s", id.String()) 289 } 290 291 if err = json.Unmarshal(obj, &m); err != nil { 292 return metadata.Meta{}, errors.Wrapf(err, "unmarshal meta.json for block %s", id.String()) 293 } 294 295 return m, nil 296 } 297 298 func IsBlockMetaFile(path string) bool { 299 return filepath.Base(path) == MetaFilename 300 } 301 302 func IsBlockDir(path string) (id ulid.ULID, ok bool) { 303 id, err := ulid.Parse(filepath.Base(path)) 304 return id, err == nil 305 } 306 307 // GetSegmentFiles returns list of segment files for given block. Paths are relative to the chunks directory. 308 // In case of errors, nil is returned. 309 func GetSegmentFiles(blockDir string) []string { 310 files, err := os.ReadDir(filepath.Join(blockDir, ChunksDirname)) 311 if err != nil { 312 return nil 313 } 314 315 // ReadDir returns files in sorted order already. 316 var result []string 317 for _, f := range files { 318 result = append(result, f.Name()) 319 } 320 return result 321 } 322 323 // GatherFileStats returns metadata.File entry for files inside TSDB block (index, chunks, meta.json). 324 func GatherFileStats(blockDir string, hf metadata.HashFunc, logger log.Logger) (res []metadata.File, _ error) { 325 files, err := os.ReadDir(filepath.Join(blockDir, ChunksDirname)) 326 if err != nil { 327 return nil, errors.Wrapf(err, "read dir %v", filepath.Join(blockDir, ChunksDirname)) 328 } 329 for _, f := range files { 330 fi, err := f.Info() 331 if err != nil { 332 return nil, errors.Wrapf(err, "getting file info %v", filepath.Join(ChunksDirname, f.Name())) 333 } 334 335 mf := metadata.File{ 336 RelPath: filepath.Join(ChunksDirname, f.Name()), 337 SizeBytes: fi.Size(), 338 } 339 if hf != metadata.NoneFunc && !f.IsDir() { 340 h, err := metadata.CalculateHash(filepath.Join(blockDir, ChunksDirname, f.Name()), hf, logger) 341 if err != nil { 342 return nil, errors.Wrapf(err, "calculate hash %v", filepath.Join(ChunksDirname, f.Name())) 343 } 344 mf.Hash = &h 345 } 346 res = append(res, mf) 347 } 348 349 indexFile, err := os.Stat(filepath.Join(blockDir, IndexFilename)) 350 if err != nil { 351 return nil, errors.Wrapf(err, "stat %v", filepath.Join(blockDir, IndexFilename)) 352 } 353 mf := metadata.File{ 354 RelPath: indexFile.Name(), 355 SizeBytes: indexFile.Size(), 356 } 357 if hf != metadata.NoneFunc { 358 h, err := metadata.CalculateHash(filepath.Join(blockDir, IndexFilename), hf, logger) 359 if err != nil { 360 return nil, errors.Wrapf(err, "calculate hash %v", indexFile.Name()) 361 } 362 mf.Hash = &h 363 } 364 res = append(res, mf) 365 366 metaFile, err := os.Stat(filepath.Join(blockDir, MetaFilename)) 367 if err != nil { 368 return nil, errors.Wrapf(err, "stat %v", filepath.Join(blockDir, MetaFilename)) 369 } 370 res = append(res, metadata.File{RelPath: metaFile.Name()}) 371 372 sort.Slice(res, func(i, j int) bool { 373 return strings.Compare(res[i].RelPath, res[j].RelPath) < 0 374 }) 375 return res, err 376 } 377 378 // MarkForNoCompact creates a file which marks block to be not compacted. 379 func MarkForNoCompact(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID, reason metadata.NoCompactReason, details string, markedForNoCompact prometheus.Counter) error { 380 m := path.Join(id.String(), metadata.NoCompactMarkFilename) 381 noCompactMarkExists, err := bkt.Exists(ctx, m) 382 if err != nil { 383 return errors.Wrapf(err, "check exists %s in bucket", m) 384 } 385 if noCompactMarkExists { 386 level.Warn(logger).Log("msg", "requested to mark for no compaction, but file already exists; this should not happen; investigate", "err", errors.Errorf("file %s already exists in bucket", m)) 387 return nil 388 } 389 390 noCompactMark, err := json.Marshal(metadata.NoCompactMark{ 391 ID: id, 392 Version: metadata.NoCompactMarkVersion1, 393 394 NoCompactTime: time.Now().Unix(), 395 Reason: reason, 396 Details: details, 397 }) 398 if err != nil { 399 return errors.Wrap(err, "json encode no compact mark") 400 } 401 402 if err := bkt.Upload(ctx, m, bytes.NewBuffer(noCompactMark)); err != nil { 403 return errors.Wrapf(err, "upload file %s to bucket", m) 404 } 405 markedForNoCompact.Inc() 406 level.Info(logger).Log("msg", "block has been marked for no compaction", "block", id) 407 return nil 408 } 409 410 // MarkForNoDownsample creates a file which marks block to be not downsampled. 411 func MarkForNoDownsample(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID, reason metadata.NoDownsampleReason, details string, markedForNoDownsample prometheus.Counter) error { 412 m := path.Join(id.String(), metadata.NoDownsampleMarkFilename) 413 noDownsampleMarkExists, err := bkt.Exists(ctx, m) 414 if err != nil { 415 return errors.Wrapf(err, "check exists %s in bucket", m) 416 } 417 if noDownsampleMarkExists { 418 level.Warn(logger).Log("msg", "requested to mark for no deletion, but file already exists; this should not happen; investigate", "err", errors.Errorf("file %s already exists in bucket", m)) 419 return nil 420 } 421 noDownsampleMark, err := json.Marshal(metadata.NoDownsampleMark{ 422 ID: id, 423 Version: metadata.NoDownsampleMarkVersion1, 424 425 NoDownsampleTime: time.Now().Unix(), 426 Reason: reason, 427 Details: details, 428 }) 429 if err != nil { 430 return errors.Wrap(err, "json encode no downsample mark") 431 } 432 433 if err := bkt.Upload(ctx, m, bytes.NewBuffer(noDownsampleMark)); err != nil { 434 return errors.Wrapf(err, "upload file %s to bucket", m) 435 } 436 markedForNoDownsample.Inc() 437 level.Info(logger).Log("msg", "block has been marked for no downsample", "block", id) 438 return nil 439 } 440 441 // RemoveMark removes the file which marked the block for deletion, no-downsample or no-compact. 442 func RemoveMark(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID, removeMark prometheus.Counter, markedFilename string) error { 443 markedFile := path.Join(id.String(), markedFilename) 444 markedFileExists, err := bkt.Exists(ctx, markedFile) 445 if err != nil { 446 return errors.Wrapf(err, "check if %s file exists in bucket", markedFile) 447 } 448 if !markedFileExists { 449 level.Warn(logger).Log("msg", "requested to remove the mark, but file does not exist", "err", errors.Errorf("file %s does not exist in bucket", markedFile)) 450 return nil 451 } 452 if err := bkt.Delete(ctx, markedFile); err != nil { 453 return errors.Wrapf(err, "delete file %s from bucket", markedFile) 454 } 455 removeMark.Inc() 456 level.Info(logger).Log("msg", "mark has been removed from the block", "block", id) 457 return nil 458 }