-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
s3: improve walk performance #2455
Conversation
Please sign your commits following these rules: $ git clone -b "make-walk-faster" [email protected]:sargun/distribution.git somewhere
$ cd somewhere
$ git rebase -i HEAD~842357011424
editor opens
change each 'pick' to 'edit'
save the file and quit
$ git commit --amend -s --no-edit
$ git rebase --continue # and repeat the amend for each commit
$ git push -f Amending updates the existing PR. You DO NOT need to open a new one. |
89eefd5
to
e0557fd
Compare
Codecov Report
@@ Coverage Diff @@
## master #2455 +/- ##
===========================================
- Coverage 61.4% 51.15% -10.25%
===========================================
Files 128 129 +1
Lines 11727 11818 +91
===========================================
- Hits 7201 6046 -1155
- Misses 3628 5011 +1383
+ Partials 898 761 -137
Continue to review full report at Codecov.
|
I have no idea what this codecov error is, any ideas? |
@sargun Don't worry about it. codecov is just noise. Could you describe the design and the approach, including how this works and what caveats it may introduce? |
The root problem is that the Walk function is implemented in a manner that assumes that metadata ops are free on the filesystem driver that you're using. Unfortunately, this is not true. This changes it so that it is the responsibility of the backend to implement a Walk Function. This walk function is a little bit different than the walk function that previously existed, as in your can return an ErrSkipDir in the middle to stop walking this directory. This adds an implementation of a generic version of this new walk function, and it adds an S3-specific implementation. Since there are only a few code paths that exercise this code, I'm not overly worried about negative impact. I think that the correctness of the algorithm is the primary concern. |
@stevvooe Any opinions? |
Bump. |
@sargun That sounds great!
I would prefer that we preserve this capability. It is the same thing supported in the standard library with |
@stevvooe The "new" walk function has the ErrSkipDir to stop going through that directory, whereas the old code didn't have that. The old one could only do ErrSkipDir when initially entering the directory, and not while mid-way through a directory. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a first pass at this, but its seems that the new WalkFallback
function isn't necessary as the original already provided the ability to stop traversal with a non ErrSkipDir
error. It's up to the caller of Walk
to decide how to handle the error.
registry/storage/blobstore.go
Outdated
@@ -109,11 +110,16 @@ func (bs *blobStore) Enumerate(ctx context.Context, ingester func(dgst digest.Di | |||
|
|||
digest, err := digestFromPath(currentPath) | |||
if err != nil { | |||
retErr = err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this necessary? It seems that the return value of WalkFn
eventually gets returned from Walk
unless its ErrSkipDir
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
@@ -40,22 +41,20 @@ func (reg *registry) Repositories(ctx context.Context, repos []string, last stri | |||
|
|||
// if we've filled our array, no need to walk any further | |||
if len(foundRepos) == len(repos) { | |||
return errFinishedWalk |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the previous Walk
function already had the ability to stop walking early by providing a non ErrSkipDir
error. I think its a little confusing to overload the use of ErrSkipDir
to stop the traversal in the middle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the way that the one from the Golang stdlib works: https://golang.org/pkg/path/filepath/#Walk
--
I think it's somewhat preferential to move in this direction, because we could eventually get rid of our custom walk function users and just use stdlib for filesystems, and such.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also means that people don't have to do mid-directory interruptions "by hand"
registry/storage/driver/s3-aws/s3.go
Outdated
var objectCount int64 | ||
|
||
path := from | ||
if path != "/" && path[len(path)-1] != '/' { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just path[len(path)-1] != '/'
is sufficient.
registry/storage/driver/s3-aws/s3.go
Outdated
// Walk traverses a filesystem defined within driver, starting | ||
// from the given path, calling f on each file | ||
func (d *driver) Walk(ctx context.Context, from string, f storagedriver.WalkFn) error { | ||
var objectCount int64 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps its better to declare this closer to where it's used, i.e. near the invocation of doWalk
.
registry/storage/driver/s3-aws/s3.go
Outdated
return err | ||
} | ||
|
||
if objectCount == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you leave a comment why this errors? Does this mean it'll error on an empty directory?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea of an "empty" directory doesn't exist on EC2, see:
(titusagent) ~ $ aws s3 ls s3://bucket/titan/devvpc7/
PRE logs/
PRE registry/
PRE t/
(titusagent) ~ $ aws s3 cp foo s3://bucket/titan/devvpc7/sargun/
upload: ./foo to s3://bucket/titan/devvpc7/sargun/foo
(titusagent) ~ $ aws s3 ls s3://bucket/titan/devvpc7/
PRE logs/
PRE registry/
PRE sargun/
PRE t/
(titusagent) ~ $ aws s3 rm s3://bucket/titan/devvpc7/sargun/foo
delete: s3://bucket/titan/devvpc7/sargun/foo
(titusagent) ~ $ aws s3 ls s3://bucket/titan/devvpc7/
PRE logs/
PRE registry/
PRE t/
registry/storage/driver/s3-aws/s3.go
Outdated
func (d *driver) doWalk(parentCtx context.Context, objectCount *int64, path, prefix string, f storagedriver.WalkFn) error { | ||
var retError error | ||
|
||
// fmt.Printf("Called with path=%s prefix=%s", path, prefix) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leftover debugging statement.
registry/storage/driver/s3-aws/s3.go
Outdated
*objectCount += *objects.KeyCount | ||
walkInfos := make([]walkInfoContainer, *objects.KeyCount) | ||
i := 0 | ||
for _, dir := range objects.CommonPrefixes { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for i, dir := range objects.CommonPrefixes
will give you the index you're accessing.
registry/storage/driver/s3-aws/s3.go
Outdated
i++ | ||
} | ||
|
||
for _, file := range objects.Contents { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for i, file := range objects.Contents
will give you the index you're accessing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, because the range is len(objects.CommonPrefixes) + i. I refactored this to use append.
registry/storage/driver/s3-aws/s3.go
Outdated
prefix: dir.Prefix, | ||
FileInfoFields: storagedriver.FileInfoFields{ | ||
IsDir: true, | ||
Path: strings.Replace(commonPrefix[0:len(commonPrefix)-1], d.s3Path(""), prefix, 1), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For array slices, the 0 is implicit so you can simply do: commonPrefix[:len(commonPrefix)-1]
. Also note the end index is exclusive, is this intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this path shouldn't have the trailing '/' from my understanding.
34d4fd8
to
b22b62c
Compare
|
This upgrade, and vendors aws-sdk-go to version v1.12.36. This is because it has new API calls accessible to the S3 client, specifically S3.ListObjectsV2PagesWithContext Signed-off-by: Sargun Dhillon <[email protected]>
Move the Walk types into registry/storage/driver, and add a Walk method to each storage driver. Although this is yet another API to implement, there is a fall back implementation that relies on List and Stat. For some filesystems this is very slow. Also, this WalkDir Method conforms better do a traditional WalkDir (a la filepath). This change is in preparation for refactoring. Signed-off-by: Sargun Dhillon <[email protected]>
This changes the Walk Method used for catalog enumeration. Just to show how much an effect this has on our s3 storage: Original: List calls: 6839 real 3m16.636s user 0m0.000s sys 0m0.016s New: ListObjectsV2 Calls: 1805 real 0m49.970s user 0m0.008s sys 0m0.000s This is because it no longer performs a list and stat per item, and instead is able to use the metadata gained from the list as a replacement to stat. Signed-off-by: Sargun Dhillon <[email protected]>
b22b62c
to
b2b1560
Compare
@hinshun PTAL. |
registry/storage/linkedblobstore.go
Outdated
return err | ||
} | ||
|
||
return nil | ||
}) | ||
|
||
if err != nil { | ||
return err | ||
if retErr != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the case where retErr != err
in this scope
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If Walk is returning a different error than what retErr
is set to, can you add a comment explaining that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we should make it so the walk functions never transmute err? Is that a reasonable expectation from Driver implementations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a comment.
LGTM @sargun Do you have a way to run the unit tests against s3? |
@@ -40,22 +41,20 @@ func (reg *registry) Repositories(ctx context.Context, repos []string, last stri | |||
|
|||
// if we've filled our array, no need to walk any further | |||
if len(foundRepos) == len(repos) { | |||
return errFinishedWalk | |||
finishedWalk = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also remove the errFinishedWalk
variable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
b879326
to
37b1e8d
Compare
@stevvooe Yeah, I did. And also, I've run this against our registry for a bit. |
registry/storage/linkedblobstore.go
Outdated
return err | ||
} | ||
|
||
return nil | ||
}) | ||
|
||
if err != nil { | ||
return err | ||
// We check retErr first, in case the Walk function somehow mutates the error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the error is mutated, wouldn't we still want the mutated error? I guess I am just trying to figure out if there is a good reason not to just return lbs.driver.Walk(....
// If the returned error from the WalkFn is ErrSkipDir and fileInfo refers | ||
// to a directory, the directory will not be entered and Walk | ||
// will continue the traversal. If fileInfo refers to a normal file, processing stops | ||
func WalkFallback(ctx context.Context, driver StorageDriver, from string, f WalkFn) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see why this and the implementation from registry/storage/walk.go
must both exist. Can we kill one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
This change is primarily to make GC faster. Signed-off-by: Sargun Dhillon <[email protected]>
e885ec4
to
a78edb3
Compare
This removes the old global walk function, and changes all the code to use the per-driver walk functions. Signed-off-by: Sargun Dhillon <[email protected]>
a78edb3
to
cbcbcb0
Compare
LGTM I would still like someone more familiar with the s3 driver to take a look and give an LGTM. I am not a huge fan of overloading |
Maybe @BrianBland or @RichardScothern? |
We could dream Let me check to see if I can find anyone interested in taking a look, otherwise we have reached the LGTM threshold already 😄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this change looks good to me.
return wi.FileInfoFields.IsDir | ||
} | ||
|
||
func (d *driver) doWalk(parentCtx context.Context, objectCount *int64, path, prefix string, f storagedriver.WalkFn) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty nit-picky, but it's generally better practice to return the objectCount
and error
values instead of mutating a pointer argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking a look! :D.
I think there's just the awkwardness of using a direct variable that there's no way to keep it across callbacks from ListObjectsV2PagesWithContext, so you need some pointer mutation at a minimum.
No description provided.