Skip to content

Stream object downloads instead of buffering objects in memory - #829

Merged
fsouza merged 13 commits into
fsouza:mainfrom
dnerdy:streaming-downloads
Sep 2, 2022
Merged

Stream object downloads instead of buffering objects in memory#829
fsouza merged 13 commits into
fsouza:mainfrom
dnerdy:streaming-downloads

Conversation

@dnerdy

@dnerdy dnerdy commented Jun 15, 2022

Copy link
Copy Markdown
Contributor

Fixes #828.

This PR makes it so objects in the FS backend are streamed from disk
instead of being loaded into memory. This drastically reduces memory
usage when working with large files, especially when performing small
concurrent range reads on a large file.

A StreamingObject type has been added to both the fakestorage and
backend packages. This type contains a io.ReadSeekCloser that can be
used to access object data.

This change sets the stage for fixing #669 and #397. #669 isn't fixed
because it will require re-working of how resumable uploads are stored
(though other upload types have been made streaming in this PR). Also,
fixing #397 would require changing how initial objects are specified
when starting the server, which with this approach would require
changing the public API.

Regarding the public API: the server methods for working with objects
have been changed to use StreamingObject instead of Object. This is a
breaking change. It would be possible to make streaming backward
compatible by adding some metadata and methods to Object (to distinguish
between streaming and buffered objects), but I went with this approach
because it made identifying the code that needed to be updated easier;
the compiler pointed out the locations to update instead of needing to
track down runtime and test errors. An additional improvement, which is
compatible with the backward compatible approach, would be to add an
OpenForReading method on Object so callers that are currently
responsible for closing objects also explicitly open objects (which
would make the responsibility clearer). Anyway, I wanted to get this
initial approach up for feedback before putting in additional work.

Fixes fsouza#828.

This PR makes it so objects in the FS backed are streamed from disk
instead of being loaded into memory. This drastically reduces memory
usage when working with large files, especially when performing small
concurrent range reads on a large file.

A StreamingObject type has been added to both the fakestorage and API
packages. This type contains a io.ReadSeekCloser that can be used to
access object data.

This change sets the stage for fixing fsouza#669 and fsouza#397. fsouza#669 isn't fixed
because it will require re-working of how resumable uploads are stored
(though other upload types have been made streaming in this PR). Also,
fixing fsouza#397 would require changing how initial objects are specified
when starting the server, which with this approach would require
changing the public API.

Regarding the public API: the server methods for working with objects
have been changed to use StreamingObject instead of Object. This is a
breaking change. It would be possible to make streaming backward
compatible by adding some metadata and methods to Object (to distinguish
between streaming and buffered objects), but I went with this approach
because it made identifying the code that needed to be updated easier;
the compiler pointed out the locations to update instead of needing to
track down runtime and test errors. An additional improvement, which is
compatible with the backward compatible approach, would be to add an
OpenForReading method on Object so callers that are currently
responsible for closing objects also explicitly open objects (which
would make the responsibility clearer). Anyway, I wanted to get this
initial approach up for feedback before putting in additional work.
@dnerdy dnerdy changed the title Stream object downloads instead of buffering object in memory Jun 15, 2022
@dnerdy
dnerdy force-pushed the streaming-downloads branch from f650f66 to 3d78ac9 Compare June 16, 2022 03:23
@dnerdy
dnerdy force-pushed the streaming-downloads branch from 3da2a5c to b15c8a5 Compare June 16, 2022 03:49
@StevenACoffman

Copy link
Copy Markdown
Contributor

@fsouza what do you think of this? It has made a very large performance improvement in our internal testing.

@dnerdy

dnerdy commented Jun 21, 2022

Copy link
Copy Markdown
Contributor Author

Our engineers have been using this change in our dev environment for about a week (probably millions of requests). Folks use Linux and macOS. It would be helpful if someone in the community could also test the change on Windows. Windows has very different file open, rename and delete semantics. I've used github.com/alexbrainman/goissue34681 (found via golang/go#34681), though I'm not sure if this works well with multiple readers.

The strategy taken in this code is as follows:

  • Obtain lock when accessing an object
  • Open backing file
  • Unlock
  • If later the object is updated or deleted, the backing file is replaced or deleted (under a different lock)
  • Open readers (which no longer hold a lock) continue to read from the original open backing file

This allows a slow reader (or multiple readers) to download the original file even if the backing file has been modified or deleted.

@fsouza

fsouza commented Jun 24, 2022

Copy link
Copy Markdown
Owner

@fsouza what do you think of this? It has made a very large performance improvement in our internal testing.

On a quick glance, this looks sane. I'm a bit slow to respond right now, but I'll try to come back to it later today or some time tomorrow.

I also don't have access to any Windows hosts, so if we don't get any volunteers the best strategy will probably be to merge the PR and wait for bug reports 🙈

@fsouza fsouza left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Looks good overall, just want to make sure we don't ship a breaking change in the lib API.

Comment thread fakestorage/object.go
Comment thread fakestorage/object.go
Size int64 `json:"size,string"`
ContentType string `json:"contentType"`
ContentEncoding string `json:"contentEncoding"`
Content []byte `json:"-"`

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

lol didn't even realize we did this

Comment thread fakestorage/upload.go Outdated
@dnerdy

dnerdy commented Aug 9, 2022

Copy link
Copy Markdown
Contributor Author

@fsouza are you still interested in merging this PR? I'm curious what your opinion is about other breaking changes in this PR. You mentioned CreateObject, and @StevenACoffman changed the name back in #882, though the function signature is still different: it takes a StreamingObject. Some of the other functions also have different signatures. Are you okay with any breaking changes, or do you want the exported functions to stay backward compatible (which would mean creating new streaming variants)?

@fsouza

fsouza commented Aug 11, 2022

Copy link
Copy Markdown
Owner

@fsouza are you still interested in merging this PR? I'm curious what your opinion is about other breaking changes in this PR. You mentioned CreateObject, and @StevenACoffman changed the name back in #882, though the function signature is still different: it takes a StreamingObject. Some of the other functions also have different signatures. Are you okay with any breaking changes, or do you want the exported functions to stay backward compatible (which would mean creating new streaming variants)?

Hey, apologies for the silence, I was on a long break, but coming back now! Yeah, I wanted to keep it fully backwards compatible. We could keep the streaming variant private unless yall are already using fake-gcs-server as a library for this. Like, keep it private unless someone actually needs it? Not sure what you think.

@dnerdy

dnerdy commented Aug 12, 2022

Copy link
Copy Markdown
Contributor Author

We could keep the streaming variant private unless yall are already using fake-gcs-server as a library for this. Like, keep it private unless someone actually needs it? Not sure what you think.

No worries! And apologies for taking a while to circle back to this myself. Yeah, that seems reasonable. I'll work on reverting the existing APIs and I'll double check our usage to see if anything new needs to be exported (I suspect not).

@fsouza

fsouza commented Aug 14, 2022

Copy link
Copy Markdown
Owner

@dnerdy I'm curious on how you and @StevenACoffman would like to proceed. Should we close #882 and continue working on this PR?

@dnerdy

dnerdy commented Sep 2, 2022

Copy link
Copy Markdown
Contributor Author

@fsouza The original buffered APIs have been restored:

  • CreateObject
  • GetObject
  • GetObjectWithGeneration

The new streaming variants are named <X>Streaming, i.e.

  • CreateObjectStreaming
  • GetObjectStreaming
  • GetObjectWithGenerationStreaming

Internally, the server uses streaming objects and the objects are converted to buffered objects in the restored methods.

I've moved the documentation from the buffered variants to the streaming variants, to encourage their use, and updated the documentation for the buffered methods to point to the streaming variants.

One additional change I made to CreateObjectStreaming is that it returns an error instead of panicking when an error occurs.

I've also merged in main and updated the code to use the new metadata interface.

@fsouza fsouza left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This looks good in general, I have some minor comments left. Thank you very much for pushing this through!

Comment thread internal/backend/fs.go

if err = os.WriteFile(path, obj.Content, 0o600); err != nil {
return Object{}, err
tempFile, err := os.CreateTemp("", "fake-gcs-object")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

If we want to guarantee that rename will work, we'll likely want to specify the rename, otherwise the temporary directory may be in a different mount point. No need to overthink it though, we could address it later if someone raises the issue :)

Comment thread internal/backend/fs.go Outdated
@@ -0,0 +1,21 @@
//go:build !windows
// +build !windows

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Hmm now that we only need to support Go 1.18 and 1.19, we can get rid of +build (go fix should do it automatically I think).

"path/filepath"
"time"

"github.com/alexbrainman/goissue34681"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This feels wrong lol I wonder if the author will keep that around. I guess that in the worst case it'll be cached in the goproxy and we can fork it.

Applying it myself so we can ship this. I'll run `go fix` after merging.

Co-authored-by: Steve Coffman <StevenACoffman@users.noreply.github.com>

@fsouza fsouza left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thank you very much for contributing!

@fsouza
fsouza merged commit 12a03cf into fsouza:main Sep 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants