詳解如何保留Go程序崩潰現(xiàn)場(chǎng)
引言
沒(méi)有消滅一切的銀彈,也沒(méi)有可以保證永不出錯(cuò)的程序。我們應(yīng)當(dāng)如何捕捉 Go 程序錯(cuò)誤?我想同學(xué)們的第一反應(yīng)是:打日志。
但錯(cuò)誤日志的能力是有限的。第一,日志是開(kāi)發(fā)者在代碼中定義的打印信息,我們沒(méi)法保證日志信息能包含所有的錯(cuò)誤情況。第二,在 Go 程序中發(fā)生 panic 時(shí),我們也并不總是能通過(guò) recover 捕獲(沒(méi)法插入日志代碼)。
那線(xiàn)上 Go 程序突然莫名崩潰后,當(dāng)日志記錄沒(méi)有覆蓋到錯(cuò)誤場(chǎng)景時(shí),還有別的方法排查嗎?
core dump
core dump 又即核心轉(zhuǎn)儲(chǔ),簡(jiǎn)單來(lái)說(shuō)它就是程序意外終止時(shí)產(chǎn)生的內(nèi)存快照。我們可以通過(guò) core dump 文件來(lái)調(diào)式程序,找出其崩潰原因。
在 linux 平臺(tái)上,可通過(guò)ulimit -c
命令查看核心轉(zhuǎn)儲(chǔ)配置,系統(tǒng)默認(rèn)為 0,表明未開(kāi)啟 core dump 記錄功能。
$ ulimit -c 0
可以使用ulimit -c [size]
命令指定記錄 core dump 文件的大小,即是開(kāi)啟 core dump 記錄。當(dāng)然,如果電腦資源足夠,避免 core dump 丟失或記錄不全,也可執(zhí)行ulimit -c unlimited
而不限制 core dump 文件大小。
那在 Go 程序中,如何開(kāi)啟 core dump 呢?
GOTRACEBACK
我們?cè)?a href="http://www.dbjr.com.cn/article/262965.htm" target="_blank">你真的懂string與[]byte的轉(zhuǎn)換了嗎一文中探討過(guò) string 轉(zhuǎn) []byte 的黑魔法,如下例所示。
package main import ( "reflect" "unsafe" ) func String2Bytes(s string) []byte { sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh := reflect.SliceHeader{ Data: sh.Data, Len: sh.Len, Cap: sh.Len, } return *(*[]byte)(unsafe.Pointer(&bh)) } func Modify() { a := "hello" b := String2Bytes(a) b[0] = 'H' } func main() { Modify() }
string 是不可以被修改的,當(dāng)我們將 string 類(lèi)型通過(guò)黑魔法轉(zhuǎn)為 []byte 后,企圖修改其值,程序會(huì)發(fā)生一個(gè)不能被 recover 捕獲到的錯(cuò)誤。
$ go run main.go unexpected fault address 0x106a6a4 fatal error: fault [signal SIGBUS: bus error code=0x2 addr=0x106a6a4 pc=0x105b01a] goroutine 1 [running]: runtime.throw({0x106a68b, 0x0}) /usr/local/go/src/runtime/panic.go:1198 +0x71 fp=0xc000092ee8 sp=0xc000092eb8 pc=0x102bad1 runtime.sigpanic() /usr/local/go/src/runtime/signal_unix.go:732 +0x1d6 fp=0xc000092f38 sp=0xc000092ee8 pc=0x103f2f6 main.Modify(...) /Users/slp/github/PostDemo/coreDemo/main.go:21 main.main() /Users/slp/github/PostDemo/coreDemo/main.go:25 +0x5a fp=0xc000092f80 sp=0xc000092f38 pc=0x105b01a runtime.main() /usr/local/go/src/runtime/proc.go:255 +0x227 fp=0xc000092fe0 sp=0xc000092f80 pc=0x102e167 runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:1581 +0x1 fp=0xc000092fe8 sp=0xc000092fe0 pc=0x1052dc1 exit status 2
這些堆棧信息是由 GOTRACEBACK 變量來(lái)控制打印粒度的,它有五種級(jí)別。
- none,不顯示任何 goroutine 堆棧信息
- single,默認(rèn)級(jí)別,顯示當(dāng)前 goroutine 堆棧信息
- all,顯示所有 user (不包括 runtime)創(chuàng)建的 goroutine 堆棧信息
- system,顯示所有 user + runtime 創(chuàng)建的 goroutine 堆棧信息
- crash,和 system 打印一致,但會(huì)生成 core dump 文件(Unix 系統(tǒng)上,崩潰會(huì)引發(fā) SIGABRT 以觸發(fā)core dump)
如果我們將 GOTRACEBACK 設(shè)置為 system ,我們將看到程序崩潰時(shí)所有 goroutine 狀態(tài)信息
$ GOTRACEBACK=system go run main.go unexpected fault address 0x106a6a4 fatal error: fault [signal SIGBUS: bus error code=0x2 addr=0x106a6a4 pc=0x105b01a] goroutine 1 [running]: runtime.throw({0x106a68b, 0x0}) ... goroutine 2 [force gc (idle)]: runtime.gopark(0x0, 0x0, 0x0, 0x0, 0x0) ... created by runtime.init.7 /usr/local/go/src/runtime/proc.go:294 +0x25 goroutine 3 [GC sweep wait]: runtime.gopark(0x0, 0x0, 0x0, 0x0, 0x0) ... created by runtime.gcenable /usr/local/go/src/runtime/mgc.go:181 +0x55 goroutine 4 [GC scavenge wait]: runtime.gopark(0x0, 0x0, 0x0, 0x0, 0x0) ... created by runtime.gcenable /usr/local/go/src/runtime/mgc.go:182 +0x65 exit status 2
如果想獲取 core dump 文件,那么就應(yīng)該把 GOTRACEBACK 的值設(shè)置為 crash 。當(dāng)然,我們還可以通過(guò) runtime/debug 包中的 SetTraceback
方法來(lái)設(shè)置堆棧打印級(jí)別。
delve 調(diào)試
delve 是 Go 語(yǔ)言編寫(xiě)的 Go 程序調(diào)試器,我們可以通過(guò) dlv core
命令來(lái)調(diào)試 core dump。
首先,通過(guò)以下命令安裝 delve
go get -u github.com/go-delve/delve/cmd/dlv
還是以上文中的例子為例,我們通過(guò)設(shè)置 GOTRACEBACK 為 crash 級(jí)別來(lái)獲取 core dump 文件
$ tree . └── main.go $ ulimit -c unlimited $ go build main.go $ GOTRACEBACK=crash ./main ... Aborted (core dumped) $ tree . ├── core ├── main └── main.go $ ls -alh core -rw------- 1 slp slp 41M Oct 31 22:15 core
此時(shí),在同級(jí)目錄得到了 core dump 文件 core(文件名、存儲(chǔ)路徑、是否加上進(jìn)程號(hào)都可以配置修改)。
通過(guò) dlv 調(diào)試器來(lái)調(diào)試 core 文件,執(zhí)行命令格式 dlv core 可執(zhí)行文件名 core文件
$ dlv core main core Type 'help' for list of commands. (dlv)
命令 goroutines
獲取所有 goroutine 相關(guān)信息
(dlv) goroutines * Goroutine 1 - User: ./main.go:21 main.main (0x45b81a) (thread 18061) Goroutine 2 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x42ed96) [force gc (idle)] Goroutine 3 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x42ed96) [GC sweep wait] Goroutine 4 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x42ed96) [GC scavenge wait] [4 goroutines] (dlv)
Goroutine 1 是出問(wèn)題的 goroutine (帶有 * 代表當(dāng)前幀),通過(guò)命令 goroutine 1
切換到其棧幀
(dlv) goroutine 1 Switched from 1 to 1 (thread 18061) (dlv)
執(zhí)行命令 bt
(breakpoints trace) 查看當(dāng)前的棧幀詳細(xì)信息
(dlv) bt 0 0x0000000000454bc1 in runtime.raise at /usr/local/go/src/runtime/sys_linux_amd64.s:165 1 0x0000000000452f60 in runtime.systemstack_switch at /usr/local/go/src/runtime/asm_amd64.s:350 2 0x000000000042c530 in runtime.fatalthrow at /usr/local/go/src/runtime/panic.go:1250 3 0x000000000042c2f1 in runtime.throw at /usr/local/go/src/runtime/panic.go:1198 4 0x000000000043fa76 in runtime.sigpanic at /usr/local/go/src/runtime/signal_unix.go:742 5 0x000000000045b81a in main.Modify at ./main.go:21 6 0x000000000045b81a in main.main at ./main.go:25 7 0x000000000042e9c7 in runtime.main at /usr/local/go/src/runtime/proc.go:255 8 0x0000000000453361 in runtime.goexit at /usr/local/go/src/runtime/asm_amd64.s:1581 (dlv)
通過(guò) 5 0x000000000045b81a in main.Modify
發(fā)現(xiàn)了錯(cuò)誤代碼所在函數(shù),執(zhí)行命令 frame 5
進(jìn)入函數(shù)具體代碼
(dlv) frame 5 > runtime.raise() /usr/local/go/src/runtime/sys_linux_amd64.s:165 (PC: 0x454bc1) Warning: debugging optimized function Frame 5: ./main.go:21 (PC: 45b81a) 16: } 17: 18: func Modify() { 19: a := "hello" 20: b := String2Bytes(a) => 21: b[0] = 'H' 22: } 23: 24: func main() { 25: Modify() 26: } (dlv)
自此,破案了,問(wèn)題就出在了擅自修改 string 底層值。
Mac 不能使用
有一點(diǎn)需要注意,上文 core dump 生成的例子,我是在 linux 系統(tǒng)下完成的,mac amd64 系統(tǒng)沒(méi)法弄(很氣,害我折騰了兩個(gè)晚上)。
這是由于 mac 系統(tǒng)下的 Go 限制了生成 core dump 文件,這個(gè)在 Go 源碼 src/runtime/signal_unix.go 中有相關(guān)說(shuō)明。
//go:nosplit func crash() { // OS X core dumps are linear dumps of the mapped memory, // from the first virtual byte to the last, with zeros in the gaps. // Because of the way we arrange the address space on 64-bit systems, // this means the OS X core file will be >128 GB and even on a zippy // workstation can take OS X well over an hour to write (uninterruptible). // Save users from making that mistake. if GOOS == "darwin" && GOARCH == "amd64" { return } dieFromSignal(_SIGABRT) }
總結(jié)
core dump 文件是操作系統(tǒng)提供給我們的一把利器,它是程序意外終止時(shí)產(chǎn)生的內(nèi)存快照。利用 core dump,我們可以在程序崩潰后更好地恢復(fù)事故現(xiàn)場(chǎng),為故障排查保駕護(hù)航。
當(dāng)然,core dump 文件的生成也是有弊端的。core dump 文件較大,如果線(xiàn)上服務(wù)本身內(nèi)存占用就很高,那在生成 core dump 文件上的內(nèi)存與時(shí)間開(kāi)銷(xiāo)都會(huì)很大。另外,我們往往會(huì)布置服務(wù)守護(hù)進(jìn)程,如果我們的程序頻繁崩潰和重啟,那會(huì)生成大量的 core dump 文件(設(shè)定了core+pid 命名規(guī)則),產(chǎn)生磁盤(pán)打滿(mǎn)的風(fēng)險(xiǎn)(如果放開(kāi)了內(nèi)核限制 ulimit -c unlimited)。
最后,如果擔(dān)心錯(cuò)誤日志不能幫助我們定位 Go 代碼問(wèn)題,我們可以為它開(kāi)啟 core dump 功能,在 hotfix 上增加奇兵。對(duì)于有守護(hù)進(jìn)程的服務(wù),建議設(shè)置好 ulimt -c 大小限制。
以上就是詳解如何保留Go程序崩潰現(xiàn)場(chǎng)的詳細(xì)內(nèi)容,更多關(guān)于Go程序崩潰保留的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Go語(yǔ)言使用defer+recover解決panic導(dǎo)致程序崩潰的問(wèn)題
- goland安裝1.7版本報(bào)錯(cuò)Unpacked?SDK?is?corrupted解決
- 執(zhí)行g(shù)o?build報(bào)錯(cuò)go:?go.mod?file?not?found?in?current?directory?or?any?parent?directory
- go?mode?tidy出現(xiàn)報(bào)錯(cuò)go:?warning:?“all“?matched?no?packages的解決方法
- Golang運(yùn)行報(bào)錯(cuò)找不到包:package?xxx?is?not?in?GOROOT的解決過(guò)程
相關(guān)文章
詳解go語(yǔ)言單鏈表及其常用方法的實(shí)現(xiàn)
這篇文章主要介紹了詳解go語(yǔ)言單鏈表及其常用方法的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11Go語(yǔ)言實(shí)現(xiàn)的web爬蟲(chóng)實(shí)例
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)的web爬蟲(chóng),實(shí)例分析了web爬蟲(chóng)的原理與Go語(yǔ)言的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02golang簡(jiǎn)單獲取上傳文件大小的實(shí)現(xiàn)代碼
這篇文章主要介紹了golang簡(jiǎn)單獲取上傳文件大小的方法,涉及Go語(yǔ)言文件傳輸及文件屬性操作的相關(guān)技巧,需要的朋友可以參考下2016-07-07Golang中如何對(duì)MySQL進(jìn)行操作詳解
這篇文章主要給大家介紹了關(guān)于在Golang中如何對(duì)MySQL進(jìn)行操作的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用Golang具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03Go?net?http超時(shí)應(yīng)用場(chǎng)景全面詳解
HTTP是一個(gè)復(fù)雜的多階段協(xié)議,因此沒(méi)有一個(gè)一刀切的超時(shí)解決方案,在這篇文章中,我將分解您可能需要應(yīng)用超時(shí)的各個(gè)階段,并研究在服務(wù)器端和客戶(hù)端上執(zhí)行超時(shí)的不同方法2024-01-01