溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Go語(yǔ)言開(kāi)發(fā)(八)、Go語(yǔ)言程序測(cè)試與性能調(diào)優(yōu)

發(fā)布時(shí)間:2020-06-28 22:42:28 來(lái)源:網(wǎng)絡(luò) 閱讀:29172 作者:天山老妖S 欄目:編程語(yǔ)言

Go語(yǔ)言開(kāi)發(fā)(八)、Go語(yǔ)言程序測(cè)試與性能調(diào)優(yōu)

一、Go語(yǔ)言自動(dòng)化測(cè)試框架簡(jiǎn)介

1、自動(dòng)化測(cè)試框架簡(jiǎn)介

go語(yǔ)言標(biāo)準(zhǔn)包的testing提供了單元測(cè)試(功能性測(cè)試)和性能測(cè)試(壓力測(cè)試)常用方法的框架,可以非常方便地利用其進(jìn)行自動(dòng)化測(cè)試。
go語(yǔ)言測(cè)試代碼只需要放到以?_test.go?結(jié)尾的文件中即可。golang的測(cè)試分為單元測(cè)試和性能測(cè)試,單元測(cè)試的測(cè)試用例必須以Test開(kāi)頭,其后的函數(shù)名不能以小寫(xiě)字母開(kāi)頭;性能測(cè)試必須以Benchmark開(kāi)頭,其后的函數(shù)名不能以小寫(xiě)字母開(kāi)頭。為了測(cè)試方法和被測(cè)試方法的可讀性,一般Test或Benchmark后為被測(cè)試方法的函數(shù)名。測(cè)試代碼通常與測(cè)試對(duì)象文件在同一目錄下。

2、單元測(cè)試

Go語(yǔ)言單元測(cè)試的測(cè)試用例必須以Test開(kāi)頭,其后的函數(shù)名不能以小寫(xiě)字母開(kāi)頭。
add.go文件:

package add

func add(a,b int)int{
   return a + b
}

單元測(cè)試用例:

package add

import "testing"

func TestAdd(t *testing.T){
   sum := add(1,2)
   if sum == 3 {

      t.Logf("add(1,2) == %d",sum)
   }
}

上述代碼測(cè)試數(shù)據(jù)與測(cè)試邏輯混合在一起,根據(jù)Go語(yǔ)言的特點(diǎn)和工程實(shí)踐,產(chǎn)生了一種表格驅(qū)動(dòng)測(cè)試方法。表格驅(qū)動(dòng)測(cè)試將測(cè)試數(shù)據(jù)集中保存在切片中,測(cè)試數(shù)據(jù)與測(cè)試邏輯實(shí)現(xiàn)了分離。
表格驅(qū)動(dòng)測(cè)試:

package add

import "testing"

func TestAdd(t *testing.T) {
   //定義測(cè)試數(shù)據(jù)
   tests := []struct{ a, b, c int }{
      {3, 4, 7},
      {5, 12, 17},
      {8, 15, 23},
      {12, 35, 47},
      {30000, 40000, 70000},
   }
   //測(cè)試邏輯
   for _,tt := range tests{
      if actual := add(tt.a,tt.b);actual != tt.c{
         t.Errorf("Add(%d,%d) got %d;expected %d", tt.a,tt.b,actual,tt.c)
      }
   }
}

表格驅(qū)動(dòng)測(cè)試的優(yōu)點(diǎn):
A、分離測(cè)試數(shù)據(jù)和測(cè)試邏輯
B、明確出錯(cuò)信息
C、可以部分失敗
D、Go語(yǔ)言更容易實(shí)現(xiàn)表格驅(qū)動(dòng)測(cè)試
執(zhí)行測(cè)試:
go test
結(jié)果如下:

[user@localhost test]$ go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      _/home/user/GoLang/test 0.001s

3、性能測(cè)試

性能測(cè)試即壓力測(cè)試(BMT: Benchmark Testing)。
性能測(cè)試用例:

func BenchmarkAdd(t *testing.B){

   //重置時(shí)間點(diǎn)
   t.ResetTimer()
   for i := 0; i < t.N; i++{
      add(1,2)
   }
}

完整測(cè)試代碼如下:

package add

import "testing"

func TestAdd(t *testing.T) {
   //定義測(cè)試數(shù)據(jù)
   tests := []struct{ a, b, c int }{
      {3, 4, 7},
      {5, 12, 17},
      {8, 15, 23},
      {12, 35, 47},
      {30000, 40000, 70000},
   }
   //測(cè)試邏輯
   for _,tt := range tests{
      if actual := add(tt.a,tt.b);actual != tt.c{
         t.Errorf("Add(%d,%d) got %d;expected %d", tt.a,tt.b,actual,tt.c)
      }
   }
}

func BenchmarkAdd(t *testing.B){

   //重置時(shí)間點(diǎn)
   t.ResetTimer()
   for i := 0; i < t.N; i++{
      add(1,2)
   }
}

執(zhí)行測(cè)試:
go test -bench=.
結(jié)果如下:

[user@localhost test]$ go test -bench=.
goos: linux
goarch: amd64
BenchmarkAdd-4      2000000000           0.38 ns/op
PASS
ok      _/home/user/GoLang/test 0.803s

4、代碼覆蓋率測(cè)試

測(cè)試覆蓋率是用于通過(guò)執(zhí)行某包的測(cè)試用例來(lái)確認(rèn)到的描述其的代碼在測(cè)試用例中被執(zhí)行的程度的術(shù)語(yǔ)。
在go語(yǔ)言的測(cè)試覆蓋率統(tǒng)計(jì)時(shí),go test通過(guò)參數(shù)covermode的設(shè)定可以對(duì)覆蓋率統(tǒng)計(jì)模式作如下三種設(shè)定:
A、set:缺省模式, 只記錄語(yǔ)句是否被執(zhí)行過(guò)
B、count:記錄語(yǔ)句被執(zhí)行的次數(shù)
C、atomic:記錄語(yǔ)句被執(zhí)行的次數(shù),并保證在并發(fā)執(zhí)行時(shí)的正確性
執(zhí)行覆蓋率測(cè)試:
go test -cover
結(jié)果如下:

[user@localhost test]$ go test -cover
PASS
coverage: 100.0% of statements
ok      _/home/user/GoLang/test 0.001s

執(zhí)行命令,生成代碼覆蓋率測(cè)試信息:
go test -coverprofile=covprofile
查看covprofile文件信息:

[user@localhost test]$ cat covprofile 
mode: set
_/home/user/GoLang/test/add.go:3.21,5.2 1 1
[user@localhost test]$ 

將生成代碼覆蓋率測(cè)試信息轉(zhuǎn)換為HTML格式:
go tool cover -html=covprofile -o coverage.html
使用瀏覽器查看coverage.html文件。

二、go tool pprof性能分析工具

1、go tool pprof簡(jiǎn)介

Golang內(nèi)置cpu、mem、block三種profiler采樣工具,允許程序在運(yùn)行時(shí)使用profiler進(jìn)行數(shù)據(jù)采樣,生成采樣文件。通過(guò)go tool pprof工具可以交互式分析采樣文件,得到高可讀性的輸出信息。
任何以go tool開(kāi)頭的Go命令內(nèi)部指向的特殊工具都被保存在目錄$GOROOT/pkg/tool/$GOOS_$GOARCH/目錄,即Go工具目錄。pprof工具并不是用Go語(yǔ)言編寫(xiě)的,而是由Perl語(yǔ)言編寫(xiě)。Perl語(yǔ)言可以直接讀取源碼并運(yùn)行。因此,pprof工具的源碼文件被直接保存在Go工具目錄下。
pprof工具是用Perl語(yǔ)言編寫(xiě)的,執(zhí)行g(shù)o tool pprof命令的前提條件是需要在當(dāng)前環(huán)境下安裝Perl語(yǔ)言
go tool pprof命令會(huì)分析指定的概要文件并使得能夠以交互式的方式訪(fǎng)問(wèn)其中的信息。但只有概要文件還不夠,還需要概要文件中信息的來(lái)源——命令源碼文件的可執(zhí)行文件。而可以運(yùn)行的Go語(yǔ)言程序只能是編譯命令源碼文件后生成的可執(zhí)行文件。

2、profile采樣文件簡(jiǎn)介

在Go語(yǔ)言中,可以通過(guò)標(biāo)準(zhǔn)庫(kù)的代碼包runtime和runtime/pprof中的程序來(lái)生成三種包含實(shí)時(shí)性數(shù)據(jù)的概要文件,分別是CPU概要文件、內(nèi)存概要文件和程序阻塞概要文件。
A、CPU概要文件
CPU的主頻,即CPU內(nèi)核工作的時(shí)鐘頻率(CPU Clock Speed)。CPU的主頻的基本單位是赫茲(Hz)。時(shí)鐘頻率的倒數(shù)即為時(shí)鐘周期。在一個(gè)時(shí)鐘周期內(nèi),CPU執(zhí)行一條運(yùn)算指令。在1000 Hz的CPU主頻下,每1毫秒可以執(zhí)行一條CPU運(yùn)算指令;在1 MHz的CPU主頻下,每1微妙可以執(zhí)行一條CPU運(yùn)算指令;在1 GHz的CPU主頻下,每1納秒可以執(zhí)行一條CPU運(yùn)算指令。
在默認(rèn)情況下,Go語(yǔ)言的運(yùn)行時(shí)系統(tǒng)會(huì)以100 Hz的的頻率對(duì)CPU使用情況進(jìn)行取樣,即每秒取樣100次(每10毫秒會(huì)取樣一次)。100 Hz既足夠產(chǎn)生有用的數(shù)據(jù),又不至于讓系統(tǒng)產(chǎn)生停頓,并且100容易做換算。對(duì)CPU使用情況的取樣就是對(duì)當(dāng)前的Goroutine的堆棧上的程序計(jì)數(shù)器的取樣。由此,可以從樣本記錄中分析出哪些代碼是計(jì)算時(shí)間最長(zhǎng)或者說(shuō)最耗CPU資源的部分??梢酝ㄟ^(guò)以下代碼啟動(dòng)對(duì)CPU使用情況的記錄。

func startCPUProfile() {
   if *cpuProfile != "" {
      f, err := os.Create(*cpuProfile)
      if err != nil {
         fmt.Fprintf(os.Stderr, "Can not create cpu profile output file: %s",
            err)
         return
      }
      if err := pprof.StartCPUProfile(f); err != nil {
         fmt.Fprintf(os.Stderr, "Can not start cpu profile: %s", err)
         f.Close()
         return
      }
   }
}

在函數(shù)startCPUProfile中,首先創(chuàng)建了一個(gè)用于存放CPU使用情況記錄的文件,即CPU概要文件,其絕對(duì)路徑由*cpuProfile的值表示。然后,把profile文件的實(shí)例作為參數(shù)傳入到函數(shù)pprof.StartCPUProfile中。如果pprof.StartCPUProfile函數(shù)沒(méi)有返回錯(cuò)誤,說(shuō)明記錄操作已經(jīng)開(kāi)始。只有CPU概要文件的絕對(duì)路徑有效時(shí),pprof.StartCPUProfile函數(shù)才會(huì)開(kāi)啟記錄操作。
如果想要在某一時(shí)刻停止CPU使用情況記錄操作,需要調(diào)用以下函數(shù):

func stopCPUProfile() {
   if *cpuProfile != "" {
      pprof.StopCPUProfile() // 把記錄的概要信息寫(xiě)到已指定的文件
   }
}

在以上函數(shù)中,并沒(méi)有代碼用于CPU概要文件寫(xiě)入操作。在啟動(dòng)CPU使用情況記錄操作后,運(yùn)行時(shí)系統(tǒng)就會(huì)以每秒100次的頻率將采樣數(shù)據(jù)寫(xiě)入到CPU概要文件中。pprof.StopCPUProfile函數(shù)通過(guò)把CPU使用情況取樣的頻率設(shè)置為0來(lái)停止取樣操作。只有當(dāng)所有CPU使用情況記錄都被寫(xiě)入到CPU概要文件后,pprof.StopCPUProfile函數(shù)才會(huì)退出,保證CPU概要文件的完整性。
B、內(nèi)存概要文件
內(nèi)存概要文件用于保存在用戶(hù)程序執(zhí)行期間的內(nèi)存使用情況,即程序運(yùn)行過(guò)程中堆內(nèi)存的分配情況。Go語(yǔ)言運(yùn)行時(shí)系統(tǒng)會(huì)對(duì)用戶(hù)程序運(yùn)行期間的所有的堆內(nèi)存分配進(jìn)行記錄。不論在取樣的哪一時(shí)刻、堆內(nèi)存已用字節(jié)數(shù)是否有增長(zhǎng),只要有字節(jié)被分配且數(shù)量足夠,分析器就會(huì)對(duì)其進(jìn)行取樣。開(kāi)啟內(nèi)存使用情況記錄的可以使用以下函數(shù):

func startMemProfile() {
   if *memProfile != "" && *memProfileRate > 0 {
      runtime.MemProfileRate = *memProfileRate
   }
}

開(kāi)啟內(nèi)存使用情況記錄的方式非常簡(jiǎn)單。在函數(shù)startMemProfile中,只有在memProfile和memProfileRate的值有效時(shí)才會(huì)進(jìn)行后續(xù)操作。memProfile的含義是內(nèi)存概要文件的絕對(duì)路徑。memProfileRate的含義是分析器的取樣間隔,單位是字節(jié)。當(dāng)將memProfileRate值賦給int類(lèi)型的變量runtime.MemProfileRate時(shí),意味著分析器將會(huì)在每分配指定的字節(jié)數(shù)量后對(duì)內(nèi)存使用情況進(jìn)行取樣。實(shí)際上,即使不給runtime.MemProfileRate變量賦值,內(nèi)存使用情況的取樣操作也會(huì)照樣進(jìn)行。此取樣操作會(huì)從用戶(hù)程序開(kāi)始時(shí)啟動(dòng),且一直持續(xù)進(jìn)行到用戶(hù)程序結(jié)束。runtime.MemProfileRate變量的默認(rèn)值是512 1024,即512K個(gè)字節(jié)。只有當(dāng)顯式的將0賦給runtime.MemProfileRate變量后,才會(huì)取消取樣操作。
在默認(rèn)情況下,內(nèi)存使用情況的取樣數(shù)據(jù)只會(huì)被保存在運(yùn)行時(shí)內(nèi)存中,而保存到文件的操作只能由開(kāi)發(fā)者自己來(lái)完成。取消采樣操作代碼如下:

func stopMemProfile() {
   if *memProfile != "" {
      f, err := os.Create(*memProfile)
      if err != nil {
         fmt.Fprintf(os.Stderr, "Can not create mem profile output file: %s", err)
         return
      }
      if err = pprof.WriteHeapProfile(f); err != nil {
         fmt.Fprintf(os.Stderr, "Can not write %s: %s", *memProfile, err)
      }
      f.Close()
   }
}

stopMemProfile函數(shù)的功能是停止對(duì)內(nèi)存使用情況的取樣操作。stopMemProfile只做了將取樣數(shù)據(jù)保存到內(nèi)存概要文件的操作。在stopMemProfile函數(shù)中,調(diào)用函數(shù)pprof.WriteHeapProfile,并把代表內(nèi)存概要文件的文件實(shí)例作為參數(shù)。如果pprof.WriteHeapProfile函數(shù)沒(méi)有返回錯(cuò)誤,就說(shuō)明數(shù)據(jù)已被寫(xiě)入到了內(nèi)存概要文件中。
對(duì)內(nèi)存使用情況進(jìn)行取樣的程序會(huì)假定取樣間隔在用戶(hù)程序的運(yùn)行期間內(nèi)都是一成不變的,并且等于runtime.MemProfileRate變量的當(dāng)前值。因此,應(yīng)該在Go程序中只改變內(nèi)存取樣間隔一次,且應(yīng)盡早改變。比如,在命令源碼文件的main函數(shù)的開(kāi)始處就改變內(nèi)存采樣間隔。
C、程序阻塞概要文件
程序阻塞概要文件用于保存用戶(hù)程序中的Goroutine阻塞事件的記錄。開(kāi)啟程序阻塞采樣的代碼如下:

func startBlockProfile() {
   if *blockProfile != "" && *blockProfileRate > 0 {
      runtime.SetBlockProfileRate(*blockProfileRate)
   }
}

在函數(shù)startBlockProfile中,當(dāng)blockProfile和blockProfileRate的值有效時(shí),會(huì)設(shè)置對(duì)Goroutine阻塞事件的取樣間隔。blockProfile的含義為程序阻塞概要文件的絕對(duì)路徑。blockProfileRate的含義是分析器的取樣間隔,單位是次。函數(shù)runtime.SetBlockProfileRate的唯一參數(shù)是int類(lèi)型的,含義是分析器會(huì)在每發(fā)生幾次Goroutine阻塞事件時(shí)對(duì)阻塞事件進(jìn)行取樣。如果不顯式的使用runtime.SetBlockProfileRate函數(shù)設(shè)置取樣間隔,那么取樣間隔就為1。即在默認(rèn)情況下,每發(fā)生一次Goroutine阻塞事件,分析器就會(huì)取樣一次。運(yùn)行時(shí)系統(tǒng)對(duì)Goroutine阻塞事件的取樣操作也會(huì)貫穿于用戶(hù)程序的整個(gè)運(yùn)行期。但是,如果通過(guò)runtime.SetBlockProfileRate函數(shù)將取樣間隔設(shè)置為0或者負(fù)數(shù),那么取樣操作就會(huì)被取消。
在程序結(jié)束前可以將被保存在運(yùn)行時(shí)內(nèi)存中的Goroutine阻塞事件記錄存放到指定的文件中。代碼如下:

func stopBlockProfile() {
   if *blockProfile != "" && *blockProfileRate >= 0 {
      f, err := os.Create(*blockProfile)
      if err != nil {
         fmt.Fprintf(os.Stderr, "Can not create block profile output file: %s", err)
         return
      }
      if err = pprof.Lookup("block").WriteTo(f, 0); err != nil {
         fmt.Fprintf(os.Stderr, "Can not write %s: %s", *blockProfile, err)
      }
      f.Close()
   }
}

在創(chuàng)建程序阻塞概要文件后,stopBlockProfile函數(shù)會(huì)先通過(guò)函數(shù)pprof.Lookup將保存在運(yùn)行時(shí)內(nèi)存中的內(nèi)存使用情況記錄取出,并在記錄的實(shí)例上調(diào)用WriteTo方法將記錄寫(xiě)入到文件中。

3、profiling使用場(chǎng)景

A、基準(zhǔn)測(cè)試
使用go test -bench . -cpuprofile prof.cpu生成基準(zhǔn)測(cè)試的采樣文件,再通過(guò)命令go tool pprof [binary] prof.cpu對(duì)采樣文件進(jìn)行分析。
B、Web服務(wù)測(cè)試
如果應(yīng)用是一個(gè)web服務(wù),可以在http服務(wù)啟動(dòng)的代碼文件添加import _ net/http/pprof,Web服務(wù)會(huì)自動(dòng)開(kāi)啟profile功能,輔助開(kāi)發(fā)者直接分析采樣結(jié)果??梢栽跒g覽器中使用http://localhost:port/debug/pprof/直接看到當(dāng)前web服務(wù)的狀態(tài),包括CPU占用情況和內(nèi)存使用情況等。
C、應(yīng)用程序
如果go程序是一個(gè)應(yīng)用程序,不能使用net/http/pprof包,需要使用runtime/pprof包。使用pprof.StartCPUProfile、pprof.StopCPUProfile或是內(nèi)存采樣、阻塞采樣接口等對(duì)運(yùn)行時(shí)信息進(jìn)行采樣。最終使用go tool pprof工具對(duì)采樣文件進(jìn)行分析。
D、服務(wù)進(jìn)程
如果go程序不是web服務(wù)器,而是一個(gè)服務(wù)進(jìn)程,那么也可以選擇使用net/http/pprof包,同樣引入包net/http/pprof,然后再開(kāi)啟另外一個(gè)goroutine來(lái)開(kāi)啟端口監(jiān)聽(tīng)。

go func() {
   log.Println(http.ListenAndServe("localhost:6666", nil))
}()

4、pprof使用

編寫(xiě)一個(gè)簡(jiǎn)單的應(yīng)用程序,使用pprof.StartCPUProfile和pprof.StopCPUProfile對(duì)CPU信息進(jìn)行采樣。

package main

import (
   "flag"
   "log"
   "os"
   "runtime/pprof"
   "fmt"
)

// 斐波納契數(shù)列
func Fibonacci() func() int {
   back1, back2 := 1, 1
   return func() int {
      //重新賦值
      back1, back2 = back2, (back1 + back2)
      return back1
   }
}

func count(){
    a := 0;
   for i := 0; i < 10000000000; i++ {
      a = a + i
   }
}

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")

func main() {
   flag.Parse()
   if *cpuprofile != "" {
      f, err := os.Create(*cpuprofile)
      if err != nil {
         log.Fatal(err)
      }
      pprof.StartCPUProfile(f)
      defer f.Close()
   }
   fibonacci := Fibonacci()
   for i := 0; i < 100; i++ {
      fmt.Println(fibonacci())
   }
   count()

   defer pprof.StopCPUProfile()
}

進(jìn)行運(yùn)行時(shí)信息采樣時(shí),可以指定不同的采樣參數(shù):
--cpuprofile:指定CPU概要文件的保存路徑
--blockprofile:指定程序阻塞概要文件的保存路徑。
--blockprofilerate:定義其值為n,指定每發(fā)生n次Goroutine阻塞事件時(shí),進(jìn)行一次取樣操作。
--memprofile:指定內(nèi)存概要文件的保存路徑。
--memprofilerate:定義其值為n,指定每分配n個(gè)字節(jié)的堆內(nèi)存時(shí),進(jìn)行一次取樣操作。
運(yùn)行g(shù)o程序,對(duì)CPU信息進(jìn)行采樣:
go run fibonacci.go --cpuprofile=profile.cpu
分析CPU采樣文件profile.cpu:
go tool pprof profile.cpu
Go語(yǔ)言開(kāi)發(fā)(八)、Go語(yǔ)言程序測(cè)試與性能調(diào)優(yōu)
如果Go程序非常簡(jiǎn)單,比如只有fibonacci()函數(shù)調(diào)用(注釋count()函數(shù)),使用pprof.StartCPUProfile是打印不出任何信息的。
默認(rèn)情況下top命令會(huì)列出前10項(xiàng)內(nèi)容??梢詔op命令后面緊跟一個(gè)數(shù)字,限制列出的項(xiàng)數(shù)。
Go語(yǔ)言開(kāi)發(fā)(八)、Go語(yǔ)言程序測(cè)試與性能調(diào)優(yōu)

三、go-torch性能分析工具

1、go-torch簡(jiǎn)介

go-torch是Uber公司開(kāi)源的一款針對(duì)Golang程序的火焰圖生成工具,能收集stack traces,整理成火焰圖,并直觀(guān)地顯示程序給開(kāi)發(fā)人員。go-torch是基于使用BrendanGregg創(chuàng)建的火焰圖工具生成直觀(guān)的圖像,方便地分析Go的各個(gè)方法所占用CPU的時(shí)間。

2、FlameGraph安裝

git clone https://github.com/brendangregg/FlameGraph.git
sudo cp FlameGraph/flamegraph.pl /usr/local/bin
在終端輸入flamegraph.pl -h測(cè)試FlameGraph是否安裝成功

3、go-torch安裝

go get -v github.com/uber/go-torch
go-torch默認(rèn)安裝在GOPATH指定的第一個(gè)目錄中,位于bin目錄下。

4、go-wrk壓力測(cè)試

安裝go-wrk壓力測(cè)試工具:
go get -v github.com/adjust/go-wrk
執(zhí)行35s 1W次高并發(fā)場(chǎng)景模擬:
go-wrk -d 35 -n 10000 http://localhost:port/demo

5、go-torch使用

在Web服務(wù)壓力測(cè)試過(guò)程中,使用go-torch生成采樣文件。
go-torch -u http://localhost:port -t 30
go-torch完成采樣時(shí)輸出如下信息:
Writing svg to torch.svg
torch.svg是go-torch自動(dòng)生成的profile文件,使用瀏覽器打開(kāi)如下:
Go語(yǔ)言開(kāi)發(fā)(八)、Go語(yǔ)言程序測(cè)試與性能調(diào)優(yōu)
火焰圖的y軸表示cpu調(diào)用方法的先后,x軸表示在每個(gè)采樣調(diào)用時(shí)間內(nèi),方法所占的時(shí)間百分比,越寬代表占據(jù)cpu時(shí)間越多。
根據(jù)火焰圖可以清楚的查看哪個(gè)方法調(diào)用耗時(shí)長(zhǎng),然后不斷的修正代碼,重新采樣,不斷優(yōu)化。

四、Go語(yǔ)言程序性能優(yōu)化

1、內(nèi)存優(yōu)化

A、將小對(duì)象合并成結(jié)構(gòu)體一次分配,減少內(nèi)存分配次數(shù)
Go runtime底層采用內(nèi)存池機(jī)制,每個(gè)span大小為4k,同時(shí)維護(hù)一個(gè)cache。cache有一個(gè)0到n的list數(shù)組,list數(shù)組的每個(gè)單元掛載的是一個(gè)鏈表,鏈表的每個(gè)節(jié)點(diǎn)就是一塊可用的內(nèi)存塊,同一鏈表中的所有節(jié)點(diǎn)內(nèi)存塊都是大小相等的;但是不同鏈表的內(nèi)存大小是不等的,即list數(shù)組的一個(gè)單元存儲(chǔ)的是一類(lèi)固定大小的內(nèi)存塊,不同單元里存儲(chǔ)的內(nèi)存塊大小是不等的。cache緩存的是不同類(lèi)大小的內(nèi)存對(duì)象,申請(qǐng)的內(nèi)存大小最接近于哪類(lèi)緩存內(nèi)存塊時(shí),就分配哪類(lèi)內(nèi)存塊。當(dāng)cache不夠時(shí)再向spanalloc中分配。
B、緩存區(qū)內(nèi)容一次分配足夠大小空間,并適當(dāng)復(fù)用
在協(xié)議編解碼時(shí),需要頻繁地操作[]byte,可以使用bytes.Buffer或其它byte緩存區(qū)對(duì)象。
bytes.Buffer等通過(guò)預(yù)先分配足夠大的內(nèi)存,避免當(dāng)增長(zhǎng)時(shí)動(dòng)態(tài)申請(qǐng)內(nèi)存,減少內(nèi)存分配次數(shù)。對(duì)于byte緩存區(qū)對(duì)象需要考慮適當(dāng)?shù)貜?fù)用。
C、slice和map采make創(chuàng)建時(shí),預(yù)估大小指定容量
slice和map與數(shù)組不一樣,不存在固定空間大小,可以根據(jù)增加元素來(lái)動(dòng)態(tài)擴(kuò)容。
slice初始會(huì)指定一個(gè)數(shù)組,當(dāng)對(duì)slice進(jìn)行append等操作時(shí),當(dāng)容量不夠時(shí),會(huì)自動(dòng)擴(kuò)容:
如果新的大小是當(dāng)前大小2倍以上,則容量增漲為新的大??;
否則循環(huán)以下操作:如果當(dāng)前容量小于1024,按2倍增加;否則每次按當(dāng)前容量1/4增漲,直到增漲的容量超過(guò)或等新大小。
map的擴(kuò)容比較復(fù)雜,每次擴(kuò)容會(huì)增加到上次容量的2倍。map的結(jié)構(gòu)體中有一個(gè)buckets和oldbuckets,用于實(shí)現(xiàn)增量擴(kuò)容:
正常情況下,直接使用buckets,oldbuckets為空;
如果正在擴(kuò)容,則oldbuckets不為空,buckets是oldbuckets的2倍,
因此,建議初始化時(shí)預(yù)估大小指定容量
D、長(zhǎng)調(diào)用棧避免申請(qǐng)較多的臨時(shí)對(duì)象
Goroutine的調(diào)用棧默認(rèn)大小是4K(1.7修改為2K),采用連續(xù)棧機(jī)制,當(dāng)??臻g不夠時(shí),Go runtime會(huì)自動(dòng)擴(kuò)容:
當(dāng)??臻g不夠時(shí),按2倍增加,原有棧的變量會(huì)直接copy到新的??臻g,變量指針指向新的空間地址;
退棧會(huì)釋放??臻g的占用,GC時(shí)發(fā)現(xiàn)??臻g占用不到1/4時(shí),則??臻g減少一半。
比如棧的最終大小2M,則極端情況下,就會(huì)有10次的擴(kuò)棧操作,會(huì)帶來(lái)性能下降。
因此,建議控制調(diào)用棧和函數(shù)的復(fù)雜度,不要在一個(gè)goroutine做完所有邏輯;如的確需要長(zhǎng)調(diào)用棧,而考慮goroutine池化,避免頻繁創(chuàng)建goroutine帶來(lái)?xiàng)?臻g的變化。
E、避免頻繁創(chuàng)建臨時(shí)對(duì)象
Go在GC時(shí)會(huì)引發(fā)stop the world,即整個(gè)情況暫停。Go1.8最壞情況下GC為100us。但暫停時(shí)間還是取決于臨時(shí)對(duì)象的個(gè)數(shù),臨時(shí)對(duì)象數(shù)量越多,暫停時(shí)間可能越長(zhǎng),并消耗CPU。
因此,建議GC優(yōu)化方式是盡可能地減少臨時(shí)對(duì)象的個(gè)數(shù):盡量使用局部變量;所多個(gè)局部變量合并一個(gè)大的結(jié)構(gòu)體或數(shù)組,減少掃描對(duì)象的次數(shù),一次回盡可能多的內(nèi)存。

2、并發(fā)優(yōu)化

A、高并發(fā)的任務(wù)處理使用goroutine池
Goroutine雖然輕量,但對(duì)于高并發(fā)的輕量任務(wù)處理,頻繁來(lái)創(chuàng)建goroutine來(lái)執(zhí)行,執(zhí)行效率并不會(huì)太高,因?yàn)椋哼^(guò)多的goroutine創(chuàng)建,會(huì)影響go runtime對(duì)goroutine調(diào)度,以及GC消耗;高并發(fā)時(shí)若出現(xiàn)調(diào)用異常阻塞積壓,大量的goroutine短時(shí)間積壓可能導(dǎo)致程序崩潰。
B、避免高并發(fā)調(diào)用同步系統(tǒng)接口
goroutine的實(shí)現(xiàn),是通過(guò)同步來(lái)模擬異步操作。
網(wǎng)絡(luò)IO、鎖、channel、Time.sleep、基于底層系統(tǒng)異步調(diào)用的Syscall操作并不會(huì)阻塞go runtime的線(xiàn)程調(diào)度。
本地IO調(diào)用、基于底層系統(tǒng)同步調(diào)用的Syscall、CGo方式調(diào)用C語(yǔ)言動(dòng)態(tài)庫(kù)中的調(diào)用IO或其它阻塞會(huì)創(chuàng)建新的調(diào)度線(xiàn)程。
網(wǎng)絡(luò)IO可以基于epoll的異步機(jī)制(或kqueue等異步機(jī)制),但對(duì)于一些系統(tǒng)函數(shù)并沒(méi)有提供異步機(jī)制。例如常見(jiàn)的posix api中,對(duì)文件的操作就是同步操作。雖有開(kāi)源的fileepoll來(lái)模擬異步文件操作。但Go的Syscall還是依賴(lài)底層的操作系統(tǒng)的API。系統(tǒng)API沒(méi)有異步,Go也做不了異步化處理。
因此,建議:把涉及到同步調(diào)用的goroutine,隔離到可控的goroutine中,而不是直接高并的goroutine調(diào)用。
C、高并發(fā)時(shí)避免共享對(duì)象互斥
傳統(tǒng)多線(xiàn)程編程時(shí),當(dāng)并發(fā)沖突在4~8線(xiàn)程時(shí),性能可能會(huì)出現(xiàn)拐點(diǎn)。Go推薦不通過(guò)共享內(nèi)存來(lái)通信,Go創(chuàng)建goroutine非常容易,當(dāng)大量goroutine共享同一互斥對(duì)象時(shí),也會(huì)在某一數(shù)量的goroutine出在拐點(diǎn)。
因此,建議:goroutine盡量獨(dú)立,無(wú)沖突地執(zhí)行;若goroutine間存在沖突,則可以采分區(qū)來(lái)控制goroutine的并發(fā)個(gè)數(shù),減少同一互斥對(duì)象沖突并發(fā)數(shù)。

3、其它優(yōu)化

A、避免使用CGO或者減少CGO調(diào)用次數(shù)
GO可以調(diào)用C庫(kù)函數(shù),但Go帶有垃圾收集器且Go的棧動(dòng)態(tài)增漲,無(wú)法與C無(wú)縫地對(duì)接。Go的環(huán)境轉(zhuǎn)入C代碼執(zhí)行前,必須為C創(chuàng)建一個(gè)新的調(diào)用棧,把棧變量賦值給C調(diào)用棧,調(diào)用結(jié)束現(xiàn)拷貝回來(lái)。調(diào)用開(kāi)銷(xiāo)較大,需要維護(hù)Go與C的調(diào)用上下文,兩者調(diào)用棧的映射。相比直接的GO調(diào)用棧,單純的調(diào)用??赡苡?個(gè)甚至3個(gè)數(shù)量級(jí)以上。
因此,建議:盡量避免使用CGO,無(wú)法避免時(shí),要減少跨CGO的調(diào)用次數(shù)。
B、減少[]byte與string之間轉(zhuǎn)換,盡量采用[]byte來(lái)字符串處理
GO里面的string類(lèi)型是一個(gè)不可變類(lèi)型,GO中[]byte與string底層是兩個(gè)不同的結(jié)構(gòu),轉(zhuǎn)換存在實(shí)實(shí)在在的值對(duì)象拷貝,所以盡量減少不必要的轉(zhuǎn)化。
因此,建議:存在字符串拼接等處理,盡量采用[]byte。
C、字符串的拼接優(yōu)先考慮bytes.Buffer
string類(lèi)型是一個(gè)不可變類(lèi)型,但拼接會(huì)創(chuàng)建新的string。GO中字符串拼接常見(jiàn)有如下幾種方式:
string + 操作 :導(dǎo)致多次對(duì)象的分配與值拷貝
fmt.Sprintf :會(huì)動(dòng)態(tài)解析參數(shù),效率好不哪去
strings.Join :內(nèi)部是[]byte的append
bytes.Buffer :可以預(yù)先分配大小,減少對(duì)象分配與拷貝
因此,建議:對(duì)于高性能要求,優(yōu)先考慮bytes.Buffer,預(yù)先分配大小。fmt.Sprintf可以簡(jiǎn)化不同類(lèi)型轉(zhuǎn)換與拼接。

五、Go程序文檔生成

1、go doc工具簡(jiǎn)介

go doc?工具會(huì)從Go程序和包文件中提取頂級(jí)聲明的首行注釋以及每個(gè)對(duì)象的相關(guān)注釋?zhuān)⑸上嚓P(guān)文檔。
go doc也可以作為一個(gè)提供在線(xiàn)文檔瀏覽的web服務(wù)器。

2、終端查看文檔

go doc package:獲取包的文檔注釋?zhuān)纾篻o doc fmt?會(huì)顯示使用?godoc?生成的?fmt?包的文檔注釋。
go doc package/subpackage:?獲取子包的文檔注釋?zhuān)纾篻o doc container/list。
go doc package function?:獲取某個(gè)函數(shù)在某個(gè)包中的文檔注釋?zhuān)纾篻o doc fmt Printf?會(huì)顯示有關(guān)?fmt.Printf()?的使用說(shuō)明。

3、在線(xiàn)瀏覽文檔

godoc支持啟動(dòng)一個(gè)Web在線(xiàn)API文檔服務(wù),在命令行執(zhí)行:
godoc -http=:6666
啟動(dòng)Web服務(wù)后,使用瀏覽器打開(kāi)http://127.0.0.1:6666,可以看到本地文檔瀏覽服務(wù)器提供的頁(yè)面。
Go語(yǔ)言開(kāi)發(fā)(八)、Go語(yǔ)言程序測(cè)試與性能調(diào)優(yōu)
經(jīng)測(cè)試,Google Chrome瀏覽器不能訪(fǎng)問(wèn)godoc開(kāi)啟的Web服務(wù)。

4、生成文檔

Go文檔工具支持開(kāi)發(fā)人員自己寫(xiě)的代碼,只要開(kāi)發(fā)者按照一定的規(guī)則,就可以自動(dòng)生成文檔。
add.go文件如下:

/*
add function will be add a and b.
return a+b
  */
package add

// add
func add(a,b int)int{
   return a + b
}

生成文文檔如下:

[user@localhost add]$ go doc
package add // import "add"

add function will be add a and b. return a+b

[user@localhost add]$ 

go doc工具會(huì)將包文件中頂級(jí)聲明的首行注釋提取出來(lái)。如果函數(shù)為private(函數(shù)名稱(chēng)首字母小寫(xiě)),go doc工具會(huì)隱藏函數(shù)。

/*
add function will be add a and b.
return a+b
  */
package add

// add
func Add(a,b int)int{
   return a + b
}

生成文檔如下:

[user@localhost add]$ go doc
package add // import "add"

add function will be add a and b. return a+b

func Add(a, b int) int
[user@localhost add]$ 

5、添加文檔示例

Go語(yǔ)言的文檔中添加示例代碼的步驟如下:
A、示例代碼必須單獨(dú)存放在一個(gè)文件(文件名字為example_test.go)中或是測(cè)試代碼文件中。
B、在示例代碼文件里,定義一個(gè)名字為Example的函數(shù),參數(shù)為空
C、示例的輸出采用注釋的方式,以// Output:開(kāi)頭,另起一行,每行輸出占一行。

package add

import (
    "fmt"
)

func Example(){
    sum := add(1,2)
    fmt.Println(sum)
    // Output:
    // 3
}

生成文檔結(jié)果如下:
Go語(yǔ)言開(kāi)發(fā)(八)、Go語(yǔ)言程序測(cè)試與性能調(diào)優(yōu)

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI