go crypto/tls:TLS连接使用小缓冲区大小,导致小的系统调用并忽略HTTP客户端传输缓冲区大小,

1tuwyuhd  于 6个月前  发布在  Go
关注(0)|答案(9)|浏览(54)

你正在使用的Go版本是什么( go version )?

$ go version

`go1.16 darwin/amd6`

这个问题在最新版本的发布中是否重现?

是的。

你正在使用什么操作系统和处理器架构( go env )?

go env 输出

$ go env

COMP12013:dd-go richard.artoul$ go env
GO111MODULE="auto"
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/richard.artoul/Library/Caches/go-build"
GOENV="/Users/richard.artoul/Library/Application Support/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/richard.artoul/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/richard.artoul/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/Cellar/go/1.15/libexec"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/Cellar/go/1.15/libexec/pkg/tool/darwin_amd64"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/t2/02qzh_vs4cn57ctvc7dwcsc80000gn/T/go-build075316343=/tmp/go-build -gno-record-gcc-switches -fno-common"

你做了什么?

我正在使用这个库: https://github.com/google/go-cloud 从S3以流式方式读取文件。
我注意到很多时间都在系统调用中度过:

我知道这个问题: #22618
所以我调整了我的http客户端读/写缓冲区传输大小为256kib,而不是64kib,但这并没有影响到在系统调用中花费的时间,这让我怀疑某种方式下读取操作实际上没有按照我预期的方式进行缓冲。
我编写了一个小程序,从S3以流式方式下载文件,使用大1mib的读取:

stream, err := store.GetStream(ctx, *bucket, *path)
if err != nil {
    log.Fatalf("error getting: %s, err: %v", *path, err)
}
defer stream.Close()

buf := make([]byte, 1<<20)
    for {
        n, err := bufio.NewReaderSize(stream, 1<<20).Read(buf)
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatal(err)
        }
	fmt.Println("n:", n)
}

我在OS X上无法让dtrace正常工作,但幸运的是,我的应用程序使用了一个自定义拨号器,用于在每次套接字读取时设置写/读截止时间,所以我可以像这样仪器化实际的套接字读取大小:

func (d *deadlineConn) Read(p []byte) (int, error) {
	d.Conn.SetReadDeadline(time.Now().Add(d.readDeadline))
	fmt.Println("read size", len(p))
	n, err := d.Conn.Read(p)
	err = maybeWrapErr(err)
	return n, err
}

你期望看到什么?

大的系统调用读取(在256KiB范围内)

你看到了什么?

极小的系统调用读取:

read size 52398
n: 16384
n: 1024
n: 16384
n: 1024
n: 16384
n: 1024
read size 52398
n: 16384
n: 1024
read size 28361
read size 26929
n: 16384
n: 1024
n: 16384
read size 5449
n: 1024
read size 56415
read size 47823
n: 16384
n: 1024
n: 16384
read size 23479

之后你做了什么?

我在 tls.go 中做了一个小改动,示例化TLS客户端并使用一个更大的 rawInput 缓冲区:

// Client returns a new TLS client side connection
// using conn as the underlying transport.
// The config cannot be nil: users must set either ServerName or
// InsecureSkipVerify in the config.
func Client(conn net.Conn, config *Config) *Conn {
	c := &Conn{
		rawInput: *bytes.NewBuffer(make([]byte, 0, 1<<20)),
		conn:     conn,
		config:   config,
		isClient: true,
	}
	c.handshakeFn = c.clientHandshake
	return c
}

如预期,我开始观察到更大的系统调用读取:

read size 1034019
read size 1024035
read size 1022603
read size 1003987
read size 993963
read size 991099
read size 985371
read size 982507
read size 981075
read size 965323
read size 963891
read size 955299
read size 945275
read size 935251

我还没有尝试将我的分支部署到生产环境,而且在我的笔记本电脑上测量性能并不有趣,因为我对S3的连接非常糟糕,但我认为大家都明白,syscalls(尤其是如此小的读取大小为64kib)的10倍增加会对性能产生巨大影响。

建议

我不确定这里最好的方法是什么,但我认为我们应该做点什么,因为这个问题意味着通过TLS传输大量数据比它需要的CPU密集度要高得多,这对于处理网络上大量数据的分布式数据库等应用程序来说是个大问题。
tls 包已经有一个 Config 结构。似乎可以在那里简单地添加缓冲区大小配置,就像已经为HTTP传输所做的那样。此外,如果用户没有指定特定的覆盖值,那么HTTP客户端传输缓冲区大小应该自动传播为TLS缓冲区大小的值似乎是合理的。

oxcyiej7

oxcyiej71#

好的,为了测试我的理论在生产环境中,我编写了一个自定义的TLS拨号器,它执行了一些非常不安全的操作:

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))
}

在我使用新的拨号器和256kib缓冲区大小部署了服务之后,我在系统调用中花费的所有时间几乎完全从我的配置文件中消失了。性能差异最终比我预期的要大得多,使用旧实现的配置文件有23.36秒的CPU时间,而新版本在同一工作负载下为19.98秒。
我会分享CPU配置文件的比较,但这有点困难,因为我的应用程序不是开源的,我不想通过方法名称潜在地泄露任何敏感信息。

mzaanser

mzaanser2#

CC @FiloSottile, @katiehockman, @rolandshoemaker, @kevinburke via owners

olmpazwi

olmpazwi3#

+1 赞同这个观点!我们的数据库在提供读取服务的同时,从S3下载新的分区。我们正在考虑用不同的语言重写下载器组件,因为这4GB的下载必须通过最多64KB宽的系统调用管道进行,这是Conn.rawInput工作方式导致的。
读取系统调用返回值:

@bytes:
(..., 0)           87559 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[0]                 1629 |                                                    |
[1]                 1287 |                                                    |
[2, 4)               103 |                                                    |
[4, 8)                54 |                                                    |
[8, 16)              464 |                                                    |
[16, 32)             121 |                                                    |
[32, 64)             134 |                                                    |
[64, 128)            213 |                                                    |
[128, 256)           666 |                                                    |
[256, 512)           608 |                                                    |
[512, 1K)            948 |                                                    |
[1K, 2K)            4310 |@@                                                  |
[2K, 4K)            6920 |@@@@                                                |
[4K, 8K)           25991 |@@@@@@@@@@@@@@@                                     |
[8K, 16K)          46532 |@@@@@@@@@@@@@@@@@@@@@@@@@@@                         |
[16K, 32K)         42064 |@@@@@@@@@@@@@@@@@@@@@@@@                            |
[32K, 64K)         16326 |@@@@@@@@@                                           |
[64K, 128K)          160 |                                                    |
[128K, 256K)           3 |                                                    |
[256K, 512K)           1 |                                                    |
[512K, 1M)             2 |                                                    |
wa7juj8i

wa7juj8i5#

#20420 的副本。我认为它相关但不是副本。#20420 是关于协商协议中较小的单个记录以减少(对等方)内存需求的。这是关于一次从线路读取多个记录以减少系统调用开销的。

ruoxqz4g

ruoxqz4g6#

@FiloSottile ,为了解决这个问题,需要采取什么措施?提案是否可接受,如果沿着这些方向进行PR,是否会被接受?

dy2hfwbg

dy2hfwbg7#

正在重构一些代码,发现上面有一个链接指向这个问题,我引入了一个非常丑陋的hack来解决它:P如果能看到这个建议被接受或者有替代方案提出就太棒了!

ht4b089n

ht4b089n8#

我们显然没有在1.20(抱歉)达到这个目标,但这将是我在1.21关注的事情。

20jt8wwn

20jt8wwn9#

感谢@richardartoul提供的解决方法,我也使用了这个方法。
对于Go维护者——我们是否有机会在下一个Go版本中对此进行调查?

相关问题