-
Notifications
You must be signed in to change notification settings - Fork 17.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
crypto/tls: TLS connections use small buffer size that results in small syscalls and ignore HTTP client transport buffer sizes #47672
Comments
Ok so to test my theory in production I wrote a custom TLS dialer that does some crazy unsafe stuff: func (d *deadlineDialer) DialTLSContext(ctx context.Context, network, addr string) (c net.Conn, err error) {
defer func() {
if err != nil {
err = maybeWrapErr(err)
}
}()
var firstTLSHost string
if firstTLSHost, _, err = net.SplitHostPort(addr); err != nil {
return nil, err
}
cfg := &tls.Config{}
cfg.ServerName = firstTLSHost
trace := httptrace.ContextClientTrace(ctx)
plainConn, err := d.dialer.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
tlsConn := tls.Client(plainConn, cfg)
increaseTLSBufferSizeUnsafely(tlsConn)
errc := make(chan error, 2)
var timer *time.Timer // for canceling TLS handshake
if d := transport.TLSHandshakeTimeout; d != 0 {
timer = time.AfterFunc(d, func() {
errc <- errs.NewRetryableError(errors.New("TLS handshake timeout"))
})
}
go func() {
if trace != nil && trace.TLSHandshakeStart != nil {
trace.TLSHandshakeStart()
}
err := tlsConn.Handshake()
if timer != nil {
timer.Stop()
}
errc <- err
}()
if err := <-errc; err != nil {
plainConn.Close()
if trace != nil && trace.TLSHandshakeDone != nil {
trace.TLSHandshakeDone(tls.ConnectionState{}, err)
}
return nil, err
}
cs := tlsConn.ConnectionState()
if trace != nil && trace.TLSHandshakeDone != nil {
trace.TLSHandshakeDone(cs, nil)
}
return tlsConn, nil
}
func increaseTLSBufferSizeUnsafely(tlsConn *tls.Conn) {
var (
pointerVal = reflect.ValueOf(tlsConn)
val = reflect.Indirect(pointerVal)
member = val.FieldByName("rawInput")
ptrToY = unsafe.Pointer(member.UnsafeAddr())
realPtrToY = (*bytes.Buffer)(ptrToY)
)
*realPtrToY = *bytes.NewBuffer(make([]byte, 0, 1<<18))
} After I deployed my service using the new dialer and a 256kib buffer size, all the time spent in syscalls more or less completely disappeared from my profiles. The performance difference ended up being more dramatic than I expected, profiles with the old implementation had 23.36s of CPU time whereas the new version was 19.98 for the same workload. I would share CPU profile comparisons but its a bit difficult since my application is not open source and I don't want to potentially leak any sensitive information via the method names. |
CC @FiloSottile, @katiehockman, @rolandshoemaker, @kevinburke via owners. |
+1 on this! Our database downloads new partitions to serve from S3 while it is serving reads, and we are considering rewriting our downloader component in a different language as these 4GB downloads must be squeezed through the syscall pipe that is at most 64KB wide due to the way read syscall return values:
|
Dupe of #20420. |
@FiloSottile what needs to happen to get this out of the backlog? Is the proposal acceptable and a PR along those lines would be accepted? |
Was refactoring some code and found a link to this issue above an extremely ugly hack I introduced to work around it :P Would be awesome to see this proposal accepted or an alternative proposed! |
We obviously didn't get to this for 1.20 (sorry), but it is something on my radar to look at in 1.21. |
Thanks for the workaround @richardartoul, I just used it as well. For the Go maintainers – any chance we can get this investigated for the next Go release? |
Bump on this one; it seems the unsafe workaround hack mentioned in the issue is proliferating in the wild which we probably want to avoid. |
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
Yes.
What operating system and processor architecture are you using (
go env
)?go env
OutputWhat did you do?
I'm using this library: https://github.com/google/go-cloud to read files from S3 in a streaming fashion.
I noticed that a lot of time was being spent in syscalls:
I was aware of this issue: #22618
So I tuned my http client read/write buffer transport sizes to be 256kib instead of 64kib but this had no impact on time spent in syscalls which made me suspicious that somehow the way the reads were being performed that reads were not actually being buffered as I expected.
I wrote a small program to download a file from S3 in a streaming fashion using large 1mib reads, like this:
I couldn't get dtrace to work properly on OS X, but luckily my application uses a custom dialer for setting write/read deadlines on every socket read so I was able to instrument the actual socket read sizes like this:
What did you expect to see?
Large syscall reads (in the range of 256KiB)
What did you see instead?
Extremely small sys call reads:
What did you do after?
I made a small change in
tls.go
to instantiate the TLS client with a much largerrawInput
buffer:As expected I began to observe much larger syscall reads:
I haven't tried deploying my fork to production, and measuring performance on my laptop is not interesting since I have a terrible connection to S3, but I think its well understood that a 10x increase in syscalls (especially with such a small read size of 64kib) has a dramatic impact on performance.
Proposal
I'm not 100% sure what the best approach is here, but I think we should do something since this issue means that streaming large amounts of data over TLS is much more CPU intensive than it needs to be which is a big deal for applications that process large volumes of data over the network like distributed databases.
The
tls
package already has aConfig
struct. It seems like it would be straightforward to add buffer size configuration there like has already been done for the HTTP transport. In addition, it seems reasonable that the HTTP client transport buffer sizes should be automatically propagated as the values for the TLS buffer size if the user doesn't specify a specific override.The text was updated successfully, but these errors were encountered: