Skip to content

net/http: request body transfer optimization issues (ReadFrom/sendfile) #30377

Closed
@vancluever

Description

@vancluever

What version of Go are you using (go version)?

$ go version
go version go1.11.5 linux/amd64

Does this issue reproduce with the latest release?

Yes (1.11.5), haven't tried the 1.12 RC, but the implementations of persistConnWriter and transferWriter.writeBody do not appear to have changed, which is where the issue lies.

What operating system and processor architecture are you using (go env)?

$ go env GOOS GOARCH
linux
amd64

What did you do?

Upload a file using PUT in a http.Request, by setting the file as Body:

fh, err := os.Open(fileName)
// ....
req, err := http.NewRequest("PUT", destURL, fh)

(Not that our implementations at HashiCorp usually wrap in go-retryablehttp, which is interface-compatible with the above. This is what the benchmarks below use. The above example is to illustrate the repro using the stdlib.)

This test utility was tested on live systems sending uploads to live services.

What did you expect to see?

An upload speed matching something more representative of the speed of the links the systems were connected to. fasthttp, which was also tested, gave upload speeds of about 50-70 MB/sec on live systems depending on the location of the client machine.

What did you see instead?

Upload speeds ranging from about 8MB/sec (lower latency systems) to as low as sub-1MB/sec (higher-latency systems).

Additional info/root cause

Investigation into the internals of the net/http transport and its behavior compared to fasthttp revealed that the path fasthttp currently takes allowed it to use sendfile to transfer the data instead of write calls. This was discovered by using various tracing tools (strace/bpftrace) during testing.

Further investigation into the net/http transport revealed that its writer is currently wrapped in persistConnWriter which does not implement io.ReaderFrom, which is what is necessary for an io.Copy to the transport's connection (ie: TCPConn) to ultimately fast-path to sendfile. See here. Further to that, the request body is wrapped in a transferBodyReader, and possibly an ioutil.nopCloser, obfuscating the reader to the point where TCPConn cannot discern properly whether or not the underlying reader is an *os.File.

Adding a trivial implementation for ReadFrom in persistConnWriter allows for the pathing to sendfile, in addition to significantly speeding up uploads when the reader is not an *os.File, or if the OS does not have a sendfile implementation (example: darwin), due to the use of genericReadFrom. Replacing transferBodyReader with a non-writer implementation - and providing additional unwrapping within writeBody to unwrap the underlying reader from the nopCloser if the wrapping exists - ensures that the proper reader makes its way to TCPConn.sendFile.

Example on a local machine using a simple net/http server reading the body into an ioutil.Discard, uploading a 1GB file of random data:

$ ./simpleput-write garbage.dat http://127.0.0.1:8080/
2019/02/24 09:39:16 [DEBUG] PUT http://127.0.0.1:8080/
2019/02/24 09:39:18 upload complete; duration: 1.564957672s; size: 1048576000 bytes; rate: 638.994918 MB/sec

$ ./simpleput-sendfile garbage.dat http://127.0.0.1:8080/
2019/02/24 09:39:39 [DEBUG] PUT http://127.0.0.1:8080/
2019/02/24 09:39:39 upload complete; duration: 244.542093ms; size: 1048576000 bytes; rate: 4089.275542 MB/sec

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performance

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions