go 自带的 profiling 工具很强大(pprof, trace, GODEBUG …), 但有时我还是想不修改目标进程的源码获取它的一些 runtime 信息, 最近研究了一下 py-spy 和 delve, 发现还是可实现的, 就做了个小东西gospy.
用法
目前就两个命令: gospy summary
和 gospy 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 泄漏.
目前实现的性能不是很好, 如果目标进程有上百万的 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 的视角看正在执行的函数.