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

Fix duplicate tree checks within restic check #2328

Merged
merged 11 commits into from
Jul 22, 2020

Conversation

MichaelEischer
Copy link
Member

@MichaelEischer MichaelEischer commented Jul 6, 2019

What is the purpose of this change? What does it change?

Running restic check for large repositories is currently really slow. I had one run for an 8 TB repository with roughly 40 million files that took approximately one week to complete (Using GOMAXPROCS=1 to avoid interference with other processes on the server)

This is partially caused by decoding for each snapshot, every referenced tree blobs. The integrity check later on checks a tree blob only a single time and ignores later repetitions. Decoding a JSON file is not particularly fast (and the necessary index lookups are even slower, see #2284 ).

This change ensures that tree blobs are only decoded once. As I had to modify the blobRefs map anyways, it is now also used to track whether a blob is listed in an index or not, thereby obsoleting the blob IDSet.

The checker now also traverses the tree blobs in depth-first order which better matches the traversal order of the backup code.

Was the change discussed in an issue or in the forum before?

The change is related but not identical to issue #2284 .

Checklist

  • I have read the Contribution Guidelines
  • I have added tests for all changes in this PR
  • No user visible changes - I have added documentation for the changes (in the manual)
  • There's a new file in changelog/unreleased/ that describes the changes for our users (template here)
  • I have run gofmt on the code in all commits
  • All commit messages are formatted in the same style as the other commits in the repo
  • I'm done, this Pull Request is ready for review

@codecov-io
Copy link

codecov-io commented Jul 6, 2019

Codecov Report

Merging #2328 into master will decrease coverage by 4.07%.
The diff coverage is 86.04%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2328      +/-   ##
==========================================
- Coverage   50.95%   46.88%   -4.08%     
==========================================
  Files         178      178              
  Lines       14548    14544       -4     
==========================================
- Hits         7413     6819     -594     
- Misses       6069     6715     +646     
+ Partials     1066     1010      -56
Impacted Files Coverage Δ
internal/repository/repository.go 57.59% <72.22%> (+1.36%) ⬆️
internal/checker/checker.go 66.37% <96%> (-0.22%) ⬇️
internal/backend/b2/b2.go 0% <0%> (-79.55%) ⬇️
internal/backend/swift/swift.go 0% <0%> (-78.83%) ⬇️
internal/backend/gs/gs.go 0% <0%> (-74%) ⬇️
internal/backend/azure/azure.go 0% <0%> (-69.46%) ⬇️
internal/backend/swift/config.go 34.69% <0%> (-57.15%) ⬇️
internal/repository/master_index.go 43.07% <0%> (-4.62%) ⬇️
internal/archiver/file_saver.go 85.96% <0%> (-2.64%) ⬇️
internal/repository/index.go 66.43% <0%> (-1.05%) ⬇️
... and 3 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1e9eefa...472d5b1. Read the comment docs.

@@ -498,15 +487,21 @@ func filterTrees(ctx context.Context, backlog restic.IDs, loaderChan chan<- rest
job treeJob
nextTreeID restic.ID
outstandingLoadTreeJobs = 0
processedTrees = make(map[restic.ID]bool)
Copy link
Member Author

Choose a reason for hiding this comment

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

The processedTrees map will increase the memory usage of the check command as it is stored in addition to blobrefs.M. It would be possible to add processedTrees to c.blobrefs and use it exclusively to store the tree ids. The main drawback of this would be that UnusedBlobs has to look into both maps.

@ad-m
Copy link

ad-m commented Dec 28, 2019

Have you collected any stats regarding performance impact?

@MichaelEischer
Copy link
Member Author

I've noted that I forgot to mention the speedup seen for the 40 million files test set: It went from one week down to approximately one day.

And now on to something more comparable:

Preface: Github seems to mess up the displayed commit ordering, the actual order is as follows (oldest to newest). The benchmark just copies an existing snapshot and adds it with a new set of tags. I've mixed the commit list with the observed changes, the measurement data is shown below.

5b94a9a8 Add benchmark for checker scaling with snapshot count
<-- baseline
ced4890f checker: Decode identical tree nodes only once
<-- nearly no more slowdown with additional snapshot copies
ca3f828a checker: Reduce cost of debug log
10e2aabf checker: Remove dead index map
bf27fbf3 checker: Traverse trees in depth-first order
189f44a8 checker: Remove duplicate sets used to track existing blobs
babac588 checker: Unify processed tree and referenced blobs map
<-- slightly faster and fewer allocations / memory usage
88776b5e repository: Don't sort one element pack lists
0003b64b repository: Remove size lookup when loading blob/tree
<-- slightly faster
> git rebase -i HEAD~9 -x 'git log --oneline -n 1 ; ../go/bin/go test -bench BenchmarkChecker -benchtime 500x -benchmem github.com/restic/restic/internal/checker'
Executing: git log --oneline -n 1 ; ../go/bin/go test -bench BenchmarkChecker -benchtime 500x -benchmem github.com/restic/restic/internal/checker
5b94a9a8 (HEAD) Add benchmark for checker scaling with snapshot count
goos: linux
goarch: amd64
pkg: github.com/restic/restic/internal/checker
BenchmarkChecker-8                      	     500	  37276318 ns/op	 4208978 B/op	    2348 allocs/op
BenchmarkCheckerSnapshotScaling_50-8    	     500	  40370646 ns/op	 5584485 B/op	   20994 allocs/op
BenchmarkCheckerSnapshotScaling_100-8   	     500	  43772751 ns/op	 6960990 B/op	   39647 allocs/op
BenchmarkCheckerSnapshotScaling_200-8   	     500	  51724338 ns/op	 9718810 B/op	   76952 allocs/op
PASS
ok  	github.com/restic/restic/internal/checker	94.344s

ced4890f (HEAD) checker: Decode identical tree nodes only once
BenchmarkChecker-8                      	     500	  36961886 ns/op	 4208199 B/op	    2296 allocs/op
BenchmarkCheckerSnapshotScaling_50-8    	     500	  37743232 ns/op	 4511634 B/op	    4890 allocs/op
BenchmarkCheckerSnapshotScaling_100-8   	     500	  39472519 ns/op	 4818885 B/op	    7490 allocs/op
BenchmarkCheckerSnapshotScaling_200-8   	     500	  39081652 ns/op	 5433777 B/op	   12694 allocs/op
PASS
ok  	github.com/restic/restic/internal/checker	84.363s

babac588 (HEAD) checker: Unify processed tree and referenced blobs map
BenchmarkChecker-8                      	     500	  36746626 ns/op	 4126244 B/op	    1089 allocs/op
BenchmarkCheckerSnapshotScaling_50-8    	     500	  36839477 ns/op	 4430195 B/op	    3695 allocs/op
BenchmarkCheckerSnapshotScaling_100-8   	     500	  37969459 ns/op	 4737592 B/op	    6298 allocs/op
BenchmarkCheckerSnapshotScaling_200-8   	     500	  39273562 ns/op	 5352415 B/op	   11500 allocs/op
PASS
ok  	github.com/restic/restic/internal/checker	83.186s

0003b64b (HEAD, origin/no-repeated-checks, no-repeated-checks) repository: Remove size lookup when loading blob/tree
BenchmarkChecker-8                      	     500	  36449458 ns/op	 4126226 B/op	    1089 allocs/op
BenchmarkCheckerSnapshotScaling_50-8    	     500	  36914002 ns/op	 4430199 B/op	    3695 allocs/op
BenchmarkCheckerSnapshotScaling_100-8   	     500	  36549722 ns/op	 4737589 B/op	    6298 allocs/op
BenchmarkCheckerSnapshotScaling_200-8   	     500	  37407083 ns/op	 5352403 B/op	   11500 allocs/op
PASS
ok  	github.com/restic/restic/internal/checker	81.400s

Thus from my point of view this leaves primarily the following commits without a demonstrable speed-up.

bf27fbf3 checker: Traverse trees in depth-first order
88776b5e repository: Don't sort one element pack lists

These commits improve data locality or avoid some unnecessary syscalls, respectively. As the accessed files come from the page cache there's not much that can be shown.

@MichaelEischer
Copy link
Member Author

MichaelEischer commented Mar 7, 2020

I've split off the repository related changes into separate PRs. Apart from the commits "checker: Remove duplicate sets used to track existing blobs" and "checker: Unify processed tree and referenced blobs map" which are now merged into a single commit, the PR is largely unchanged, that is the old performance measurements still apply.

The commit checker: Traverse trees in depth-first order is currently still part of this PR, but could be split off as it has a slightly different focus than the remainder of the PR.

The changelog also refers to issues #2284 as this PR fixes the check command specific performance problem and #2523 tackles the index performance.

@MichaelEischer MichaelEischer force-pushed the no-repeated-checks branch 2 times, most recently from 1a95be0 to 6410284 Compare March 7, 2020 15:10
@rawtaz
Copy link
Contributor

rawtaz commented Mar 7, 2020

Interesting work! Based on what you've seen in other PRs, do you think this PR makes changes that conflict with other PRs? Just in general/off the top of your head, I'm not suggesting you sift through them all.

@MichaelEischer
Copy link
Member Author

The only conflict I'm aware of is with #2523 which also removes some IDSets from the checker code. The solution is rather easy in that case: just use the code from this PR as it reduces the memory usage even a bit further.

@rawtaz rawtaz requested a review from fd0 March 17, 2020 01:48
@rawtaz
Copy link
Contributor

rawtaz commented Mar 17, 2020

I just tested this myself on a repo that's 101 GB on disk and contains 4437 snapshots.

Running restic check on it (without --read-data) with master (restic 0.9.6 (v0.9.6-104-g58bd1652) compiled with go1.13.5 on linux/amd64) took 145 minutes (real 145m7.542s, user 520m36.152s, sys 9m24.677s).

Running it with this PR (restic 0.9.6 (v0.9.6-146-g3f2e8b1b) compiled with go1.14 on linux/amd64) took just 7 minutes (real 7m3.783s, user 24m46.927s, sys 0m5.213s).

That's a huge improvement. I also noticed that restic was using around 395-399% CPU with this PR instead of 380% with master.

The system is a small Dell server with 7200 RPM disks on hardware RAID, but clearly that's not a relevant bottleneck in this case. Memory is 8 GB and the CPU has four cores.

The generated output from the two runs were identical in everything that matters (a bunch of unreferenced packs and one missing pack in this case).

@rawtaz
Copy link
Contributor

rawtaz commented Mar 17, 2020

Another run with a 90+ GB repo, took 4 minutes instead of 31. Same machine, 1767 snapshots.

@rawtaz
Copy link
Contributor

rawtaz commented Mar 24, 2020

@greatroar I cannot request a review from you using GitHub, but I'd like to ask if you could review this PR nonetheless?

@MichaelEischer MichaelEischer force-pushed the no-repeated-checks branch 2 times, most recently from 176a9da to f58b0e9 Compare April 18, 2020 19:56
@MichaelEischer MichaelEischer mentioned this pull request Jul 18, 2020
11 tasks
@@ -504,8 +493,21 @@ func filterTrees(ctx context.Context, backlog restic.IDs, loaderChan chan<- rest

for {
if loadCh == nil && len(backlog) > 0 {
// process last added ids first, that is traverse the tree in depth-first order
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not depth-first order. It's more like post-order, if I understand the surrouding code correctly.

Copy link
Member Author

Choose a reason for hiding this comment

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

If the checker only uses a single treeLoader then the traversal order is a pre-order depth-first search. This line dequeues a child subtree and sends it to the tree loader. Once the tree is loaded the subtrees are added to the backlog and the tree node is sent via the outCh to the error checking. The child nodes are then loaded next from the backlog end and so forth.

With multiple treeLoader it gets a bit more chaotic but the overall pattern still holds. The archiver code uses a post-order depth-first traversal so the traversal order in the checker is still not a perfect fit, but at least it resembles the archiving process much more closely than the breath-first search that was used previously.

Even though the checkTreeWorker skips already processed chunks,
filterTrees did queue the same tree blob on every occurence. This
becomes a serious performance bottleneck for larger number of snapshots
that cover mostly the same directories. Therefore decode a tree blob
exactly once.
Avoid duplicate allocation of the Subtree list.
The blobRefs map and the processedTrees IDSet are merged to reduce the
memory usage. The blobRefs map now uses separate flags to track blob
usage as data or tree blob. This prevents skipping of trees whose
content is identical to an already processed data blob. A third flag
tracks whether a blob exists or not, which removes the need for the
blobs IDSet.
Backups traverse the file tree in depth-first order and saves trees on
the way back up. This results in tree packs filled in a way comparable
to the reverse Polish notation.  In order to check tree blobs in that
order, the treeFilter would have to delay the forwarding of tree nodes
until all children of it are processed which would complicate the
implementation.

Therefore do the next similar thing and traverse the tree in depth-first
order, but process trees already on the way down. The tree blob ids are
added in reverse order to the backlog, which is once again reverted when
removing the ids from the back of the backlog.
This change only moves code around but does not result in any change in
behavior.
The `DuplicateTree` flag is necessary to ensure that failures cannot be
swallowed. The old checker implementation ignores errors from LoadTree
if the corresponding tree was already checked.
If a data blob and a tree blob with the same ID (= same content) exist,
then the checker did not report a data or tree blob as unused when the
blob of the other type was still in use.
Copy link
Member

@fd0 fd0 left a comment

Choose a reason for hiding this comment

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

LGTM!

@MichaelEischer MichaelEischer merged commit 34181b1 into restic:master Jul 22, 2020
@MichaelEischer MichaelEischer deleted the no-repeated-checks branch July 22, 2020 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants