golang 分析調(diào)試高階技巧


layout: post
title: "golang 調(diào)試高階技巧"
date: 2020-6-03 1:44:09 +0800
categories: golang GC 垃圾回收


  • golang 高階調(diào)試
    • Golang tools
      • nm
      • compile
      • objdump
      • pprof
      • trace
    • 單元測試
      • 執(zhí)行單元測試
        • go test 運行
        • 編譯,運行
      • 統(tǒng)計代碼覆蓋率
    • 程序 Debug
      • dlv 調(diào)試用法
        • 調(diào)試二進制
        • 調(diào)試進程
        • 調(diào)試 core 文件
        • 調(diào)試常用語法
          • 系統(tǒng)整理
          • 應(yīng)用舉例
      • gdb 調(diào)試
    • 小技巧
      • 不知道怎么斷點函數(shù)?
      • 不知道調(diào)用上下文?
      • 不知道怎么開啟 pprof ?
      • 為什么有時候單點調(diào)試的時候,總是非預(yù)期的執(zhí)行代碼?
    • 總結(jié)

golang 高階調(diào)試

本文專注 golang debug 的一些技巧應(yīng)用,以及相關(guān)工具的實用用法,再也不用怕 golang 怎么調(diào)試。golang 作為一門現(xiàn)代化語音,出生的時候就自帶完整的 debug 手段:

  • golang tools 是直接集成在語言工具里,支持內(nèi)存分析,cpu分析,阻塞鎖分析等;
  • delve,gdb 作為最常用的 debug 工具,讓你能夠更深入的進入程序調(diào)試;
    • delve 當前是最友好的 golang 調(diào)試程序,ide 調(diào)試其實也是調(diào)用 dlv 而已,比如 goland;
  • 單元測試的設(shè)計深入到語言設(shè)計級別,可以非常方便執(zhí)行單元測試并且生成代碼覆蓋率;

Golang tools

golang 從語言原生層面就集成了大量的實用工具,這些都是 Robert Griesemer, Rob Pike, Ken Thompson 這幾位大神經(jīng)驗沉淀下的精華。你安裝好 golang 之后,執(zhí)行 go tool 就能看到內(nèi)置支持的所有工具了。

root@ubuntu:~# go tool
addr2line
asm
buildid
cgo
compile
cover
dist
doc
fix
link
nm
objdump
pack
pprof
test2json
trace
vet

我這里專注挑選幾個 debug 常用的:

  • nm:查看符號表(等同于系統(tǒng) nm 命令)
  • objdump:反匯編工具,分析二進制文件(等同于系統(tǒng) objdump 命令)
  • pprof:指標,性能分析工具
  • cover:生成代碼覆蓋率
  • trace:采樣一段時間,指標跟蹤分析工具
  • compile:代碼匯編

nm

查看符號表的命令,等同于系統(tǒng)的 nm 命令,非常有用。在斷點的時候,如果你不知道斷點的函數(shù)符號,那么用這個命令查一下就知道了(命令處理的是二進制程序文件)。

# exmple 為你編譯的二進制文件
go tool nm ./example

第一列是地址,第二列是類型,第三列是符號:

[圖片上傳失敗...(image-1c9b7a-1594910164396)]

compile

匯編某個文件

go tool compile -N -l -S example.go

你就能看到你 golang 語言對應(yīng)的匯編代碼了(注意了,命令處理的是 golang 代碼文本),酷。

objdump

反匯編二進制的工具,等同于系統(tǒng) objdump(注意了,命令解析的是二進制格式的程序文件)。

go tool objdump example.o
go tool objdump -s DoFunc example.o  // 反匯編具體函數(shù)

匯編代碼這個東西在 90% 的場景可能都用不上,但是如果你處理過 c 的程序,在某些特殊場景,通過反匯編一段邏輯來推斷應(yīng)用程序行為將是你唯一的出路。因為線上的代碼一般都是會開啟編譯優(yōu)化,所以這里會導(dǎo)致你的代碼對不上。再者,線上不可能讓你隨意 attach 進程,很多時候都是出 core 了,你就只有一個 core 文件去排查。

pprof

pprof 支持四種類型的分析:

  • CPU :CPU 分析,采樣消耗 cpu 的調(diào)用,這個一般用來定位排查程序里耗費計算資源的地方;
  • Memroy :內(nèi)存分析,一般用來排查內(nèi)存占用,內(nèi)存泄露等問題;
  • Block :阻塞分析,會采樣程序里阻塞的調(diào)用情況;
  • Mutex :互斥鎖分析,采樣互斥鎖的競爭情況;

我們這里詳細以內(nèi)存占用分析舉例(其他的類似),pprof 這個是內(nèi)存分析神器?;旧希琯olang 有了這個東西,99% 的內(nèi)存問題(比如內(nèi)存泄露,內(nèi)存占用過大等等)都是可以非常快的定位出來的。首先,對于 golang 的內(nèi)存分析(或者其他的鎖消耗,cpu 消耗)我們明確幾個重要的點:

  • golang 內(nèi)存 pprof 是采樣的,每 512KB 采樣一次;
  • golang 的內(nèi)存采樣的是堆棧路徑,而不是類型信息;
  • golang 的內(nèi)存采樣入口一定是通過mProf_MallocmProf_Free 這兩個函數(shù)。所以,如果是 cgo 分配的內(nèi)存,那么是沒有機會調(diào)用到這兩個函數(shù)的,所以如果是 cgo 導(dǎo)致的內(nèi)存問題,go tool pprof 是分析不出來的;

詳細原理,可以復(fù)習另一篇文章:內(nèi)存分析;

分析的形式有兩種:

  1. 如果是 net/http/pporf 方式開啟的,那么可以直接在控制臺上輸入,瀏覽器就能看;
  2. 另一種方式是先把信息 dump 到本地文件,然后用 go tool 去分析(我們以這個舉例,因為這種方式才是生產(chǎn)環(huán)境通用的方式)
# 查看累計分配占用
go tool pprof -alloc_space ./29075_20190523_154406_heap
# 查看當前的分配占用
go tool pprof -inuse_space ./29075_20190523_154406_allocs

你也可以不指定類型,直接 go tool pprof ./xxx ,進入分析之后,調(diào)用 o 選項,指定類型:

我寫了一個 demo 程序,然后 dump 出了一份 heap 的 pprof 采樣文件,我們先通過這個 pprof 得出一些結(jié)論,最后我再貼出源代碼,再品一品。

go tool pprof ./29075_20190523_154406_heap
(pprof) o              
...          
  sample_index              = inuse_space          //: [alloc_objects | alloc_space | inuse_objects | inuse_space]
...       
(pprof) alloc_space
(pprof) top
Showing nodes accounting for 290MB, 100% of 290MB total
      flat  flat%   sum%        cum   cum%
     140MB 48.28% 48.28%      140MB 48.28%  main.funcA (inline)
     100MB 34.48% 82.76%      190MB 65.52%  main.funcB (inline)
      50MB 17.24%   100%      140MB 48.28%  main.funcC (inline)
         0     0%   100%      290MB   100%  main.main
         0     0%   100%      290MB   100%  runtime.main

這個 top 信息表明了這么幾點信息:

  • main.funcA 這個函數(shù)現(xiàn)場分配了 140M 的內(nèi)存,main.funcB 這個函數(shù)現(xiàn)場分配了 100M 內(nèi)存,main.funcC 現(xiàn)場分配了 50M 內(nèi)存;
    • 現(xiàn)場的意思:純粹自己函數(shù)直接分配的,而不是調(diào)用別的函數(shù)分配的;
    • 這些信息通過 flat 得知;
  • main.funcA 分配的 140M 內(nèi)存純粹是自己分配的,沒有調(diào)用別的函數(shù)分配過內(nèi)存;
    • 這個信息通過 main.funcA flat 和 cum 都為 140 M 得出;
  • main.funcB 自己分配了 100MB,并且還調(diào)用了別的函數(shù),別的函數(shù)里面涉及了 90M 的內(nèi)存分配;
    • 這個信息通過 main.funcB flat 和 cum 分別為 100 M,190M 得出;
  • main.funcC 自己分配了 50MB,并且還調(diào)用了別的函數(shù),別的函數(shù)里面涉及了 90M 的內(nèi)存分配;
    • 這個信息通過 main.funcC flat 和 cum 分別為 50 M,140 M 得出;
  • main.main :所有分配內(nèi)存的函數(shù)調(diào)用都是走這個函數(shù)出去的。main 函數(shù)本身沒有函數(shù)分配,但是他調(diào)用的函數(shù)分配了 290M;

