1

如何给 Go 提性能优化的 pr

 3 years ago
source link: https://qcrao.com/2021/08/03/go-tls-pr-by-xargin/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

如何给 Go 提性能优化的 pr

之前写了一篇《成为 Go Contributor》 的文章,讲了如何给 Go 提一个 typo 的 pr,以此熟悉整个流程。当然,离真正的 Contributor 还差得远。

开课前曹大在 Go 夜读上讲了他给 Go 提的一个关于 tls 的性能优化,课上又细讲了下,本文就带大家来学习下他优化了啥以及如何看优化效果。

第一次提的 pr 在这里,之后又挪到了一个新的位置,前后有一些代码上的简化,最后看着挺舒服。

优化前每个 tls 连接上都有一个 write buffer,但是活跃的连接数很少,很多内存都被闲置了,这种就可以用 sync.Pool 来优化了。

用 sync.Pool 缓存 []byte,并顺带将连接上的一个 outBuf 字段给干掉了:

files changed

整体上改动挺少,效果也不错。

虽然一开始给了 _test 文件,但其实并不能太好反映性能的提升。因此后面曹大又写了一个简单的 client 和 server 来实际测试。

我在开发机上测了一下,优化还是挺明显的。这又是一个使用 pprof 查看性能优化的好例子。

client 的代码如下:

package main

import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"os"
"strconv"
"sync"

"go.uber.org/ratelimit"
)

func main() {
url := os.Args[3]
connNum, err := strconv.ParseInt(os.Args[1], 10, 64)
if err != nil {
fmt.Println(err)
return
}

qps, err := strconv.ParseInt(os.Args[2], 10, 64)
if err != nil {
fmt.Println(err)
return
}

bucket := ratelimit.New(int(qps))

var l sync.Mutex
connList := make([]*http.Client, connNum)

for i := 0; ; i++ {
bucket.Take()
i := i
go func() {
l.Lock()
if connList[i%len(connList)] == nil {
connList[i%len(connList)] = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
IdleConnTimeout: 0,
MaxIdleConns: 1,
MaxIdleConnsPerHost: 1,
},
}
}
conn := connList[i%len(connList)]
l.Unlock()
if resp, e := conn.Get(url); e != nil {
fmt.Println(e)
} else {
defer resp.Body.Close()
ioutil.ReadAll(resp.Body)
}
}()
}

}

逻辑比较简单,就是固定连接数、固定 QPS 向服务端发请求。

server 的代码如下:

package main

import (
"fmt"
"net/http"
_ "net/http/pprof"
)

var content = make([]byte, 16000)

func sayhello(wr http.ResponseWriter, r *http.Request) {
wr.Header()["Content-Length"] = []string{fmt.Sprint(len(content))}
wr.Header()["Content-Type"] = []string{"application/json"}
wr.Write(content)
}

func main() {
go func() {
http.ListenAndServe(":3333", nil)
}()
http.HandleFunc("/", sayhello)

err := http.ListenAndServeTLS(":4443", "server.crt", "server.key", nil)
if err != nil {
fmt.Println(err)
}
}

逻辑也很简单,起了一个 tls server,并注册了一个 sayhello 接口。

启动 server 后,先用 1.15(1.17 之前的版本都可以,曹大的改动还没合入)测试:

go run server.go

# 1000 个连接,100 个 QPS
go run client.go 1000 100 https://localhost:4443

查看 server 的内存 profile。后面还会用 --base 的命令,比较前后两个 profile 文件的差异。

pprof 的命令如下:

go tool pprof --http=:8000 http://127.0.0.1:3333/debug/pprof/heap

Go 1.15 mem profile

看看这个大“平顶山”,有那味了(平顶山表示可以优化,如果是那种特别窄的尖尖就没办法了)~

因为这个 pr 已经合到了 1.17,我们再用 1.17 来测一下:

go1.17rc1 run server.go
go1.17rc1 run client.go 1000 100 https://localhost:4443

Go 1.17 mem profile

为了使用 --base 命令来进行比较,需要把 profile 文件保存下来:

curl http://127.0.0.1:3333/debug/pprof/heap > mem.1.14
curl http://127.0.0.1:3333/debug/pprof/heap > mem.1.17

最后来比较优化前后的差异:

go tool pprof -http=:8000 --base mem.1.15 mem.1.17

--base

优化效果还是很明显的。我们来看菜单栏里的 view->top

view->top

整个优化从最终的提交来看还挺简单,但是能发现问题所在,并能结合自己的知识储备进行优化还是挺难的。我们平时也要多积累相关的优化经验,到关键时候才能顶上去。像 pprof 的使用,要自己多加练习。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK