Skip to content

Commit fa3543e

Browse files
committed
crypto/tls: adjust dynamic record sizes to grow arithmetically
The current code, introduced after Go 1.6 to improve latency on low-bandwidth connections, sends 1 kB packets until 1 MB has been sent, and then sends 16 kB packets (the maximum record size). Unfortunately this decreases throughput for 1-16 MB responses by 20% or so. Following discussion on #15713, change cutoff to 128 kB sent and also grow the size allowed for successive packets: 1 kB, 2 kB, 3 kB, ..., 15 kB, 16 kB. This fixes the throughput problems: the overhead is now closer to 2%. I hope this still helps with latency but I don't have a great way to test it. At the least, it's not worse than Go 1.6. Comparing MaxPacket vs DynamicPacket benchmarks: name maxpkt time/op dyn. time/op delta Throughput/1MB-8 5.07ms ± 7% 5.21ms ± 7% +2.73% (p=0.023 n=16+16) Throughput/2MB-8 15.7ms ±201% 8.4ms ± 5% ~ (p=0.604 n=20+16) Throughput/4MB-8 14.3ms ± 1% 14.5ms ± 1% +1.53% (p=0.000 n=16+16) Throughput/8MB-8 26.6ms ± 1% 26.8ms ± 1% +0.47% (p=0.003 n=19+18) Throughput/16MB-8 51.0ms ± 1% 51.3ms ± 1% +0.47% (p=0.000 n=20+20) Throughput/32MB-8 100ms ± 1% 100ms ± 1% +0.24% (p=0.033 n=20+20) Throughput/64MB-8 197ms ± 0% 198ms ± 0% +0.56% (p=0.000 n=18+7) The small MB runs are bimodal in both cases, probably GC pauses. But there's clearly no general slowdown anymore. Fixes #15713. Change-Id: I5fc44680ba71812d24baac142bceee0e23f2e382 Reviewed-on: https://go-review.googlesource.com/23487 Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent 52c2db7 commit fa3543e

File tree

3 files changed

+174
-14
lines changed

3 files changed

+174
-14
lines changed

src/crypto/tls/conn.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,11 @@ type Conn struct {
7676
input *block // application data waiting to be read
7777
hand bytes.Buffer // handshake data waiting to be read
7878

79-
// bytesSent counts the number of bytes of application data that have
80-
// been sent.
79+
// bytesSent counts the bytes of application data sent.
80+
// packetsSent counts packets.
8181
bytesSent int64
82-
82+
packetsSent int64
83+
8384
// activeCall is an atomic int32; the low bit is whether Close has
8485
// been called. the rest of the bits are the number of goroutines
8586
// in Conn.Write.
@@ -732,7 +733,7 @@ const (
732733
// recordSizeBoostThreshold is the number of bytes of application data
733734
// sent after which the TLS record size will be increased to the
734735
// maximum.
735-
recordSizeBoostThreshold = 1 * 1024 * 1024
736+
recordSizeBoostThreshold = 128 * 1024
736737
)
737738

738739
// maxPayloadSizeForWrite returns the maximum TLS payload size to use for the
@@ -787,8 +788,19 @@ func (c *Conn) maxPayloadSizeForWrite(typ recordType, explicitIVLen int) int {
787788
panic("unknown cipher type")
788789
}
789790
}
790-
791-
return payloadBytes
791+
792+
// Allow packet growth in arithmetic progression up to max.
793+
pkt := c.packetsSent
794+
c.packetsSent++
795+
if pkt > 1000 {
796+
return maxPlaintext // avoid overflow in multiply below
797+
}
798+
799+
n := payloadBytes * int(pkt + 1)
800+
if n > maxPlaintext {
801+
n = maxPlaintext
802+
}
803+
return n
792804
}
793805

794806
// writeRecordLocked writes a TLS record with the given type and payload to the

