如何給 Go 提性能優(yōu)化的 pr
本文轉(zhuǎn)載自微信公眾號(hào)「碼農(nóng)桃花源」,作者小X。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼農(nóng)桃花源公眾號(hào)。
你好,我是小X。
曹大最近開(kāi) Go 課程了,小X 正在和曹大學(xué) Go。
這個(gè)系列會(huì)講一些從課程中學(xué)到的讓人醍醐灌頂?shù)臇|西,撥云見(jiàn)日,帶你重新認(rèn)識(shí) Go。
之前寫(xiě)了一篇《成為 Go Contributor》 的文章,講了如何給 Go 提一個(gè) typo 的 pr,以此熟悉整個(gè)流程。當(dāng)然,離真正的 Contributor 還差得遠(yuǎn)。
開(kāi)課前曹大在 Go 夜讀上講了他給 Go 提的一個(gè)關(guān)于 tls 的性能優(yōu)化,課上又細(xì)講了下,本文就帶大家來(lái)學(xué)習(xí)下他優(yōu)化了啥以及如何看優(yōu)化效果。
第一次提的 pr 在這里,之后又挪到了一個(gè)新的位置,前后有一些代碼上的簡(jiǎn)化,最后看著挺舒服。
優(yōu)化前每個(gè) tls 連接上都有一個(gè) write buffer,但是活躍的連接數(shù)很少,很多內(nèi)存都被閑置了,這種就可以用 sync.Pool 來(lái)優(yōu)化了。
conn
用 sync.Pool 緩存 []byte,并順帶將連接上的一個(gè) outBuf 字段給干掉了:
files changed
整體上改動(dòng)挺少,效果也不錯(cuò)。
雖然一開(kāi)始給了 _test 文件,但其實(shí)并不能太好反映性能的提升。因此后面曹大又寫(xiě)了一個(gè)簡(jiǎn)單的 client 和 server 來(lái)實(shí)際測(cè)試。
我在開(kāi)發(fā)機(jī)上測(cè)了一下,優(yōu)化還是挺明顯的。這又是一個(gè)使用 pprof 查看性能優(yōu)化的好例子。
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)
- }
- }()
- }
- }
邏輯比較簡(jiǎn)單,就是固定連接數(shù)、固定 QPS 向服務(wù)端發(fā)請(qǐng)求。
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)
- }
- }
邏輯也很簡(jiǎn)單,起了一個(gè) tls server,并注冊(cè)了一個(gè) sayhello 接口。
啟動(dòng) server 后,先用 1.15(1.17 之前的版本都可以,曹大的改動(dòng)還沒(méi)合入)測(cè)試:
- go run server.go
- # 1000 個(gè)連接,100 個(gè) QPS
- go run client.go 1000 100 https://localhost:4443
查看 server 的內(nèi)存 profile。后面還會(huì)用 --base 的命令,比較前后兩個(gè) profile 文件的差異。
pprof 的命令如下:
- go tool pprof --http=:8000 http://127.0.0.1:3333/debug/pprof/heap
Go 1.15 mem profile
看看這個(gè)大“平頂山”,有那味了(平頂山表示可以優(yōu)化,如果是那種特別窄的尖尖就沒(méi)辦法了)~
因?yàn)檫@個(gè) pr 已經(jīng)合到了 1.17,我們?cè)儆?1.17 來(lái)測(cè)一下:
- go1.17rc1 run server.go
- go1.17rc1 run client.go 1000 100 https://localhost:4443
Go 1.17 mem profile
為了使用 --base 命令來(lái)進(jìn)行比較,需要把 profile 文件保存下來(lái):
- 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
最后來(lái)比較優(yōu)化前后的差異:
- go tool pprof -http=:8000 --base mem.1.15 mem.1.17
--base
優(yōu)化效果還是很明顯的。我們來(lái)看菜單欄里的 view->top:
view->top
整個(gè)優(yōu)化從最終的提交來(lái)看還挺簡(jiǎn)單,但是能發(fā)現(xiàn)問(wèn)題所在,并能結(jié)合自己的知識(shí)儲(chǔ)備進(jìn)行優(yōu)化還是挺難的。我們平時(shí)也要多積累相關(guān)的優(yōu)化經(jīng)驗(yàn),到關(guān)鍵時(shí)候才能頂上去。像 pprof 的使用,要自己多加練習(xí)。
好了,這就是今天全部的內(nèi)容了~ 我是小X,我們下期再見(jiàn)~
參考資料
[1]tls 的性能優(yōu)化: https://www.bilibili.com/video/BV1Z64y1m7uc
[2]這里: https://go-review.googlesource.com/c/go/+/263277
[3]位置: https://go-review.googlesource.com/c/go/+/267957