如何保留 Go 程序崩溃现场

时间:2022-05-23 00:08:17

如何保留 Go 程序崩溃现场

没有消灭一切的银弹,也没有可以保证永不出错的程序。我们应当如何捕捉 Go 程序错误?我想同学们的第一反应是:打日志

但错误日志的能力是有限的。第一,日志是开发者在代码中定义的打印信息,我们没法保证日志信息能包含所有的错误情况。第二,在 Go 程序中发生 panic 时,我们也并不总是能通过 recover 捕获(没法插入日志代码)。

那线上 Go 程序突然莫名崩溃后,当日志记录没有覆盖到错误场景时,还有别的方法排查吗?

core dump

core dump 又即核心转储,简单来说它就是程序意外终止时产生的内存快照。我们可以通过 core dump 文件来调式程序,找出其崩溃原因。

在 linux 平台上,可通过ulimit -c命令查看核心转储配置,系统默认为 0,表明未开启 core dump 记录功能。

  1. $ulimit-c
  2. 0

可以使用ulimit -c [size]命令指定记录 core dump 文件的大小,即是开启 core dump 记录。当然,如果电脑资源足够,避免 core dump 丢失或记录不全,也可执行ulimit -c unlimited而不限制 core dump 文件大小。

那在 Go 程序中,如何开启 core dump 呢?

GOTRACEBACK

我们在你真的懂string与[]byte的转换了吗一文中探讨过 string 转 []byte 的黑魔法,如下例所示。

  1. packagemain
  2. import(
  3. "reflect"
  4. "unsafe"
  5. )
  6. funcString2Bytes(sstring)[]byte{
  7. sh:=(*reflect.StringHeader)(unsafe.Pointer(&s))
  8. bh:=reflect.SliceHeader{
  9. Data:sh.Data,
  10. Len:sh.Len,
  11. Cap:sh.Len,
  12. }
  13. return*(*[]byte)(unsafe.Pointer(&bh))
  14. }
  15. funcModify(){
  16. a:="hello"
  17. b:=String2Bytes(a)
  18. b[0]='H'
  19. }
  20. funcmain(){
  21. Modify()
  22. }

string 是不可以被修改的,当我们将 string 类型通过黑魔法转为 []byte 后,企图修改其值,程序会发生一个不能被 recover 捕获到的错误。

  1. $gorunmain.go
  2. unexpectedfaultaddress0x106a6a4
  3. fatalerror:fault
  4. [signalSIGBUS:buserrorcode=0x2addr=0x106a6a4pc=0x105b01a]
  5. goroutine1[running]:
  6. runtime.throw({0x106a68b,0x0})
  7. /usr/local/go/src/runtime/panic.go:1198+0x71fp=0xc000092ee8sp=0xc000092eb8pc=0x102bad1
  8. runtime.sigpanic()
  9. /usr/local/go/src/runtime/signal_unix.go:732+0x1d6fp=0xc000092f38sp=0xc000092ee8pc=0x103f2f6
  10. main.Modify(...)
  11. /Users/slp/github/PostDemo/coreDemo/main.go:21
  12. main.main()
  13. /Users/slp/github/PostDemo/coreDemo/main.go:25+0x5afp=0xc000092f80sp=0xc000092f38pc=0x105b01a
  14. runtime.main()
  15. /usr/local/go/src/runtime/proc.go:255+0x227fp=0xc000092fe0sp=0xc000092f80pc=0x102e167
  16. runtime.goexit()
  17. /usr/local/go/src/runtime/asm_amd64.s:1581+0x1fp=0xc000092fe8sp=0xc000092fe0pc=0x1052dc1
  18. exitstatus2

这些堆栈信息是由 GOTRACEBACK 变量来控制打印粒度的,它有五种级别。

  • none,不显示任何 goroutine 堆栈信息
  • single,默认级别,显示当前 goroutine 堆栈信息
  • all,显示所有 user (不包括 runtime)创建的 goroutine 堆栈信息
  • system,显示所有 user + runtime 创建的 goroutine 堆栈信息
  • crash,和 system 打印一致,但会生成 core dump 文件(Unix 系统上,崩溃会引发 SIGABRT 以触发core dump)

如果我们将 GOTRACEBACK 设置为 system ,我们将看到程序崩溃时所有 goroutine 状态信息

  1. $GOTRACEBACK=systemgorunmain.go
  2. unexpectedfaultaddress0x106a6a4
  3. fatalerror:fault
  4. [signalSIGBUS:buserrorcode=0x2addr=0x106a6a4pc=0x105b01a]
  5. goroutine1[running]:
  6. runtime.throw({0x106a68b,0x0})
  7. ...
  8. goroutine2[forcegc(idle)]:
  9. runtime.gopark(0x0,0x0,0x0,0x0,0x0)
  10. ...
  11. createdbyruntime.init.7
  12. /usr/local/go/src/runtime/proc.go:294+0x25
  13. goroutine3[GCsweepwait]:
  14. runtime.gopark(0x0,0x0,0x0,0x0,0x0)
  15. ...
  16. createdbyruntime.gcenable
  17. /usr/local/go/src/runtime/mgc.go:181+0x55
  18. goroutine4[GCscavengewait]:
  19. runtime.gopark(0x0,0x0,0x0,0x0,0x0)
  20. ...
  21. createdbyruntime.gcenable
  22. /usr/local/go/src/runtime/mgc.go:182+0x65
  23. exitstatus2

如果想获取 core dump 文件,那么就应该把 GOTRACEBACK 的值设置为 crash 。当然,我们还可以通过 runtime/debug 包中的 SetTraceback 方法来设置堆栈打印级别。

delve 调试

delve 是 Go 语言编写的 Go 程序调试器,我们可以通过 dlv core 命令来调试 core dump。

首先,通过以下命令安装 delve

  1. goget-ugithub.com/go-delve/delve/cmd/dlv

还是以上文中的例子为例,我们通过设置 GOTRACEBACK 为 crash 级别来获取 core dump 文件

  1. $tree
  2. .
  3. └──main.go
  4. $ulimit-cunlimited
  5. $gobuildmain.go
  6. $GOTRACEBACK=crash./main
  7. ...
  8. Aborted(coredumped)
  9. $tree
  10. .
  11. ├──core
  12. ├──main
  13. └──main.go
  14. $ls-alhcore
  15. -rw-------1slpslp41MOct3122:15core

此时,在同级目录得到了 core dump 文件 core(文件名、存储路径、是否加上进程号都可以配置修改)。

通过 dlv 调试器来调试 core 文件,执行命令格式 dlv core 可执行文件名 core文件

  1. $dlvcoremaincore
  2. Type'help'forlistofcommands.
  3. (dlv)

命令 goroutines 获取所有 goroutine 相关信息

  1. (dlv)goroutines
  2. *Goroutine1-User:./main.go:21main.main(0x45b81a)(thread18061)
  3. Goroutine2-User:/usr/local/go/src/runtime/proc.go:367runtime.gopark(0x42ed96)[forcegc(idle)]
  4. Goroutine3-User:/usr/local/go/src/runtime/proc.go:367runtime.gopark(0x42ed96)[GCsweepwait]
  5. Goroutine4-User:/usr/local/go/src/runtime/proc.go:367runtime.gopark(0x42ed96)[GCscavengewait]
  6. [4goroutines]
  7. (dlv)

Goroutine 1 是出问题的 goroutine (带有 * 代表当前帧),通过命令 goroutine 1 切换到其栈帧

  1. (dlv)goroutine1
  2. Switchedfrom1to1(thread18061)
  3. (dlv)

执行命令 bt(breakpoints trace) 查看当前的栈帧详细信息

  1. (dlv)bt
  2. 00x0000000000454bc1inruntime.raise
  3. at/usr/local/go/src/runtime/sys_linux_amd64.s:165
  4. 10x0000000000452f60inruntime.systemstack_switch
  5. at/usr/local/go/src/runtime/asm_amd64.s:350
  6. 20x000000000042c530inruntime.fatalthrow
  7. at/usr/local/go/src/runtime/panic.go:1250
  8. 30x000000000042c2f1inruntime.throw
  9. at/usr/local/go/src/runtime/panic.go:1198
  10. 40x000000000043fa76inruntime.sigpanic
  11. at/usr/local/go/src/runtime/signal_unix.go:742
  12. 50x000000000045b81ainmain.Modify
  13. at./main.go:21
  14. 60x000000000045b81ainmain.main
  15. at./main.go:25
  16. 70x000000000042e9c7inruntime.main
  17. at/usr/local/go/src/runtime/proc.go:255
  18. 80x0000000000453361inruntime.goexit
  19. at/usr/local/go/src/runtime/asm_amd64.s:1581
  20. (dlv)

通过 5 0x000000000045b81a in main.Modify 发现了错误代码所在函数,执行命令 frame 5 进入函数具体代码

  1. (dlv)frame5
  2. >runtime.raise()/usr/local/go/src/runtime/sys_linux_amd64.s:165(PC:0x454bc1)
  3. Warning:debuggingoptimizedfunction
  4. Frame5:./main.go:21(PC:45b81a)
  5. 16:}
  6. 17:
  7. 18:funcModify(){
  8. 19:a:="hello"
  9. 20:b:=String2Bytes(a)
  10. =>21:b[0]='H'
  11. 22:}
  12. 23:
  13. 24:funcmain(){
  14. 25:Modify()
  15. 26:}
  16. (dlv)

自此,破案了,问题就出在了擅自修改 string 底层值。

Mac 不能使用

有一点需要注意,上文 core dump 生成的例子,我是在 linux 系统下完成的,mac amd64 系统没法弄(很气,害我折腾了两个晚上)。

这是由于 mac 系统下的 Go 限制了生成 core dump 文件,这个在 Go 源码 src/runtime/signal_unix.go 中有相关说明。

  1. //go:nosplit
  2. funccrash(){
  3. //OSXcoredumpsarelineardumpsofthemappedmemory,
  4. //fromthefirstvirtualbytetothelast,withzerosinthegaps.
  5. //Becauseofthewaywearrangetheaddressspaceon64-bitsystems,
  6. //thismeanstheOSXcorefilewillbe>128GBandevenonazippy
  7. //workstationcantakeOSXwelloveranhourtowrite(uninterruptible).
  8. //Saveusersfrommakingthatmistake.
  9. ifGOOS=="darwin"&&GOARCH=="amd64"{
  10. return
  11. }
  12. dieFromSignal(_SIGABRT)
  13. }

总结

core dump 文件是操作系统提供给我们的一把利器,它是程序意外终止时产生的内存快照。利用 core dump,我们可以在程序崩溃后更好地恢复事故现场,为故障排查保驾护航。

当然,core dump 文件的生成也是有弊端的。core dump 文件较大,如果线上服务本身内存占用就很高,那在生成 core dump 文件上的内存与时间开销都会很大。另外,我们往往会布置服务守护进程,如果我们的程序频繁崩溃和重启,那会生成大量的 core dump 文件(设定了core+pid 命名规则),产生磁盘打满的风险(如果放开了内核限制 ulimit -c unlimited)。

最后,如果担心错误日志不能帮助我们定位 Go 代码问题,我们可以为它开启 core dump 功能,在 hotfix 上增加奇兵。对于有守护进程的服务,建议设置好 ulimt -c 大小限制。

原文链接:https://mp.weixin.qq.com/s/RktnMydDtOZFwEFLLYzlCA