src/crypto/tls/conn_test.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,10 @@ func runDynamicRecordSizingTest(t *testing.T, config *Config) {
208208
seenLargeRecord := false
209209
for i, size := range recordSizes {
210210
if !seenLargeRecord {
211-
if size > tcpMSSEstimate {
212-
if i < 100 {
213-
t.Fatalf("Record #%d has size %d, which is too large too soon", i, size)
214-
}
215-
if size <= maxPlaintext {
216-
t.Fatalf("Record #%d has odd size %d", i, size)
217-
}
211+
if size > (i+1)*tcpMSSEstimate {
212+
t.Fatalf("Record #%d has size %d, which is too large too soon", i, size)
213+
}
214+
if size >= maxPlaintext {
218215
seenLargeRecord = true
219216
}
220217
} else if size <= maxPlaintext {

src/crypto/tls/tls_test.go

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"internal/testenv"
1212
"io"
13+
"math"
1314
"net"
1415
"strings"
1516
"testing"
@@ -146,7 +147,7 @@ func TestX509MixedKeyPair(t *testing.T) {
146147
}
147148
}
148149

149-
func newLocalListener(t *testing.T) net.Listener {
150+
func newLocalListener(t testing.TB) net.Listener {
150151
ln, err := net.Listen("tcp", "127.0.0.1:0")
151152
if err != nil {
152153
ln, err = net.Listen("tcp6", "[::1]:0")
@@ -473,3 +474,153 @@ func (w *changeImplConn) Close() error {
473474
}
474475
return w.Conn.Close()
475476
}
477+
478+
func throughput(b *testing.B, totalBytes int64, dynamicRecordSizingDisabled bool) {
479+
ln := newLocalListener(b)
480+
defer ln.Close()
481+
482+
var serr error
483+
go func() {
484+
for i := 0; i < b.N; i++ {
485+
sconn, err := ln.Accept()
486+
if err != nil {
487+
serr = err
488+
return
489+
}
490+
serverConfig := *testConfig
491+
serverConfig.DynamicRecordSizingDisabled = dynamicRecordSizingDisabled
492+
srv := Server(sconn, &serverConfig)
493+
if err := srv.Handshake(); err != nil {
494+
serr = fmt.Errorf("handshake: %v", err)
495+
return
496+
}
497+
io.Copy(srv, srv)
498+
}
499+
}()
500+
501+
b.SetBytes(totalBytes)
502+
clientConfig := *testConfig
503+
clientConfig.DynamicRecordSizingDisabled = dynamicRecordSizingDisabled
504+
505+
buf := make([]byte, 1<<16)
506+
chunks := int(math.Ceil(float64(totalBytes) / float64(len(buf))))
507+
for i := 0; i < b.N; i++ {
508+
conn, err := Dial("tcp", ln.Addr().String(), &clientConfig)
509+
if err != nil {
510+
b.Fatal(err)
511+
}
512+
for j := 0; j < chunks; j++ {
513+
_, err := conn.Write(buf)
514+
if err != nil {
515+
b.Fatal(err)
516+
}
517+
_, err = io.ReadFull(conn, buf)
518+
if err != nil {
519+
b.Fatal(err)
520+
}
521+
}
522+
conn.Close()
523+
}
524+
}
525+
526+
func BenchmarkThroughput(b *testing.B) {
527+
for _, mode := range []string{"Max", "Dynamic"} {
528+
for size := 1; size <= 64; size<<=1{
529+
name := fmt.Sprintf("%sPacket/%dMB", mode, size)
530+
b.Run(name, func(b *testing.B) {
531+
throughput(b, int64(size<<20), mode == "Max")
532+
})
533+
}
534+
}
535+
}
536+
537+
type slowConn struct {
538+
net.Conn
539+
bps int
540+
}
541+
542+
func (c *slowConn) Write(p []byte) (int, error) {
543+
if c.bps == 0 {
544+
panic("too slow")
545+
}
546+
t0 := time.Now()
547+
wrote := 0
548+
for wrote < len(p) {
549+
time.Sleep(100*time.Microsecond)
550+
allowed := int(time.Since(t0).Seconds() * float64(c.bps)) / 8
551+
if allowed > len(p) {
552+
allowed = len(p)
553+
}
554+
if wrote < allowed {
555+
n, err := c.Conn.Write(p[wrote:allowed])
556+
wrote += n
557+
if err != nil {
558+
return wrote, err
559+
}
560+
}
561+
}
562+
return len(p), nil
563+
}
564+
565+
func latency(b *testing.B, bps int, dynamicRecordSizingDisabled bool) {
566+
ln := newLocalListener(b)
567+
defer ln.Close()
568+
569+
var serr error
570+
go func() {
571+
for i := 0; i < b.N; i++ {
572+
sconn, err := ln.Accept()
573+
if err != nil {
574+
serr = err
575+
return
576+
}
577+
serverConfig := *testConfig
578+
serverConfig.DynamicRecordSizingDisabled = dynamicRecordSizingDisabled
579+
srv := Server(&slowConn{sconn, bps}, &serverConfig)
580+
if err := srv.Handshake(); err != nil {
581+
serr = fmt.Errorf("handshake: %v", err)
582+
return
583+
}
584+
io.Copy(srv, srv)
585+
}
586+
}()
587+
588+
clientConfig := *testConfig
589+
clientConfig.DynamicRecordSizingDisabled = dynamicRecordSizingDisabled
590+
591+
buf := make([]byte, 16384)
592+
peek := make([]byte, 1)
593+
594+
for i := 0; i < b.N; i++ {
595+
conn, err := Dial("tcp", ln.Addr().String(), &clientConfig)
596+
if err != nil {
597+
b.Fatal(err)
598+
}
599+
// make sure we're connected and previous connection has stopped
600+
if _, err := conn.Write(buf[:1]); err != nil {
601+
b.Fatal(err)
602+
}
603+
if _, err := io.ReadFull(conn, peek); err != nil {
604+
b.Fatal(err)
605+
}
606+
if _, err := conn.Write(buf); err != nil {
607+
b.Fatal(err)
608+
}
609+
if _, err = io.ReadFull(conn, peek); err != nil {
610+
b.Fatal(err)
611+
}
612+
conn.Close()
613+
}
614+
}
615+
616+
617+
func BenchmarkLatency(b *testing.B) {
618+
for _, mode := range []string{"Max", "Dynamic"} {
619+
for _, kbps := range []int{200, 500, 1000, 2000, 5000} {
620+
name := fmt.Sprintf("%sPacket/%dkbps", mode, kbps)
621+
b.Run(name, func(b *testing.B) {
622+
latency(b, kbps*1000, mode == "Max")
623+
})
624+
}
625+
}
626+
}

0 commit comments

Comments
 (0)