zhangguanzhang's Blog

golang pprof 转 svg 的最小二进制依赖尝试

字数统计: 930阅读时长: 4 min
2024/04/16

不依赖 go ,最小二进制把 pprof 转 svg 的探索

由来

内部同事做了个采集业务 pprof 的功能,发现工具镜像非常臃肿,带了个 golang 出去。用的是 go-torchcurl -o /debug/pprof/profile 采集的文件转成 svg ,转换的日志里发现它命令行调用了 go tool pprof,而且 go-torch 我看仓库已经不维护了。

尝试

一个是涉及到迭代解决安全,另一个 golang 已经 go 自举了,把 go tool pprofpprof 命令抠出来直接用就行了,甚至都不需要 golang 环境了,搜了下发现实际 go tool pprof 就是仓库 google/pprof

分析

go-torch 转换的命令如下

1
2
3
$ go-torch xxx-profile-20240416110934.pg.gz -f 1.svg
INFO[03:12:29] Run pprof command: go tool pprof -raw -seconds 30 xxx-profile-20240416110934.pg.gz
INFO[03:12:29] Writing svg to 1.svg

浅显的看了下它的源码,发现它是把 pprof 的 raw 格式输出,按照 pprof 的字节流格式解析的,就是 state funcnames samplenames records 那些,然后调用 flamegraph perl 脚本转 svg 的。

1
2
// https://github.com/uber-archive/go-torch/blob/master/renderer/flamegraph.go#L37
flameGraphScripts = []string{"flamegraph", "flamegraph.pl", "./flamegraph.pl", "./FlameGraph/flamegraph.pl", "flame-graph-gen"}

如果单纯转 svg,pprof 工具就可以,它转 svg 会先转 dot 格式,然后 exec 调用 graphviz 包下的 dot 命令。

dot 命令要包管理安装,并且很多依赖,看看有没有 golang 的实现。谷歌搜索 golang dot to svg 看到了第一个仓库 goccy/go-graphviz,大致看了下 readme.md ,发现它不依赖 graphviz 并且提供了一个 dot 的命令实现。测试下:

1
2
go install github.com/google/pprof@latest
go install github.com/goccy/go-graphviz/cmd/dot@latest

测试转 svg:

1
2
3
4
5
$ pprof -svg -seconds 30 http://xx.xxx.0.84:xxxx/debug/pprof/profile > test.svg
Fetching profile over HTTP from http://xx.xxx.0.84:xxxx/debug/pprof/profile?seconds=30
Please wait... (30s)
Saved profile in /root/pprof/pprof.xxxx.samples.cpu.001.pb.gz
the required flag `-o' was not specified

发现报错,查看了下 pprof 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// https://github.com/google/pprof/blob/26353dc0451f29f7b9cdade98377a016779b8527/internal/driver/commands.go#L385-L408
func invokeDot(format string) PostProcessor {
return func(input io.Reader, output io.Writer, ui plugin.UI) error {
cmd := exec.Command("dot", "-T"+format)
cmd.Stdin, cmd.Stdout, cmd.Stderr = input, output, os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to execute dot. Is Graphviz installed? Error: %v", err)
}
return nil
}
}

// massageDotSVG invokes the dot tool to generate an SVG image and alters
// the image to have panning capabilities when viewed in a browser.
func massageDotSVG() PostProcessor {
generateSVG := invokeDot("svg")
return func(input io.Reader, output io.Writer, ui plugin.UI) error {
baseSVG := new(bytes.Buffer)
if err := generateSVG(input, baseSVG, ui); err != nil {
return err
}
_, err := output.Write([]byte(massageSVG(baseSVG.String())))
return err
}
}

发现是把 dot 格式的字节流给 dot -Tsvg 的 stdin ,dot 命令会自动把流输出到标准输出,然后 pprof 会把 dot 输出的字节流捕获,上面的报错就是 golang 的 dot 有问题。看了下 dot 的源码,发现 -o 选项是必须的,下面是改过的 diff:

1
2
3
4
5
6
7
8
18c18
< OutputFile string `description:"specify output file name" short:"o" required:"true"`
---
> OutputFile string `description:"specify output file name" short:"o"`
51a52,54
> }
> if !terminal.IsTerminal(0) {
> opt.OutputFile = "/dev/stdout"

编译后替换了,发现可以转换成 svg 了,但是还有个问题,会输出 Saved profile in /root/pprof/pprof.main.samples.cpu.001.pb.gz 发现会把 tmp 文件存在这里,可以通过设置变量 PPROF_TMPDIR 指定 tmp 文件路径,但是 pprof 没有任何选项参数删除这个 tmp 文件,提了 issue 直接给我关了。

其实也可以扣 pprof 源码拼 go-graphviz 库源码,这样一键或者 golang 函数内采集指定参数转 svg ,但是这块功能目前还没长期定型下来,暂时还是最小的 pprof + dot

CATALOG
  1. 1. 由来
  2. 2. 尝试
    1. 2.1. 分析