demo 的源代碼:

package main

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

func funcA() []byte {
    a := make([]byte, 10*1024*1024)
    return a
}

func funcB() ([]byte, []byte) {
    a := make([]byte, 10*1024*1024)
    b := funcA()
    return a, b
}

func funcC() ([]byte, []byte, []byte) {
    a := make([]byte, 10*1024*1024)
    b, c := funcB()
    return a, b, c
}

func main() {
    for i := 0; i < 5; i++ {
        funcA()
        funcB()
        funcC()
    }

    http.ListenAndServe("0.0.0.0:9999", nil)
}

dump 命令

curl -sS 'http://127.0.0.1:9999/debug/pprof/heap?seconds=5' -o heap.pporf

對照著代碼,再品一品。

trace

程序 trace 調(diào)試

go tool trace -http=":6060" ./ssd_336959_20190704_105540_trace

trace 這個命令允許你跟蹤采集一段時間的信息,然后 dump 成文件,最后調(diào)用 go tool trace 分析 dump 文件,并且以 web 的形式打開。

單元測試

單元測試的重要性就不再論述。golang 里面 _test.go 結(jié)尾的文件認為是測試文件,golang 作為現(xiàn)代化的語言,語言工具層面支持單元測試。

執(zhí)行單元測試

執(zhí)行單元測試有兩種方式:

  • go test 直接運行,這個是最簡單的;
  • 先編譯測試文件,再運行。這種方式更靈活;

go test 運行

// 直接在你項目目錄里運行 go test .
go test .
// 指定運行函數(shù)
go test -run=TestPutAndGetKeyValue
// 打印詳細信息
go test -v

編譯,運行

本質(zhì)上,golang 跑單測是先編譯 *_test.go 文件,編譯成二進制后,再運行這個二進制文件。你執(zhí)行 go test 的時候,工具幫你做好了,這些動作其實也是可以拆開來自己做的。

編譯生成單元測試可執(zhí)行文件:

// 先編譯出 .test 文件
$ go test -c 

// 指定跑某一個文件
$ ./raftexample.test -test.timeout=10m0s -test.v=true -test.run=TestPutAndGetKeyValue

這種方式通常會出現(xiàn)在以下幾種場景:

  1. 這臺機器上編譯,另一個地方跑單測;
  2. debug 單測程序;

統(tǒng)計代碼覆蓋率

golang 的代碼覆蓋率是基于單測的,由單測作為出發(fā)點,來看你的業(yè)務(wù)代碼覆蓋率。

操作很簡單:

  1. 加一個 -coverprofile 的參數(shù),聲明在跑單測的時候,記錄代碼覆蓋率;
  2. 使用 go tool cover 命令分析,得出覆蓋率報告;
go test -coverprofile=coverage.out
go tool cover -func=coverage.out

類似如下:

root@ubuntu:~/opensource/readcode-etcd-master/src/go.etcd.io/etcd/contrib/raftexample# go tool cover -func=coverage.out
go.etcd.io/etcd/v3/contrib/raftexample/httpapi.go:33:   ServeHTTP       25.0%
go.etcd.io/etcd/v3/contrib/raftexample/httpapi.go:108:  serveHttpKVAPI      0.0%
go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:41:   newKVStore      100.0%
go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:50:   Lookup          100.0%
go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:57:   Propose         75.0%
go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:71:   readCommits     55.0%
go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:107:  getSnapshot     100.0%
go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:113:  recoverFromSnapshot 85.7%
go.etcd.io/etcd/v3/contrib/raftexample/listener.go:30:  newStoppableListener    75.0%
go.etcd.io/etcd/v3/contrib/raftexample/listener.go:38:  Accept          92.9%
go.etcd.io/etcd/v3/contrib/raftexample/main.go:24:  main            0.0%
total:                          (statements)        57.1%

這樣的話,你就知道每個函數(shù)的代碼覆蓋率。

程序 Debug

程序的調(diào)試主要由兩個工具:

  1. dlv
  2. gdb

這里推薦 dlv,因為 gdb 功能實在是有限,gdb 不理解 golang 的業(yè)務(wù)類型和協(xié)程。但是 gdb 有一個功能是無法替代的,就是 gcore 的功能。

dlv 調(diào)試用法

調(diào)試二進制

dlv exec <path/to/binary> [flags]

舉例:

dlv exec ./example

dlv 調(diào)試二進制,并帶參數(shù)

dlv exec ./example -- --audit=./d

調(diào)試進程

dlv attach ${pid} [executable] [flags]

進程號是必選的。

舉例:

dlv attach 12808 ./example

調(diào)試 core 文件

dlv 調(diào)試core文件;并且標準輸出導(dǎo)出到文件

dlv core <executable> <core> [flags]

dlv core ./example core.277282

調(diào)試常用語法

系統(tǒng)整理

程序運行

  1. call :call 函數(shù)(注意了,這個會導(dǎo)致整個程序運行的)
  2. continue :往下運行
  3. next :單步調(diào)試
  4. restart :重啟
  5. step :單步調(diào)試,某個函數(shù)
  6. step-instruction :單步調(diào)試某個匯編指令
  7. stepout :從當前函數(shù)跳出

斷點相關(guān)

  1. break (alias: b) :設(shè)置斷點
  2. breakpoints (alias: bp) :打印所有的斷點信息
  3. clear :清理斷點
  4. clearall :清理所有的斷點
  5. condition (alias: cond) :設(shè)置條件斷點
  6. on :設(shè)置一段命令,當斷點命中的時候
  7. trace (alias: t) :設(shè)置一個跟蹤點,這個跟蹤點也是一個斷點,只不過運行道德時候不會斷住程序,只是打印一行信息,這個命令在某些場景是很有用的,比如你斷住程序就會影響邏輯(業(yè)務(wù)有超時),而你僅僅是想打印某個變量而已,那么用這種類型的斷點就行;;

信息打印

  • args : 打印程序的傳參
  • examinemem (alias: x) :這個是神器,解析內(nèi)存用的,和 gdb 的 x 命令一樣;
  • locals :打印本地變量
  • print (alias: p) :打印一個表達式,或者變量
  • regs :打印寄存器的信息
  • set :set 賦值
  • vars :打印全局變量(包變量)
  • whatis :打印類型信息

協(xié)程相關(guān)

  • goroutine (alias: gr) :打印某個特定協(xié)程的信息
  • goroutines (alias: grs) :列舉所有的協(xié)程
  • thread (alias: tr) :切換到某個線程
  • threads :打印所有的線程信息

棧相關(guān)

  • deferred :在 defer 函數(shù)上下文里執(zhí)行命令
  • down :上堆棧
  • frame :跳到某個具體的堆棧
  • stack (alias: bt) :打印堆棧信息
  • up :下堆棧

其他命令

  • config :配置變更
  • disassemble (alias: disass) :反匯編
  • edit (alias: ed) :略
  • exit (alias: quit | q) :略
  • funcs :打印所有函數(shù)符號
  • libraries :打印所有加載的動態(tài)庫
  • list (alias: ls | l) :顯示源碼
  • source :加載命令
  • sources :打印源碼
  • types :打印所有類型信息

以上就是完整的 dlv 的支持的命令,從這個來看,是完全滿足我們的調(diào)試需求的(有的只適用于開發(fā)調(diào)試環(huán)節(jié),比如線上的程序不可能讓你隨意單步調(diào)試的,有的使用于線上生產(chǎn)環(huán)節(jié))。

應(yīng)用舉例

打印全局變量

(dlv) vars

這個非常有用,幫助你看一些全局變量。

條件斷點

# 先斷點
(dlv) b 

# 查看斷點信息
(dlv) bp

# 然后定制條件
(dlv) condition 2 i==2 && j==7 && z==32

查看堆棧

# 展示所有堆棧
(dlv) goroutines
# 所有堆棧展開
(dlv) goroutines -t

解析內(nèi)存

(dlv) x -fmt hex -len 20 0xc00008af38

x 命令和 gdb 的 x 是一樣的。

gdb 調(diào)試

gdb 對 golang 的調(diào)試支持是通過一個 python 腳本文件 src/runtime/runtime-gdb.py 來擴展的,所以功能非常有限。gdb 只能做到最基本的變量打印,卻理解不了 golang 的一些特殊類型,比如 channel,map,slice 等,gdb 原生是無法調(diào)適 goroutine 協(xié)程的,因為這個是用戶態(tài)的調(diào)度單位,gdb 只能理解線程。所以只能通過 python 腳本的擴展,把協(xié)程結(jié)構(gòu)按照鏈表輸出出來,支持的命令:

[圖片上傳失敗...(image-c8e3d1-1594910164394)]

gdb當前只支持6個命令:

3個 cmd 命令

  1. info goroutines;打印所有的goroutines
  2. goroutine ${id} bt;打印一個goroutine的堆棧
  3. iface;打印靜態(tài)或者動態(tài)的接口類型

3個函數(shù)

  1. len;打印string,slices,map,channels 這四種類型的長度
  2. cap;打印slices,channels 這兩種類型的cap
  3. dtype;強制轉(zhuǎn)換接口到動態(tài)類型。

打印全局變量 (注意單引號)

(gdb) p 'runtime.firstmoduledata'

由于 gdb 不理解 golang 的一些類型系統(tǒng),所以調(diào)試打印的時候經(jīng)常打印不出來,這個要注意下。

打印數(shù)組變量長度

(gdb) p $len(xxx)

所以,我一般只用 gdb 來 gcore 而已。

小技巧

不知道怎么斷點函數(shù)?

有時候不知道怎么斷點函數(shù):可以通過nm查詢下,然后再斷點,就一定能斷到了。

[圖片上傳失敗...(image-f2bd4b-1594910164394)]

[圖片上傳失敗...(image-e94d0b-1594910164394)]

不知道調(diào)用上下文?

在你的代碼里添加一行:

debug.PrintStack()

這樣就能當前代碼位置的堆棧給打印出來,這樣你就直到怎么函數(shù)的調(diào)用路徑了。

不知道怎么開啟 pprof ?

pprof 功能有兩種開啟方式,對應(yīng)兩種包:

  • net/http/pprof : 使用在 web 服務(wù)器的場景;
  • runtime/pprof :使用在非服務(wù)器應(yīng)用程序的場景;

這兩個本質(zhì)上是一致的,net/http/pporf 也只是在 runtime/pprof 上的一層 web 封裝。

net/http/pprof 方式

import _ "net/http/pprof"

runtime/pprof 方式

這種通常用于程序調(diào)優(yōu)的場景,程序只是一個應(yīng)用程序,跑一次就結(jié)束,你想找到瓶頸點,那么通常會使用到這個方式。

    // cpu pprof 文件路徑
    f, err := os.Create("cpufile.pprof")
    if err != nil {
        log.Fatal(err)
    }
    // 開啟 cpu pprof
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()

為什么有時候單點調(diào)試的時候,總是非預(yù)期的執(zhí)行代碼?

這種情況一般是被編譯器優(yōu)化了,比如函數(shù)內(nèi)聯(lián)了,編譯出的二進制刪減了無效邏輯、無效參數(shù)。這種情況就會導(dǎo)致你 dlv 單步調(diào)試的時候,總是非預(yù)期的執(zhí)行,或者打印某些變量打印不出來。這種情況解決方法就是:禁止編譯優(yōu)化。

go build -gcflags "-N -l"

總結(jié)

該篇文章系統(tǒng)的分享了 golang 程序調(diào)試的技巧和用法:

  1. 語言工具包里內(nèi)置 tool 工具,支持匯編,反匯編,pprof 分析,符號表查詢等實用功能;
  2. 語言工具包集成單元測試,代碼覆蓋率依賴于單元測試的觸發(fā);
  3. 常用 dlv/gdb 這兩個工具作為大殺器,可以分析二進制,進程,core 文件;

堅持思考,方向比努力更重要。微信公眾號關(guān)注我:奇伢云存儲

掃碼_搜索聯(lián)合傳播樣式-白色版.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容