<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 8825字,需瀏覽 18分鐘

           ·

          2021-03-31 16:14

          點(diǎn)擊上方“Go編程時光”,選擇“加為星標(biāo)

          一時間關(guān)注Go技術(shù)干貨!


          140d9756bbdd41739b874b34b2730bc3.webp


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

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

          Golang tools

          golang 從語言原生層面就集成了大量的實(shí)用工具,這些都是 Robert Griesemer, Rob Pike, Ken Thompson 這幾位大神經(jīng)驗(yàn)沉淀下的精華。你安裝好 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:反匯編工具,分析二進(jìn)制文件(等同于系統(tǒng) objdump 命令)
          • pprof:指標(biāo),性能分析工具
          • cover:生成代碼覆蓋率
          • trace:采樣一段時間,指標(biāo)跟蹤分析工具
          • compile:代碼匯編

          nm

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

          #?exmple?為你編譯的二進(jìn)制文件
          go?tool?nm?./example

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

          c4ee82468b3c601936dc16860b9da364.webp

          compile

          匯編某個文件

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

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

          objdump

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

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

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

          pprof

          pprof 支持四種類型的分析:

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

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

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

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

          分析的形式有兩種:

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

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

          我寫了一個 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 信息表明了這么幾點(diǎn)信息:

          • 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 的形式打開。


          178fa801ad8f79f227f00691fe9c29f8.webp

          單元測試

          8cb246073b3765daf0998264b5a38717.webp


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

          執(zhí)行單元測試

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

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

          go test 運(yùn)行

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

          編譯,運(yùn)行

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

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

          //?先編譯出?.test?文件
          $?go?test?-c?
          //?指定跑某一個文件
          $?./raftexample.test?-test.timeout=10m0s?-test.v=true?-test.run=TestPutAndGetKeyValue

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

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

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

          golang 的代碼覆蓋率是基于單測的,由單測作為出發(fā)點(diǎn),來看你的業(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ù)的代碼覆蓋率。


          178fa801ad8f79f227f00691fe9c29f8.webp

          程序 Debug

          8cb246073b3765daf0998264b5a38717.webp


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

          1. dlv
          2. gdb

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

          dlv 調(diào)試用法

          調(diào)試二進(jìn)制

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

          舉例:

          dlv?exec?./example

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

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

          調(diào)試進(jìn)程

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

          進(jìn)程號是必選的。

          舉例:

          dlv?attach?12808?./example

          調(diào)試 core 文件

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

          dlv?core?<executable>?<core>?[flags]
          dlv?core?./example?core.277282

          調(diào)試常用語法

          系統(tǒng)整理

          程序運(yùn)行

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

          斷點(diǎn)相關(guān)

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

          信息打印

          • args : 打印程序的傳參
          • examinemem (alias: x) ?:這個是神器,解析內(nèi)存用的,和 gdb 的 x 命令一樣;
          • locals :打印本地變量
          • print (alias: p) :打印一個表達(dá)式,或者變量
          • 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

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

          條件斷點(diǎn)

          #?先斷點(diǎn)
          (dlv)?b?

          #?查看斷點(diǎn)信息
          (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 來擴(kuò)展的,所以功能非常有限。gdb 只能做到最基本的變量打印,卻理解不了 golang 的一些特殊類型,比如 channel,map,slice 等,gdb 原生是無法調(diào)適 goroutine 協(xié)程的,因?yàn)檫@個是用戶態(tài)的調(diào)度單位,gdb 只能理解線程。所以只能通過 python 腳本的擴(kuò)展,把協(xié)程結(jié)構(gòu)按照鏈表輸出出來,支持的命令:

          39f542d4c694701117d256003194cdb8.webp

          gdb當(dāng)前只支持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;強(qiáng)制轉(zhuǎn)換接口到動態(tài)類型。

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

          (gdb)?p?'runtime.firstmoduledata'

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

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

          (gdb)?p?$len(xxx)

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


          178fa801ad8f79f227f00691fe9c29f8.webp

          小技巧

          8cb246073b3765daf0998264b5a38717.webp


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

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

          8693a415822167786cdf1fb5e0fa7260.webp
          062c765e1b1419b5c547c316ecccbca5.webp

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

          在你的代碼里添加一行:

          debug.PrintStack()

          這樣就能當(dāng)前代碼位置的堆棧給打印出來,這樣你就直到怎么函數(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é)束,你想找到瓶頸點(diǎn),那么通常會使用到這個方式。

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

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

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

          go?build?-gcflags?"-N?-l"


          178fa801ad8f79f227f00691fe9c29f8.webp

          總結(jié)

          8cb246073b3765daf0998264b5a38717.webp


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

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

          ? ?

          --?END?--


          喜歡明哥文章的同學(xué)歡迎長按下圖訂閱!

          ???

          瀏覽 66
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  不用播放器在线观看A | 欧美成人在线免费 | 亚洲色情免费电影 | 丁香五月六月 | 看国产片日逼的 |