本文方法通过 kill -11(SIGSEGV) 强制终止 Go 进程获取堆栈,执行后进程直接退出!仅限测试环境、离线调试、非核心业务使用,严禁在生产在线服务随意执行!
SIGSEGV(信号 11)是段错误信号,内存非法访问时内核会发送给进程,默认行为是直接终止进程。Go 运行时默认注册了该信号的处理函数:收到 SIGSEGV 后,会先打印完整 goroutine 堆栈,再退出进程。利用这个特性,我们可以手动向 Go 进程发送 SIGSEGV,强制触发堆栈打印(注意:进程会被杀死,仅限调试使用)
有时候 golang 出现卡死、死循环、协程泄露等问题,但又没开启 pprof、没有其他调试办法时可以使用这种方式。
先要准备一个 golang 测试代码
package mainimport ("fmt""time")funcmain() {go func() {for {time.Sleep(time.Second)fmt.Println("goroutine running")}}()for {time.Sleep(time.Second)fmt.Println("main running")}}
# 编译go build -o myapp myapp.go
Go 程序触发 SIGSEGV 打印的所有堆栈信息,只会输出到标准错误流 stderr(文件描述符 2)。 我们日常启动程序时,日志输出方式不同,stderr 挂载的目标就不一样,堆栈存放位置也随之改变,因此划分出三种场景,只需先查看 /proc/<PID>/fd/2 确认 stderr 去向,就能对应选择读取方式。
ps -elf | grep myapp# 替换为真实进程PID查询ls -l /proc/<PID>/fd/2
作用:快速判定堆栈输出位置,决定后续怎么抓取堆栈。
kill -11 <PID>•启动方式:./myapp
fd/2 指向 /dev/pts/xxx 终端设备ls -l /proc/<PID>/fd/2lrwx------ 1 mars mars 64 May 17 11:32 2 -> /dev/pts/23
•特点:堆栈直接打印在当前运行窗口,直观可见
# 新窗口上发送信号kill -11 <PID># myapp窗口可以看到./myapp......SIGSEGV: segmentation violationPC=0x40332e m=0 sigcode=0 addr=0x3e8000591c5goroutine 1 gp=0xc000006380 m=nil [sleep]:runtime.gopark(0x194c923fb1714b?, 0x1?, 0x0?, 0x0?, 0x46?)/usr/lib/go-1.22/src/runtime/proc.go:402 +0xce fp=0xc00006fea0 sp=0xc00006fe80 pc=0x4362aetime.Sleep(0x3b9aca00)/usr/lib/go-1.22/src/runtime/time.go:195 +0x115 fp=0xc00006fee0 sp=0xc00006fea0 pc=0x4600f5main.main()/home/mars/test/myapp.go:17 +0x28 fp=0xc00006ff50 sp=0xc00006fee0 pc=0x48d7e8runtime.main()/usr/lib/go-1.22/src/runtime/proc.go:271 +0x29d fp=0xc00006ffe0 sp=0xc00006ff50 pc=0x435e7druntime.goexit({})/usr/lib/go-1.22/src/runtime/asm_amd64.s:1695 +0x1 fp=0xc00006ffe8 sp=0xc00006ffe0 pc=0x462e81
• 启动方式:./myapp > myapp.log 2>&1
fd/2 指向本地日志文件路径ls /proc/<PID>/fd/2 -ll-wx------ 1 mars mars 64 May 17 11:03 2 -> /home/mars/test/myapp.log
kill -11 <PID># 当手动发11信号可以看到:cat myapp.log | tail -n 50runtime.gcenable.gowrap2()/usr/lib/go-1.22/src/runtime/mgc.go:204 +0x25 fp=0xc00003dfe0 sp=0xc00003dfc8 pc=0x417145runtime.goexit({})/usr/lib/go-1.22/src/runtime/asm_amd64.s:1695 +0x1 fp=0xc00003dfe8 sp=0xc00003dfe0 pc=0x462e81created by runtime.gcenable in goroutine 1/usr/lib/go-1.22/src/runtime/mgc.go:204 +0xa5
•启动方式:./myapp >/dev/null 2>&1 &
fd/2 指向 /dev/nullls /proc/<PID>/fd -ll-wx------ 1 mars mars 64 May 17 10:19 2 -> /dev/null
•特点:所有日志、错误信息都会被系统丢弃,常规方式看不到任何堆栈
#先跟踪进程strace -p <PID> -y -s 500 -o myapp.strace.txt#发送信号kill -11 <PID>#查看堆栈cat myapp.strace.txt--- SIGSEGV {si_signo=SIGSEGV, si_code=SI_USER, si_pid=364997, si_uid=1000} ---nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0write(2</dev/null>, "SIGSEGV: segmentation violation", 31) = 31write(2</dev/null>, "\n", 1) = 1write(2</dev/null>, "PC=", 3) = 3write(2</dev/null>, "0x464c21", 8) = 8write(2</dev/null>, " m=", 3) = 3write(2</dev/null>, "0", 1) = 1write(2</dev/null>, " sigcode=", 9) = 9write(2</dev/null>, "0", 1) = 1write(2</dev/null>, " addr=", 6) = 6write(2</dev/null>, "0x3e8000591c5", 13) = 13write(2</dev/null>, "\n", 1) = 1write(2</dev/null>, "\n", 1) = 1write(2</dev/null>, "goroutine ", 10) = 10write(2</dev/null>, "0", 1) = 1
进程退出后 strace 会自动结束;如果长时间没有输出,请检查进程是否还在运行,或手动 Ctrl+C 终止 strace。
上面的格式化看起来不好看,把标准错误输出的信息搜索出来,然后使用perl提取堆栈,做下格式化:
cat myapp.strace.txt | grep 'write(2' | perl -ne 'if($_=~/"\([^"]+)"/) { $line=$1;$line =~ s/\\n/\n/g;$line =~ s/\\t/\t/g;print $line;}'goroutine 4 gp=0xc000007500 m=nil [sleep]:runtime.gopark(0x195065a41e1ecd?, 0x1?, 0x0?, 0x0?, 0x4c?)/usr/lib/go-1.22/src/runtime/proc.go:402 +0xce fp=0xc000066f30 sp=0xc000066f10 pc=0x4362aetime.Sleep(0x3b9aca00)/usr/lib/go-1.22/src/runtime/time.go:195 +0x115 fp=0xc000066f70 sp=0xc000066f30 pc=0x4600f5main.main.func1()/home/mars/test/myapp.go:11 +0x1c fp=0xc000066fe0 sp=0xc000066f70 pc=0x48d87cruntime.goexit({})/usr/lib/go-1.22/src/runtime/asm_amd64.s:1695 +0x1 fp=0xc000066fe8 sp=0xc000066fe0 pc=0x462e81created by main.main in goroutine 1/home/mars/test/myapp.go:9 +0x1erax 0xcarbx 0x0rcx 0x464c23rdx 0x0
命令说明
cat myapp.strace.txt | grep 'write(2' | perl -ne 'if($_=~/"([^"]+)"/) { $line=$1;$line =~ s/\\n/\n/g;$line =~ s/\\t/\t/g;print $line;}'1)先搜索出标准错误的系统调用。
cat myapp.strace.txt | grep 'write(2'write(2</dev/null>, " addr=", 6) = 6write(2</dev/null>, "0x3e8000591c5", 13) = 13write(2</dev/null>, "\n", 1) = 1
2) 然后使用perl脚本提取引号里的字符串,并把\n \t打印成回车和tab键。
if($_=~/"([^"]+)"/) { # 匹配引号里的内容$line=$1; # 引号里的内容提取出来$line =~ s/\\n/\n/g; # 把字符串\n替换成回车$line =~ s/\\t/\t/g; # 把字符串\t替换成tabprint $line; # 输出替换过程的内容}
1.Go 进程长时间卡死无响应2.业务逻辑死循环阻塞3.协程堆积、goroutine 泄露排查4.服务未开启 pprof 调试端口5.无本地日志、无监控告警、无远程调试入口
1.kill -11 属于强杀调试手段,执行后进程直接退出,仅可使用一次2.务必精准核对进程 PID,禁止批量操作,避免误杀线上正常业务进程3.该方式破坏性较强,正式生产环境严格禁止使用4.仅作为无任何调试手段时的兜底排查方案
1.内置 pprof:线上实时抓取协程、堆栈、性能指标,不中断服务2.gcore 生成 core 转储文件:离线分析进程现场,不影响业务运行3.dlv 远程断点调试:线上无损动态调试,精准定位代码阻塞点