新聞中心
本文轉(zhuǎn)載自微信公眾號(hào)「 腦子進(jìn)煎魚(yú)了」,作者陳煎魚(yú)。轉(zhuǎn)載本文請(qǐng)聯(lián)系腦子進(jìn)煎魚(yú)了公眾號(hào)。

創(chuàng)新互聯(lián)公司專(zhuān)注為客戶(hù)提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)、五通橋網(wǎng)絡(luò)推廣、小程序制作、五通橋網(wǎng)絡(luò)營(yíng)銷(xiāo)、五通橋企業(yè)策劃、五通橋品牌公關(guān)、搜索引擎seo、人物專(zhuān)訪、企業(yè)宣傳片、企業(yè)代運(yùn)營(yíng)等,從售前售中售后,我們都將竭誠(chéng)為您服務(wù),您的肯定,是我們最大的嘉獎(jiǎng);創(chuàng)新互聯(lián)公司為所有大學(xué)生創(chuàng)業(yè)者提供五通橋建站搭建服務(wù),24小時(shí)服務(wù)熱線:18982081108,官方網(wǎng)址:www.cdcxhl.com
在類(lèi) Unix 系統(tǒng)中,我們常常會(huì)使用 ps 命令來(lái)查看系統(tǒng)當(dāng)前所運(yùn)行的進(jìn)程信息,該命令為我們提供了較大的幫助,能夠快速的定位到某些進(jìn)程的運(yùn)行情況和狀態(tài)。
而在 Go 語(yǔ)言中,也有類(lèi)似的命令工具,那就是 gops[1](Go Process Status)。
gops 是由 Google 官方出品的一個(gè)命令行工具,與 ps 命令的功能類(lèi)似,能夠查看并診斷當(dāng)前系統(tǒng)中 Go 程序的運(yùn)行狀態(tài)及內(nèi)部情況,在一些使用場(chǎng)景中具有較大的存在意義,屬于常用工具。
在本文中我們將對(duì) gops 進(jìn)行全面的使用和介紹。
基本使用
我們先創(chuàng)建一個(gè)示例項(xiàng)目,然后在項(xiàng)目根目錄執(zhí)行下述模塊安裝命令:
- $ go get -u github.com/google/gops
寫(xiě)入如下啟動(dòng)代碼:
- import (
- ...
- "github.com/google/gops/agent"
- )
- func main() {
- // 創(chuàng)建并監(jiān)聽(tīng) gops agent,gops 命令會(huì)通過(guò)連接 agent 來(lái)讀取進(jìn)程信息
- // 若需要遠(yuǎn)程訪問(wèn),可配置 agent.Options{Addr: "0.0.0.0:6060"},否則默認(rèn)僅允許本地訪問(wèn)
- if err := agent.Listen(agent.Options{}); err != nil {
- log.Fatalf("agent.Listen err: %v", err)
- }
- http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
- _, _ = w.Write([]byte(`Go語(yǔ)言編程之旅`))
- })
- _ := http.ListenAndServe(":6060", http.DefaultServeMux)
- }
在完成示例啟動(dòng)代碼的寫(xiě)入后,我們啟動(dòng)該程序,并在命令行執(zhí)行 gops 命令進(jìn)行查看:
- 3739 3725 main * go1.14 /private/var/folders/jm/.../b001/exe/main
- 3725 71093 go go1.14 /usr/local/Cellar/go/1.14/libexec/bin/go
- 62357 46131 go go1.14 /usr/local/Cellar/go/1.14/libexec/bin/go
- 3872 3742 gops go1.14 /Users/eddycjy/go/bin/gops
- 62379 62357 main go1.14 /private/var/folders/jm/.../b001/exe/main
- ...
在上述輸出中,你很快就發(fā)現(xiàn)有一點(diǎn)不一樣,那就是為什么某一行的輸出結(jié)果中會(huì)包含一個(gè) * 符號(hào),如下:
- 3739 3725 main * go1.14 /private/var/folders/jm/.../b001/exe/main
這實(shí)際上代表著該 Go 進(jìn)程,包含了 agent,因此它可以啟用更強(qiáng)大的診斷功能,包括當(dāng)前堆棧跟蹤,Go版本,內(nèi)存統(tǒng)計(jì)信息等等。
在最后也有一個(gè) main 的 Go 進(jìn)程,它不包含 * 符號(hào),這意味著它是一個(gè)普通的 Go 程序,也就是沒(méi)有植入 agent,只能使用最基本的功能。
常規(guī)命令
gops 工具包含了大量的分析命令,我們可以通過(guò) gops help 進(jìn)行查看:
- $ gops help
- gops is a tool to list and diagnose Go processes.
- Usage:
- gops
... - gops
# displays process info - gops help # displays this help message
- Commands:
- stack Prints the stack trace.
- gc Runs the garbage collector and blocks until successful.
- setgc Sets the garbage collection target percentage.
- memstats Prints the allocation and garbage collection stats.
- version Prints the Go version used to build the program.
- stats Prints runtime stats.
- trace Runs the runtime tracer for 5 secs and launches "go tool trace".
- pprof-heap Reads the heap profile and launches "go tool pprof".
- pprof-cpu Reads the CPU profile and launches "go tool pprof".
在接下來(lái)的小節(jié)中,我們將針對(duì)幾個(gè)常用的分析功能進(jìn)行概要分析。
查看指定進(jìn)程信息
- $ gops
- parent PID: 3725
- threads: 7
- memory usage: 0.042%
- cpu usage: 0.003%
- username: eddycjy
- cmd+args: /var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/go-build943691423/b001/exe/main
- elapsed time: 10:56
- local/remote: 127.0.0.1:59369 <-> :0 (LISTEN)
- local/remote: *:6060 <-> :0 (LISTEN)
獲取 Go 進(jìn)程的概要信息,包括父級(jí)PID、線程數(shù)、內(nèi)存/CPU使用率、運(yùn)行者的賬戶(hù)名、進(jìn)程的啟動(dòng)命令行參數(shù)、啟動(dòng)后所經(jīng)過(guò)的時(shí)間以及 gops 的 agent 監(jiān)聽(tīng)信息(若無(wú)植入 agent,則沒(méi)有這項(xiàng)信息)。
查看調(diào)用棧信息
- $ gops stack 3739
- goroutine 19 [running]:
- runtime/pprof.writeGoroutineStacks(0x1385aa0, 0xc000132038, 0x30, 0xd0)
- ...
- /Users/eddycjy/go/src/github.com/google/gops/agent/agent.go:185 +0x1af
- github.com/google/gops/agent.listen()
- /Users/eddycjy/go/src/github.com/google/gops/agent/agent.go:133 +0x2bf
- created by github.com/google/gops/agent.Listen
- /Users/eddycjy/go/src/github.com/google/gops/agent/agent.go:111 +0x36b
- goroutine 1 [IO wait]:
- internal/poll.runtime_pollWait(0x2f55e38, 0x72, 0x0)
- /usr/local/Cellar/go/1.14/libexec/src/runtime/netpoll.go:203 +0x55
- ...
獲取對(duì)應(yīng)進(jìn)程的代碼調(diào)用堆棧信息,可用于分析調(diào)用鏈路。
查看內(nèi)存使用情況
- $ gops memstats 3739
- alloc: 1.15MB (1205272 bytes)
- total-alloc: 1.15MB (1205272 bytes)
- sys: 69.45MB (72827136 bytes)
- lookups: 0
- mallocs: 644
- frees: 12
- heap-alloc: 1.15MB (1205272 bytes)
- heap-sys: 63.66MB (66748416 bytes)
- heap-idle: 62.05MB (65060864 bytes)
- heap-in-use: 1.61MB (1687552 bytes)
- heap-released: 62.02MB (65028096 bytes)
- heap-objects: 632
- ...
獲取 Go 在運(yùn)行時(shí)的當(dāng)前內(nèi)存使用情況,主要是 runtime.MemStats[2] 的相關(guān)字段信息。
查看運(yùn)行時(shí)信息
- $ gops stats 3739
- goroutines: 2
- OS threads: 8
- GOMAXPROCS: 4
- num CPU: 4
獲取 Go 運(yùn)行時(shí)的基本信息,包括當(dāng)前的 Goroutine 數(shù)量、系統(tǒng)線程、GOMAXPROCS 數(shù)值以及當(dāng)前系統(tǒng)的 CPU 核數(shù)。
查看 trace 信息
- $ gops trace 3739
- Tracing now, will take 5 secs...
- Trace dump saved to: /var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/trace092133110
- Parsing trace...
- Splitting trace...
- Opening browser. Trace viewer is listening on http://127.0.0.1:53811
與 go tool trace 作用基本一致。
查看 profile 信息
- $ gops pprof-cpu 3739
- Profiling CPU now, will take 30 secs...
- Profile dump saved to: /var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/profile563685966
- Binary file saved to: /var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/binary265411413
- File: binary265411413
- Type: cpu
- ...
- (pprof)
- $ gops pprof-heap 3739
- Profile dump saved to: /var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/profile967076057
- Binary file saved to: /var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/binary904879716
- File: binary904879716
- Type: inuse_space
- ...
- (pprof)
與 go tool pprof 作用基本一致。
你怎么知道我是誰(shuí)
在學(xué)習(xí)了 gops 的使用后,我們突然發(fā)現(xiàn)一個(gè)問(wèn)題,那就是 gops 是怎么知道哪些進(jìn)程是與 Go 相關(guān)的進(jìn)程?
如果是植入了 agent 的應(yīng)用程序還好說(shuō),可以理解為埋入了識(shí)別點(diǎn)。但實(shí)際情況是,沒(méi)有植入 agent 的 Go 程序也被識(shí)別到了,說(shuō)明 gops 本身并不是這么實(shí)現(xiàn)的,考慮植入agent 應(yīng)當(dāng)只是用于診斷信息的拓展使用,并不是一個(gè)識(shí)別點(diǎn),那么 gops 到底是怎么發(fā)現(xiàn)哪些進(jìn)程是 Go 相關(guān)的呢?
我們回歸問(wèn)題的前置需求,假設(shè)我們想知道哪些進(jìn)程與 Go 相關(guān),那么第一步我們要先知道我們當(dāng)前系統(tǒng)中都運(yùn)行了哪些進(jìn)程,這些記錄在哪里有?
認(rèn)真思考一下,答案也就呼之欲出了,假設(shè)是 Linux 相關(guān)的系統(tǒng)下,其會(huì)將進(jìn)程所有的相關(guān)信息都按照約定的數(shù)據(jù)結(jié)構(gòu)寫(xiě)入 /proc 目錄下,因此我們有充分的懷疑認(rèn)為 gops 就是從 /proc 目錄下讀取到相關(guān)信息的,源代碼如下:
- func PidsWithContext(ctx context.Context) ([]int32, error) {
- var ret []int32
- d, err := os.Open(common.HostProc())
- if err != nil {
- return nil, err
- }
- defer d.Close()
- fnames, err := d.Readdirnames(-1)
- if err != nil {
- return nil, err
- }
- for _, fname := range fnames {
- pid, err := strconv.ParseInt(fname, 10, 32)
- if err != nil {
- continue
- }
- ret = append(ret, int32(pid))
- }
- return ret, nil
- }
- // common.HostProc
- func HostProc(combineWith ...string) string {
- return GetEnv("HOST_PROC", "/proc", combineWith...)
- }
在上述代碼中,該方法通過(guò)調(diào)用 os.Open 方法打開(kāi)了 proc 目錄,并利用 Readdirnames 方法對(duì)該目錄進(jìn)行了掃描,最終獲取到了所有需要 pid,最終完成其使命,返回了所有 pid。
在確定了 gops 是通過(guò)掃描 /proc 目錄得到的進(jìn)程信息后,我們又遇到了一個(gè)新的疑問(wèn)點(diǎn),那就是 gops 是怎么確定這個(gè)進(jìn)程是 Go 進(jìn)程,又怎么知道它的具體版本信息的呢,源代碼如下:
- func isGo(pr ps.Process) (path, version string, agent, ok bool, err error) {
- ...
- path, _ = pr.Path()
- if err != nil {
- return
- }
- var versionInfo goversion.Version
- versionInfo, err = goversion.ReadExe(path)
- if err != nil {
- return
- }
- ok = true
- version = versionInfo.Release
- pidfile, err := internal.PIDFile(pr.Pid())
- if err == nil {
- _, err := os.Stat(pidfile)
- agent = err == nil
- }
- return path, version, agent, ok, nil
- }
我們可以看到該方法的主要作用是根據(jù)掃描 /proc 目錄所得到的二進(jìn)制文件地址中查找相關(guān)的標(biāo)識(shí),用于判斷其是否 Go 程序,如果是 Go 程序,那么它將會(huì)返回該進(jìn)程的 pid、二進(jìn)制文件的名稱(chēng)以及二進(jìn)制文件的完整存儲(chǔ)路徑,判斷的標(biāo)識(shí)如下:
- if name == "runtime.main" || name == "main.main" {
- isGo = true
- }
- if name == "runtime.buildVersion" {
- isGo = true
- }
而關(guān)于所編譯的 Go 語(yǔ)言的版本,Go 編譯器會(huì)在二進(jìn)制文件中打入 runtime.buildVersion標(biāo)識(shí),這個(gè)標(biāo)識(shí)能夠快速我們快速識(shí)別它的編譯信息,而 gops 也正正是利用了這一點(diǎn)。
我們可以利用 gdb 來(lái)進(jìn)行查看 Go 所編譯的二進(jìn)制文件的版本信息,如下:
- $ export GOFLAGS="-ldflags=-compressdwarf=false" && go build .
- $ gdb awesomeProject
- ...
- (gdb) p 'runtime.buildVersion'
- $1 = 0x131bbb0 "go1.14"
在上述輸出中,我們先對(duì)示例項(xiàng)目進(jìn)行了編譯,然后利用 gdb 中查看了 runtime.buildVersion 變量,最終可得知編譯這個(gè) Go 程序的版本是 Go1.14。
但在編譯時(shí),有一點(diǎn)需要注意,就是我們?cè)诰幾g時(shí)指定了 export GOFLAGS="-ldflags=-compressdwarf=false" 參數(shù)。
如果不進(jìn)行指定的話(huà),就會(huì)出現(xiàn) Reading symbols from awesomeProject...(no debugging symbols found)...done. 的相關(guān)報(bào)錯(cuò),會(huì)將會(huì)影響部分功能使用。
這是因?yàn)樵?Go1.11 版本開(kāi)始,進(jìn)行了調(diào)試信息的壓縮,目的是為了減小所編譯的二進(jìn)制文件大小,但 Mac 上的 gdb 無(wú)法理解壓縮的 DWARF,因此會(huì)產(chǎn)生問(wèn)題。
需要進(jìn)行指定在調(diào)試時(shí)不進(jìn)行 DWARF 的壓縮,便于 Mac 上的 gdb 使用。
需要注意的一點(diǎn)
假設(shè)我們?cè)谝恍┨厥鈭?chǎng)景下希望對(duì) Go 所編譯的二進(jìn)制文件進(jìn)行壓縮,那么在最后我們常常會(huì)使用到 upx 工具來(lái)減少其整體大小,命令如下:
- $ upx awesomeProject
這時(shí)候我們?cè)僦匦逻\(yùn)行所編譯的 awesomeProject 文件,這時(shí)候需要思考的是,gops 能不能識(shí)別到它是一個(gè) Go 程序呢?
答案是不行的,經(jīng)過(guò) upx 壓縮后的二進(jìn)制文件將無(wú)法被識(shí)別為 Go 程序,并且在我所使用的 gops v0.3.7版本中,由于這類(lèi)加殼進(jìn)程的存在,執(zhí)行 gops 命令直接出現(xiàn)了空指針調(diào)用的恐慌(panic),顯然,這是一個(gè) BUG,大家在實(shí)際環(huán)境中需要多加留意,如果要使用 gops 則盡量不要使用 upx 進(jìn)行壓縮。
總結(jié)
在本文中我們針對(duì) Google 官方出品的 gops 進(jìn)行了基本使用和原理性的部分剖析。
如果你仔細(xì)研讀了,就會(huì)發(fā)現(xiàn)其實(shí) gops 幾乎包含了大部分 Go 剖析工具的功能,是名副其實(shí)的進(jìn)程診斷工具。
gops 集成了大量 Go 業(yè)界中常用的分析鏈,在排查問(wèn)題上也會(huì)非常的方便,不需要一個(gè)個(gè)單獨(dú)找特定工具在哪里,只需要使用 gops 即可,而更深層次的使用可以根據(jù)實(shí)際情況進(jìn)行更一步的了解。
參考資料
[1]gops: https://github.com/google/gops
[2]runtime.MemStats: https://golang.org/pkg/runtime/#MemStats
本文標(biāo)題:必須要學(xué)的Go進(jìn)程診斷工具gops
鏈接分享:http://fisionsoft.com.cn/article/cdhhejh.html


咨詢(xún)
建站咨詢(xún)
