-
Notifications
You must be signed in to change notification settings - Fork 4.6k
transport: Avoid buffer copies when reading Data frames #8657
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
base: master
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #8657 +/- ##
==========================================
+ Coverage 81.92% 81.96% +0.03%
==========================================
Files 416 417 +1
Lines 40789 40871 +82
==========================================
+ Hits 33418 33498 +80
- Misses 6000 6007 +7
+ Partials 1371 1366 -5
🚀 New features to boost your workflow:
|
b5f777e to
ae8c8df
Compare
efe661b to
778af0a
Compare
778af0a to
45e856f
Compare
45e856f to
5ec4a4e
Compare
internal/transport/http_util.go
Outdated
| offset int | ||
| batchSize int | ||
| conn net.Conn | ||
| conn io.ReadWriter |
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.
Why does this have to a io.ReadWriter? Can it simply be an io.Writer?
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.
Yes, io.Writer is more appropriate here. I wanted to replace the net.Conn with a simpler interface to fake it in tests. I used io.ReadWriter in the constructor of the grpc framer (newFramer) and propagated the same type to the called functions. Changed now.
| // DATA frame is received whose stream identifier | ||
| // field is 0x0, the recipient MUST respond with a | ||
| // connection error (Section 5.4.1) of type | ||
| // PROTOCOL_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.
How are we returning a PROTOCOL_ERROR in this case?
The code in our http2 client and server that calls readFrame() does a type assertion for http2.StreamError and only in that case closes the stream with a protocol error. But it checks to find the stream to close, and if the stream ID here is 0, it won't find a stream, and therefore will end up closing the transport.
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 should have been returning an http2.ConnectionError with code http2.ErrCodeProtocol, similar to the std lib framer. I fixed it to do the same now and updated the test.
The code in our http2 client and server that calls readFrame() does a type assertion for http2.StreamError and only in that case closes the stream with a protocol error. But
The std lib framer returns 2 kinds of errors:
- Connection Errors: These should result in the entire http2 connection being closed. These are the majority of errors.
- Stream Errors: These should result in only a single stream being closed, keeping the connection alive. Presently stream errors are only returned during parsing of window update and header frames.
Here, we need to return a connection error because stream ID 0 is special in HTTP/2. It is reserved for frames that apply to the entire connection rather than an individual request/response stream. Stream 0 is the "control channel" for the whole HTTP/2 connection. We can't close stream 0 while keeping the connection alive.
| } | ||
| if fh.Flags.Has(http2.FlagDataPadded) { | ||
| if fh.Length == 0 { | ||
| return io.ErrUnexpectedEOF |
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.
Does it make sense to wrap io.ErrUnexpectedEOF with some additional context like "padded bit is set, but frame length is 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.
I kept it as is for consistency with the std lib framer, which returns an unwrapped error here.
internal/transport/http_util_test.go
Outdated
| // Write the frame using the provided function. | ||
| writeErr := tt.w(fr) | ||
| if writeErr != nil { | ||
| t.Fatalf("tt.w() returned unexpected error: %v", writeErr) |
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.
Nit: tt.w() is not very readable when it shows up in the log of a failing test. Maybe something more descriptive here please.
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.
Renamed to writeFrames.
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 realised that the changes in the std lib framer are not imported in g3 os this PR can't be merged in gRPC yet. I've added a Blocked label to indicate this.
internal/transport/http_util_test.go
Outdated
| // Write the frame using the provided function. | ||
| writeErr := tt.w(fr) | ||
| if writeErr != nil { | ||
| t.Fatalf("tt.w() returned unexpected error: %v", writeErr) |
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.
Renamed to writeFrames.
| // DATA frame is received whose stream identifier | ||
| // field is 0x0, the recipient MUST respond with a | ||
| // connection error (Section 5.4.1) of type | ||
| // PROTOCOL_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.
It should have been returning an http2.ConnectionError with code http2.ErrCodeProtocol, similar to the std lib framer. I fixed it to do the same now and updated the test.
The code in our http2 client and server that calls readFrame() does a type assertion for http2.StreamError and only in that case closes the stream with a protocol error. But
The std lib framer returns 2 kinds of errors:
- Connection Errors: These should result in the entire http2 connection being closed. These are the majority of errors.
- Stream Errors: These should result in only a single stream being closed, keeping the connection alive. Presently stream errors are only returned during parsing of window update and header frames.
Here, we need to return a connection error because stream ID 0 is special in HTTP/2. It is reserved for frames that apply to the entire connection rather than an individual request/response stream. Stream 0 is the "control channel" for the whole HTTP/2 connection. We can't close stream 0 while keeping the connection alive.
| } | ||
| if fh.Flags.Has(http2.FlagDataPadded) { | ||
| if fh.Length == 0 { | ||
| return io.ErrUnexpectedEOF |
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 kept it as is for consistency with the std lib framer, which returns an unwrapped error here.
internal/transport/http_util.go
Outdated
| offset int | ||
| batchSize int | ||
| conn net.Conn | ||
| conn io.ReadWriter |
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.
Yes, io.Writer is more appropriate here. I wanted to replace the net.Conn with a simpler interface to fake it in tests. I used io.ReadWriter in the constructor of the grpc framer (newFramer) and propagated the same type to the called functions. Changed now.
This change incorporates changes from golang/go#73560 to split reading HTTP/2 frame headers and payloads. If the frame is not a Data frame, it's read through the standard library framer as before. For Data frames, the payload is read directly into a buffer from the buffer pool to avoid copying it from the framer's buffer.
Testing
For 1 MB payloads, this results in ~4% improvement in throughput.
For smaller payloads, the difference in minor.
go run benchmark/benchmain/main.go -benchtime=60s -workloads=streaming \ -compression=off -maxConcurrentCalls=120 -trace=off \ -reqSizeBytes=100 -respSizeBytes=100 -networkMode=Local -resultFile="${RUN_NAME}" go run benchmark/benchresult/main.go streaming-before streaming-after Title Before After Percentage TotalOps 21490752 21477822 -0.06% SendOps 0 0 NaN% RecvOps 0 0 NaN% Bytes/op 1902.92 1902.94 0.00% Allocs/op 29.21 29.21 0.00% ReqT/op 286543360.00 286370960.00 -0.06% RespT/op 286543360.00 286370960.00 -0.06% 50th-Lat 352.505µs 352.247µs -0.07% 90th-Lat 433.446µs 434.907µs 0.34% 99th-Lat 536.445µs 539.759µs 0.62% Avg-Lat 333.403µs 333.457µs 0.02% GoVersion go1.24.7 go1.24.7 GrpcVersion 1.77.0-dev 1.77.0-devRELEASE NOTES: