百种弊病,皆从懒生

gospy: Non-invasive goroutine inspector

2019.09.20

go 自带的 profiling 工具很强大(pprof, trace, GODEBUG …), 但有时我还是想不修改目标进程的源码获取它的一些 runtime 信息, 最近研究了一下 py-spy 和 delve, 发现还是可实现的, 就做了个小东西gospy.

用法

目前就两个命令: gospy summarygospy top

sudo ./gospy summary --pid 1234, 可以 dump 目标进程的一些信息和当前活动 goroutine 正在执行的函数信息, 比如对一个 prometheus 进程做一次 snapshot:

bin: /home/will/Downloads/prometheus-2.12.0.linux-amd64/prometheus, goVer: 1.12.8, gomaxprocs: 6
P0 idle, schedtick: 642, syscalltick: 81
P1 idle, schedtick: 959, syscalltick: 67
P2 idle, schedtick: 992, syscalltick: 32
P3 idle, schedtick: 581, syscalltick: 17
P4 idle, schedtick: 89, syscalltick: 8
P5 idle, schedtick: 231, syscalltick: 5
Threads: 14 total, 0 running, 14 sleeping, 0 stopped, 0 zombie
Goroutines: 44 total, 0 idle, 0 running, 5 syscall, 39 waiting

goroutines:

1 - waiting for chan receive: rt0_go (/usr/local/go/src/runtime/asm_amd64.s:202) 
2 - waiting for force gc (idle): 5 (/usr/local/go/src/runtime/proc.go:240) 
3 - waiting for GC sweep wait: gcenable (/usr/local/go/src/runtime/mgc.go:209) 
8 - syscall: addtimerLocked (/usr/local/go/src/runtime/time.go:169) 
9 - waiting for select: 0 (/app/vendor/go.opencensus.io/stats/view/worker.go:33) 
16 - waiting for GC worker (idle): gcBgMarkStartWorkers (/usr/local/go/src/runtime/mgc.go:1785) 
17 - waiting for finalizer wait: createfing (/usr/local/go/src/runtime/mfinal.go:156) 
19 - syscall: 0 (/usr/local/go/src/os/signal/signal_unix.go:30) 
22 - waiting for GC worker (idle): gcBgMarkStartWorkers (/usr/local/go/src/runtime/mgc.go:1785) 
23 - waiting for GC worker (idle): gcBgMarkStartWorkers (/usr/local/go/src/runtime/mgc.go:1785) 
38 - waiting for GC worker (idle): gcBgMarkStartWorkers (/usr/local/go/src/runtime/mgc.go:1785) 
49 - waiting for GC worker (idle): gcBgMarkStartWorkers (/usr/local/go/src/runtime/mgc.go:1785) 
50 - waiting for GC worker (idle): gcBgMarkStartWorkers (/usr/local/go/src/runtime/mgc.go:1785) 
74 - waiting for select: sync (/app/scrape/scrape.go:408) 
75 - syscall: addtimerLocked (/usr/local/go/src/runtime/time.go:169) 
84 - syscall: addtimerLocked (/usr/local/go/src/runtime/time.go:169) 
85 - waiting for select: Run (/app/vendor/github.com/oklog/run/group.go:36) 
...

P0, P1 …, 是 go 的 GMP schedule 模型里的 P, 可以简单理解成对一个物理核心的逻辑抽象. 个数可由环境变量 GOMAXPROCS 控制, 默认是机器核心数.

sudo ./gospy top --pid 1234, 显示一个类似 top 命令的界面, 把 goroutine 按照正在执行的函数做了个 group by, 可以用来观察下有没有 goroutine 泄漏.

top

目前实现的性能不是很好, 如果目标进程有上百万的 goroutine, 不要轻易尝试, 但优化空间还挺大的, 后续整一下.

一些限制

  • 只支持 x86_64 的 linux
  • 目标进程的 binary 需要保留 debug 信息, link 的时候传了 -w -s 就没办法了.
  • build 时候如果用了 -buildmod=pie, 也不支持, pie 模式下, 二进制文件在进程虚拟内存里的基地址会随机化, 怎么算出这个基地址我倒是知道, 但 go build 出的 pie 格式里, 有 debug 信息, 却没有 .gopclntab 这个 section(用来根据 pc 查找到对应函数的行号), 目前没弄懂怎么搞… docker 官方 release 的 binary 就是 pie 模式的, 所以对 dockerd 无效.

其他参数

--bin, 如果目标进程是 stripe 过的, 可以用相同源码再编译一个带 debug 信息的 binary, 用这个参数指定位置.

--non-blocking, 默认 gospy 会用 ptrace 把目标进程暂停(进程会进入 t 状态), 做完 snapshot 之手再 detach. 对目标进程是有一定性能影响的(goroutine 越多, 暂停时间越长),这个参数就是 不暂停进程直接读取内存, 如果目标进程很活跃, 可能读不到一致的内存视图, 会失败.

--pc, pc 指 program counter, 就是 rip 寄存器里存储的下一条要执行的指令, 根据 pc 值可以从 debug info 中查找到对应的函数名. 这个参数指定使用哪种类型的 pc 值, 默认是 start, 是 goroutine 当前执行的函数, caller 是 trigger 这个 goroutine 的函数, current 是从 schedule 的视角看正在执行的函数.

comments powered by Disqus