曹大帶我學 Go(10)—— 如何給 Go 提性能優化的 pr

你好,我是小 X。

曹大最近開 Go 課程了,小 X 正在和曹大學 Go。

這個系列會講一些從課程中學到的讓人醍醐灌頂的東西,撥雲見日,帶你重新認識 Go。

之前 qcrao 寫了一篇《成爲 Go Contributor》 的文章,講了如何給 Go 提一個 typo 的 pr,以此熟悉整個流程。當然,離真正的 Contributor 還差得遠。

開課前曹大在 Go 夜讀上講了他給 Go 提的一個關於 tls 的性能優化,課上又細講了下,本文就帶大家來學習下他優化了啥以及如何看優化效果。

第一次提的 pr 在這裏,之後又挪到了一個新的位置,前後有一些代碼上的簡化,最後看着挺舒服。

優化前每個 tls 連接上都有一個 write buffer,但是活躍的連接數很少,很多內存都被閒置了,這種就可以用 sync.Pool 來優化了。

conn

用 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 的使用,要自己多加練習。

好了,這就是今天全部的內容了~ 我是小 X,我們下期再見~

參考資料

[1]

tls 的性能優化: https://www.bilibili.com/video/BV1Z64y1m7uc

[2]

這裏: https://go-review.googlesource.com/c/go/+/263277

[3]

位置: https://go-review.googlesource.com/c/go/+/267957

歡迎關注曹大的 TechPaper 以及碼農桃花源~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8R9ZLa0Xlscw6qhl-5g2Cg