后端 go pprof day 4 | 青训营笔记

109 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

今天学习的内容为Go语言性能调优 pprof

性能调优的几大原则

  • 依靠数据不是猜测, 大胆假设, 小心求证. 在性能问题定位阶段可以根据假设定位问题, 而优化阶段需要建立完整的度量机制, 来量化性能优化的效果.
  • 要定位最大瓶颈而不是细枝末节, 现实世界的代码不是完美的, 它面临着许多取舍, 性能优化也是如此, 在有限的时间/经历下, 最大程度的优化性能, 就需要我们直接对最大瓶颈下手.
  • 不要过早优化
  • 不要过度优化,

这里我想补充mask的第一性原理

  1. 约束和限制需求,识别那些真正的需求。
  2. 尽量删除多余的部分。如果你没有把自己删除的10%又找回来,那么就是删除的不够。
  3. 优化结构使其变得合理。
  4. 聚焦提高效率和效益。
  5. 固化流程和机制,实现自动化。 这五个步骤在系统优化领域同样适用, 在我们日常工作的系统中, 保证系统的高内聚低耦合, 同时高度自动化, 这样才能在系统性能劣化前提前定位, 解决问题.

接下来以 go-pprof-practice 项目, 实战pprof的用法

首先clone下来项目, 并直接运行. 随后访问 http://localhost:6060/debug/pprof/, 可以看到pprof的web统计主页, 具体有以下一些指标

类型描述
allocs内存分配情况的采样信息
blocks阻塞操作情况的采样信息
cmdline显示程序启动命令及参数
goroutine当前所有协程的堆栈信息
heap堆上内存使用情况的采样信息
mutex锁争用情况的采样信息
profileCPU 占用情况的采样信息
threadcreate系统线程创建情况的采样信息
trace程序运行跟踪信息

我们借助go自带的go tool pprof命令排查问题, 它会读取pprof通过web暴露的数据来分析上述指标.

cpu过高

go tool pprof http://localhost:6060/debug/pprof/profile

该命令会读取数据, 然后进入可交互终端

gitpod /workspace/go-pprof-practice (master) $ go tool pprof http://localhost:6060/debug/pprof/profile
Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile

Saved profile in /home/gitpod/pprof/pprof.main.samples.cpu.001.pb.gz
File: main
Type: cpu
Time: Jan 18, 2023 at 9:22am (UTC)
Duration: 30s, Total samples = 17.26s (57.53%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) 

输入top命令, 显示占用cpu时间最高的函数调用(flat为在该函数中语句的执行, sum为总的执行耗时, 包含函数调用)

(pprof) top
Showing nodes accounting for 17.21s, 99.71% of 17.26s total
Dropped 10 nodes (cum <= 0.09s)
      flat  flat%   sum%        cum   cum%
    17.21s 99.71% 99.71%     17.24s 99.88%  github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat
         0     0% 99.71%     17.24s 99.88%  github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Live
         0     0% 99.71%     17.25s 99.94%  main.main
         0     0% 99.71%     17.25s 99.94%  runtime.main

输入 list Eat, 可以查看函数具体信息 输入web, 会生成一个svg形式的函数调用耗时的树形图

内存过高

再次输入命令, 后缀改为heap `go tool pprof http://localhost:6060/debug/pprof/heap

再次使用top, list命令

gitpod /workspace/go-pprof-practice (master) $ go tool pprof http://localhost:6060/debug/pprof/heap
Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap
Saved profile in /home/gitpod/pprof/pprof.main.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
File: main
Type: inuse_space
Time: Jan 18, 2023 at 9:41am (UTC)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 768MB, 99.74% of 770MB total
Dropped 17 nodes (cum <= 3.85MB)
      flat  flat%   sum%        cum   cum%
     768MB 99.74% 99.74%      768MB 99.74%  github.com/wolfogre/go-pprof-practice/animal/muridae/mouse.(*Mouse).Steal
         0     0% 99.74%      768MB 99.74%  github.com/wolfogre/go-pprof-practice/animal/muridae/mouse.(*Mouse).Live
         0     0% 99.74%      768MB 99.74%  main.main
         0     0% 99.74%      768MB 99.74%  runtime.main
(pprof) list Steal
Total: 770MB
ROUTINE ======================== github.com/wolfogre/go-pprof-practice/animal/muridae/mouse.(*Mouse).Steal in /workspace/go-pprof-practice/animal/muridae/mouse/mouse.go
     768MB      768MB (flat, cum) 99.74% of Total
         .          .     45:
         .          .     46:func (m *Mouse) Steal() {
         .          .     47:   log.Println(m.Name(), "steal")
         .          .     48:   max := constant.Gi
         .          .     49:   for len(m.buffer)*constant.Mi < max {
     768MB      768MB     50:           m.buffer = append(m.buffer, [constant.Mi]byte{})
         .          .     51:   }
         .          .     52:}

可以看到 Steal 函数中向buffer中加入了1G内存.

频繁内存gc

通过 GODEBUG=gctrace=1 ./go-pprof-practice | grep gc, 运行程序, 开启gctrace选项.

等待程序运行一段时间后, 再运行 go tool pprof http://localhost:6060/debug/pprof/allocs, 查看allocs选项.

(pprof) top
Showing nodes accounting for 336MB, 99.70% of 337MB total
Dropped 13 nodes (cum <= 1.69MB)
      flat  flat%   sum%        cum   cum%
     336MB 99.70% 99.70%      336MB 99.70%  github.com/wolfogre/go-pprof-practice/animal/canidae/dog.(*Dog).Run (inline)
         0     0% 99.70%      336MB 99.70%  github.com/wolfogre/go-pprof-practice/animal/canidae/dog.(*Dog).Live
         0     0% 99.70%      336MB 99.70%  main.main
         0     0% 99.70%      336MB 99.70%  runtime.main
(pprof) list Run
Total: 337MB
ROUTINE ======================== github.com/wolfogre/go-pprof-practice/animal/canidae/dog.(*Dog).Run in /workspace/go-pprof-practice/animal/canidae/dog/dog.go
     336MB      336MB (flat, cum) 99.70% of Total
         .          .     38:   log.Println(d.Name(), "pee")
         .          .     39:}
         .          .     40:
         .          .     41:func (d *Dog) Run() {
         .          .     42:   log.Println(d.Name(), "run")
     336MB      336MB     43:   _ = make([]byte, 16*constant.Mi)
         .          .     44:}
         .          .     45:
         .          .     46:func (d *Dog) Howl() {
         .          .     47:   log.Println(d.Name(), "howl")
         .          .     48:}

可以看到Dog.Run创建了16MB的字节数组没有使用, 因此每次gc就会被清理掉

后续排查的原理都类似, 略去命令输出

协程泄露

go tool pprof http://localhost:6060/debug/pprof/goroutine

排查到函数会创建空协程

func (w *Wolf) Drink() { 
	log.Println(w.Name(), "drink") 
	for i := 0; i < 10; i++ { 
		go func() { time.Sleep(30 * time.Second) }() 
	} 
}

锁争用

go tool pprof http://localhost:6060/debug/pprof/mutex

func (w *Wolf) Howl() {
	log.Println(w.Name(), "howl")
	m := &sync.Mutex{}
	m.Lock()
	go func() {
		time.Sleep(time.Second)
		m.Unlock()
	}()
	m.Lock()
}

协程在Sleep时持有了锁

阻塞操作

go tool pprof http://localhost:6060/debug/pprof/block

func (c *Cat) Pee() {
	log.Println(c.Name(), "pee")
	<-time.After(time.Second)
}

channel 读取数据时发生了阻塞

pprof 同样支持http形式展现, 可以有火焰图.

pprof 原理

  1. 采样 如通过定时发送 SIGPROF 信号, 记录堆栈
  2. hook 堆分配头加入prof信息, 创建go routine时, 自动跟踪记录信息等.

引用链接

wolfogre/go-pprof-practice: go pprof practice. (github.com) golang pprof 实战 | Wolfogre's Blog