Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make SafeDelete faster for large blocks; TSDB block repair case #1056

Merged
merged 29 commits into from
Jul 8, 2019
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f0dfe27
bucket verify: repair out of order labels
jjneely Mar 22, 2019
f09d3f5
verify repair: correctly order series in the index on rewrite
jjneely Mar 25, 2019
230bc7b
Fix the TSDB block safe-delete function
jjneely Mar 28, 2019
3f0d6cd
verify repair: fix duplicate chunk detection
jjneely Apr 1, 2019
ed78bde
PR feedback: use errors.Errorf() instead of fmt.Errorf()
jjneely Apr 12, 2019
7fcf793
Allow safe-delete to use previously downloaded TSDB block
jjneely Apr 15, 2019
2f179ff
Use errors.New()
jjneely Apr 15, 2019
9ca0c05
Merge branch 'jjneely/fix-repair-for-out-of-order-labels' into jjneel…
jjneely Apr 15, 2019
fd572aa
Repair duplicate series in a TSDB block
jjneely Apr 15, 2019
24173d3
Liberally comment this for loop
jjneely Apr 15, 2019
0d39253
Take advantage of sort.Interface
jjneely Apr 17, 2019
5a7fd5d
Merge branch 'jjneely/fix-repair-for-out-of-order-labels' into jjneel…
jjneely Apr 17, 2019
bfd44f1
PR Feedback: Comments are full sentences.
jjneely Apr 18, 2019
d1274ef
Merge branch 'jjneely/fix-repair-for-out-of-order-labels' into jjneel…
jjneely Apr 23, 2019
0a01898
Merge branch 'jjneely/safe-delete' into jjneely/safe-delete-pr
jjneely Apr 23, 2019
bf62c5c
PR Feedback
jjneely Apr 29, 2019
e296f8f
PR Feedback: Handle other possible Stat() errors
jjneely May 13, 2019
768ec93
PR Feedback: Add TODO for future metric of dropped series in block
jjneely May 13, 2019
c46ff1c
PR feedback: Make SafeDelete() more explicit
jjneely May 13, 2019
b625a95
PR Feedback: refactor SafeDelete
jjneely May 17, 2019
380672b
Use lset.String() rather than fmt.Sprintf()
jjneely Jun 14, 2019
5599139
SafeDelete(): Make comments match function behavior
jjneely Jun 14, 2019
e6ad6a0
PR feedback: comments in pkg/verifier/safe_delete.go
jjneely Jun 25, 2019
649df4d
PR feedback: comments in pkg/verifier/safe_delete.go
jjneely Jun 25, 2019
9e26cfd
PR Feedback: comments in pkg/block/index.go
jjneely Jun 25, 2019
31f7dd6
Update pkg/verifier/safe_delete.go
jjneely Jun 25, 2019
f6f3443
PR feedback: split SafeDelete() into multiple functions
jjneely Jun 27, 2019
37d495a
PR Feedback: Log the upstream PR with this TODO
jjneely Jun 27, 2019
b852c34
PR Feedback: clean up function signatures
jjneely Jul 2, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion pkg/block/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,12 +495,16 @@ func Repair(logger log.Logger, dir string, id ulid.ULID, source metadata.SourceT
resmeta.Stats = tsdb.BlockStats{} // reset stats
resmeta.Thanos.Source = source // update source

if err := rewrite(indexr, chunkr, indexw, chunkw, &resmeta, ignoreChkFns); err != nil {
if err := rewrite(logger, indexr, chunkr, indexw, chunkw, &resmeta, ignoreChkFns); err != nil {
return resid, errors.Wrap(err, "rewrite block")
}
if err := metadata.Write(logger, resdir, &resmeta); err != nil {
return resid, err
}
// TSDB may rewrite metadata in bdir
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TSDB code handily rewrites the block metadata even though we don't write or change read block here. This nukes the extra metadata Thanos uses. Write it back.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is also a bug that you are trying to fix? Perhaps we should add it to the changelog and/or the PR messsage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bug, this is part of the initial patches to be able to reuse downloaded blocks. The TSDB library isn't aware of the additional meta data Thanos provides and was removing it. Therefore, when a previously downloaded block is reused it would have incomplete metadata without this change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is being changed now: prometheus-junkyard/tsdb#637 so this won't be needed TBH (once we use latest TSDB).

Also this is true what @GiedriusS said - splitting PRs into focused changes helps a lot. However this change relies on this fix. I would be more explicit in description. This will help us to understand this early on (:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add TODO to update TSDB and remove this as it will be not needed in newest TSDB. For now we need this, thanks for fixing this 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Todo added, thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also missing trailing period in comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if err := metadata.Write(logger, bdir, meta); err != nil {
return resid, err
}
return resid, nil
}

Expand Down Expand Up @@ -595,6 +599,7 @@ type seriesRepair struct {
// rewrite writes all data from the readers back into the writers while cleaning
// up mis-ordered and duplicated chunks.
func rewrite(
logger log.Logger,
indexr tsdb.IndexReader, chunkr tsdb.ChunkReader,
indexw tsdb.IndexWriter, chunkw tsdb.ChunkWriter,
meta *metadata.Meta,
Expand Down Expand Up @@ -665,8 +670,16 @@ func rewrite(
return labels.Compare(series[i].lset, series[j].lset) < 0
})

lastSet := labels.Labels{}
// Build a new TSDB block.
for _, s := range series {
if labels.Compare(lastSet, s.lset) == 0 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The series are sorted based on their labelSet. What happens if two labelSets are identical? For one they will be consecutive in the slice. So if the labelSet we have now is the same as the previous labelSet we have a duplicate series which the TSDB code will produce an error on. So we just drop that series.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a comment on the implied behavior here. I believe you when you say you've seen this behavior, but it worries me, as something went terribly wrong wherever these blocks were produced. We should at least document that the heuristic here is that on collision we keep the first series found and drop consecutive ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thank you!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry one more thing. As this shouldn't generally happen, let's add a metric so we can monitor when it does happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repair process batch job only which makes metrics less useful. I had some similar comments in #964

Perhaps I should leave a comment if that were to change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes let's in that case put a TODO in there

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO added.

level.Warn(logger).Log("msg",
"dropping duplicate series in tsdb block found",
"labelset", fmt.Sprintf("%s", s.lset),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use s.lset.String() here. The new meta linter will complain about this 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

)
continue
}
if err := chunkw.WriteChunks(s.chks...); err != nil {
return errors.Wrap(err, "write chunks")
}
Expand All @@ -691,6 +704,7 @@ func rewrite(
}
postings.Add(i, s.lset)
i++
lastSet = s.lset
}

s := make([]string, 0, 256)
Expand Down
2 changes: 1 addition & 1 deletion pkg/verifier/duplicated_compaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func DuplicatedCompactionIssue(ctx context.Context, logger log.Logger, bkt objst
}

for i, id := range toKill {
if err := SafeDelete(ctx, logger, bkt, backupBkt, id); err != nil {
if err := SafeDelete(ctx, logger, bkt, backupBkt, id, ""); err != nil {
return err
}
level.Info(logger).Log("msg", "Removed duplicated block", "id", id, "to-be-removed", len(toKill)-(i+1), "removed", i+1, "issue", DuplicatedCompactionIssueID)
Expand Down
2 changes: 1 addition & 1 deletion pkg/verifier/index_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func IndexIssue(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bac
}

level.Info(logger).Log("msg", "safe deleting broken block", "id", id, "issue", IndexIssueID)
if err := SafeDelete(ctx, logger, bkt, backupBkt, id); err != nil {
if err := SafeDelete(ctx, logger, bkt, backupBkt, id, tmpdir); err != nil {
return errors.Wrapf(err, "safe deleting old block %s failed", id)
}
level.Info(logger).Log("msg", "all good, continuing", "id", id, "issue", IndexIssueID)
Expand Down
48 changes: 31 additions & 17 deletions pkg/verifier/safe_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package verifier

import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
Expand All @@ -14,10 +15,13 @@ import (
"github.com/pkg/errors"
)

// SafeDelete moves block to backup bucket and if succeeded, removes it from source bucket.
// It returns error if block dir already exists in backup bucket (blocks should be immutable) or any
// of the operation fails.
func SafeDelete(ctx context.Context, logger log.Logger, bkt objstore.Bucket, backupBkt objstore.Bucket, id ulid.ULID) error {
// SafeDelete moves block to backup bucket and if succeeded, removes it from
// source bucket. It returns error if block dir already exists in backup
// bucket (blocks should be immutable) or any of the operation fails. If
// the block has already been downloaded it must exist in tempdir, else it
// will be downloaded to be copied to the backup bucket. tempdir set to the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer not to hide this behavior in tempdir values. Maybe "safe delete modes" (as in "force download", "try local copy") or a bool (as in just "forceDownload") at the very least that makes it very explicit what's going to happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SafeDelete() doesn't know the temporary directory where a previous function may have downloaded the block, so we need to pass in a path somehow. Perhaps rename this argument something like useExisting? Which would mean we use the existing block if found under that path, otherwise a new temporary directory is created and the block is downloaded.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I meant let's pass two parameters to explicitly describe the intention

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIP

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, committed a patch for makes this explicit. I've tested the repair process with my collection of broken blocks I've started keeping and had the behavior expected. (No repeated downloads.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had thanos-compact cut a 450GiB TSDB block today which I referenced in #1152 which is part of the motivation for this.

// zero value forces download.
func SafeDelete(ctx context.Context, logger log.Logger, bkt objstore.Bucket, backupBkt objstore.Bucket, id ulid.ULID, tempdir string) error {
foundDir := false
err := backupBkt.Iter(ctx, id.String(), func(name string) error {
foundDir = true
Expand All @@ -31,23 +35,33 @@ func SafeDelete(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bac
return errors.Errorf("%s dir seems to exists in backup bucket. Remove this block manually if you are sure it is safe to do", id)
}

tempdir, err := ioutil.TempDir("", "safe-delete")
if err != nil {
return err
if tempdir == "" {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we already have the block, don't download it again. Blocks can be large. 30GiB or close to it in my environment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I wasn't clear, I would expect that when useExisting is set, that tempDir must be set, so I would expect an error from this function if that is not the case, and this if statement to be combined with the parts below, I think that'll also make the code a lot more easy to understand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, refactored this a bit. It will return an error if you ask it to use an existing tempdir and do not provide one, and the logic is a little simpler.

// tempdir unspecified, create one
tempdir, err = ioutil.TempDir("", fmt.Sprintf("safe-delete-%s", id.String()))
if err != nil {
return err
}
}
dir := filepath.Join(tempdir, id.String())
err = os.Mkdir(dir, 0755)
if err != nil {
return err
}
defer func() {
if err := os.RemoveAll(tempdir); err != nil {
level.Warn(logger).Log("msg", "failed to delete dir", "dir", tempdir, "err", err)

if _, err := os.Stat(dir); err != nil {
// TSDB block not already present, download it
err = os.Mkdir(dir, 0755)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should only be done if os.IsNotExist(err)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks!

if err != nil {
return err
}
}()
defer func() {
if err := os.RemoveAll(tempdir); err != nil {
level.Warn(logger).Log("msg", "failed to delete dir", "dir", tempdir, "err", err)
}
}()

if err := block.Download(ctx, logger, bkt, id, dir); err != nil {
return errors.Wrap(err, "download from source")
if err := block.Download(ctx, logger, bkt, id, dir); err != nil {
return errors.Wrap(err, "download from source")
}
} else {
level.Info(logger).Log("msg", "using previously downloaded tsdb block",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what kind of situation would we end up in this path? And if the answer is a (half or maybe even fully) finished previous download, then how do we prevent that the directory is there but the content is not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My blocks (and I'm sure others) easily grow above 30GiB when compacted. So if I need to repair block A, Thanos will download block A, create a repaired block B, upload block B. So at this point, we've created, successfully, a new block from A and that block B now passes the VerifyIndex() test.

At this point we call SafeDelete() which downloads block A, uploads block A to the backup bucket, then deletes block A from the main bucket.

This process can be time consuming (and perhaps expensive). So if blocks are immutable, and the repair process verifies that A is a legitimate block (if malformed) we should be able to skip the redundant download of A and backup the existing downloaded copy.

"id", id.String())
}

if err := block.Upload(ctx, logger, backupBkt, dir); err != nil {
Expand Down