如何保留 Go 程序崩潰現(xiàn)場
沒有消滅一切的銀彈,也沒有可以保證永不出錯的程序。我們應當如何捕捉 Go 程序錯誤?我想同學們的第一反應是:打日志。
但錯誤日志的能力是有限的。第一,日志是開發(fā)者在代碼中定義的打印信息,我們沒法保證日志信息能包含所有的錯誤情況。第二,在 Go 程序中發(fā)生 panic 時,我們也并不總是能通過 recover 捕獲(沒法插入日志代碼)。
那線上 Go 程序突然莫名崩潰后,當日志記錄沒有覆蓋到錯誤場景時,還有別的方法排查嗎?
core dump
core dump 又即核心轉儲,簡單來說它就是程序意外終止時產生的內存快照。我們可以通過 core dump 文件來調式程序,找出其崩潰原因。
在 linux 平臺上,可通過ulimit -c命令查看核心轉儲配置,系統(tǒng)默認為 0,表明未開啟 core dump 記錄功能。
$?ulimit?-c
0
可以使用ulimit -c [size]命令指定記錄 core dump 文件的大小,即是開啟 core dump 記錄。當然,如果電腦資源足夠,避免 core dump 丟失或記錄不全,也可執(zhí)行ulimit -c unlimited而不限制 core dump 文件大小。
那在 Go 程序中,如何開啟 core dump 呢?
GOTRACEBACK
我們在你真的懂string與[]byte的轉換了嗎一文中探討過 string 轉 []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 是不可以被修改的,當我們將 string 類型通過黑魔法轉為 []byte 后,企圖修改其值,程序會發(fā)生一個不能被 recover 捕獲到的錯誤。
$?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 變量來控制打印粒度的,它有五種級別。
none,不顯示任何 goroutine 堆棧信息 single,默認級別,顯示當前 goroutine 堆棧信息 all,顯示所有 user (不包括 runtime)創(chuàng)建的 goroutine 堆棧信息 system,顯示所有 user + runtime 創(chuàng)建的 goroutine 堆棧信息 crash,和 system 打印一致,但會生成 core dump 文件(Unix 系統(tǒng)上,崩潰會引發(fā) SIGABRT 以觸發(fā)core dump)
如果我們將 GOTRACEBACK 設置為 system ,我們將看到程序崩潰時所有 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 文件,那么就應該把 GOTRACEBACK 的值設置為 crash 。當然,我們還可以通過 runtime/debug 包中的 SetTraceback 方法來設置堆棧打印級別。
delve 調試
delve 是 Go 語言編寫的 Go 程序調試器,我們可以通過 dlv core 命令來調試 core dump。
首先,通過以下命令安裝 delve
go?get?-u?github.com/go-delve/delve/cmd/dlv
還是以上文中的例子為例,我們通過設置 GOTRACEBACK 為 crash 級別來獲取 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
此時,在同級目錄得到了 core dump 文件 core(文件名、存儲路徑、是否加上進程號都可以配置修改)。
通過 dlv 調試器來調試 core 文件,執(zhí)行命令格式 dlv core 可執(zhí)行文件名 core文件
$?dlv?core?main?core
Type?'help'?for?list?of?commands.
(dlv)
命令 goroutines 獲取所有 goroutine 相關信息
(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 是出問題的 goroutine (帶有 * 代表當前幀),通過命令 goroutine 1 切換到其棧幀
(dlv)?goroutine?1
Switched?from?1?to?1?(thread?18061)
(dlv)
執(zhí)行命令 bt(breakpoints trace) 查看當前的棧幀詳細信息
(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)
通過 5 0x000000000045b81a in main.Modify 發(fā)現(xiàn)了錯誤代碼所在函數(shù),執(zhí)行命令 frame 5 進入函數(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)
自此,破案了,問題就出在了擅自修改 string 底層值。
Mac 不能使用
有一點需要注意,上文 core dump 生成的例子,我是在 linux 系統(tǒng)下完成的,mac amd64 系統(tǒng)沒法弄(很氣,害我折騰了兩個晚上)。
這是由于 mac 系統(tǒng)下的 Go 限制了生成 core dump 文件,這個在 Go 源碼 src/runtime/signal_unix.go 中有相關說明。
//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)
}
總結
core dump 文件是操作系統(tǒng)提供給我們的一把利器,它是程序意外終止時產生的內存快照。利用 core dump,我們可以在程序崩潰后更好地恢復事故現(xiàn)場,為故障排查保駕護航。
當然,core dump 文件的生成也是有弊端的。core dump 文件較大,如果線上服務本身內存占用就很高,那在生成 core dump 文件上的內存與時間開銷都會很大。另外,我們往往會布置服務守護進程,如果我們的程序頻繁崩潰和重啟,那會生成大量的 core dump 文件(設定了core+pid 命名規(guī)則),產生磁盤打滿的風險(如果放開了內核限制 ulimit -c unlimited)。
最后,如果擔心錯誤日志不能幫助我們定位 Go 代碼問題,我們可以為它開啟 core dump 功能,在 hotfix 上增加奇兵。對于有守護進程的服務,建議設置好 ulimt -c 大小限制。
推薦閱讀
