1. 性能分析
golang 自身提供的工具中包括性能分析工具 - pprof。
golang 内建提供了多种性能收集器,它们负责收集这些性能数据:
- Goroutine: stack traces of all current Goroutines。Go Routine 分析。
- CPU: stack traces of CPU returned by runtime。CPU 分析。按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置
- Heap: a sampling of memory allocations of live objects。内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏
- Allocation: a sampling of all past memory allocations。内存分析,对象分配统计以及内存泄漏等。
- Thread: stack traces that led to the creation of new OS threads。OS 线程创建情况。
- Block: stack traces that led to blocking on synchronization primitives。阻塞分析。记录 go routine 阻塞情况,等待以及同步情况,timer/channel 通讯等各项细节。
- Mutex: stack traces of holders of contended mutexes。互斥锁分析。包括各种竞争情况。
2. pprof工具分析
(1.)通过在main函数中添加性能监控
package main
import (
_ "net/http/pprof" // 引入 pprof 模块
_ "github.com/mkevac/debugcharts" // 可选,图形化插件
)
func main(){
// ...
// 内存分析
go func() {
http.ListenAndServe("0.0.0.0:8090", nil) // 监控端口
}()
// ...
}
"net/http/pprof" 包将会自动地添加一组 api 端点,该包的 init 函数会做这些事:
func init() {
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)
}
2.1 web界面查看
直接通过浏览器访问http://localhost:8090/debug/pprof, 就会看到如下界面:
参数说明:
- cpu(CPU Profiling): $HOST/debug/pprof/profile,默认进行 30s 的 CPU Profiling,得到一个分析用的 profile 文件
- block(Block Profiling):$HOST/debug/pprof/block,查看导致阻塞同步的堆栈跟踪
- goroutine:$HOST/debug/pprof/goroutine,查看当前所有运行的 goroutines 堆栈跟踪
- heap(Memory Profiling): $HOST/debug/pprof/heap,查看活动对象的内存分配情况
- mutex(Mutex Profiling):$HOST/debug/pprof/mutex,查看导致互斥锁的竞争持有者的堆栈跟踪
- threadcreate:$HOST/debug/pprof/threadcreate,查看创建新OS线程的堆栈跟踪
github.com/mkevac/debugchartshttp://localhost:8090/ui
2.2 命令行交互
(1.)CPU分析
go tool pprof http://localhost:6060/debug/pprof/profile\?seconds\=60
Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=60
Saved profile in /Users/eddycjy/pprof/pprof.samples.cpu.007.pb.gz
Type: cpu
Duration: 1mins, Total samples = 26.55s (44.15%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
top10list func_namepdfwebtreetraceshelp
(pprof) top10
Showing nodes accounting for 25.92s, 97.63% of 26.55s total
Dropped 85 nodes (cum <= 0.13s)
Showing top 10 nodes out of 21
flat flat% sum% cum cum%
23.28s 87.68% 87.68% 23.29s 87.72% syscall.Syscall
0.77s 2.90% 90.58% 0.77s 2.90% runtime.memmove
0.58s 2.18% 92.77% 0.58s 2.18% runtime.freedefer
0.53s 2.00% 94.76% 1.42s 5.35% runtime.scanobject
0.36s 1.36% 96.12% 0.39s 1.47% runtime.heapBitsForObject
0.35s 1.32% 97.44% 0.45s 1.69% runtime.greyobject
0.02s 0.075% 97.51% 24.96s 94.01% main.main.func1
0.01s 0.038% 97.55% 23.91s 90.06% os.(*File).Write
0.01s 0.038% 97.59% 0.19s 0.72% runtime.mallocgc
0.01s 0.038% 97.63% 23.30s 87.76% syscall.Write
参数说明:
- flat:给定函数上运行耗时
- flat%:同上的 CPU 运行耗时总比例
- sum%:给定函数累积使用 CPU 总比例
- cum:当前函数加上它之上的调用运行总耗时
- cum%:同上的 CPU 运行耗时总比例
(2.)内存分析
go tool pprof http://localhost:8080/debug/pprof/heap
// 停掉后,过一分钟后再次执行上述命令,使其生成两个文件,进行比较
go tool pprof -base pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz
// 使用top命令查看占用较多的,再使用list查看具体占用情况
内存分析有几个参数:
-alloc_objects 申请对象的内存占用情况
-inuse_objects 在使用中的对象内存占用情况
-inuse_space 使用中的内存空间
使用pprof的heap能够获取程序运行时的内存信息,在程序平稳运行的情况下,每个一段时间使用heap获取内存的profile,然后使用base能够对比两个profile文件的差别,需要不断的比较两次的内存申请和占用情况,关注哪些对象的申请不断的增长,哪些对象在程序结束后,仍然在使用中,没有释放内存。
要点:
- 内存profiling记录的是堆内存分配的情况,以及调用栈信息,并不是进程完整的内存情况
- 栈内存的分配是在调用栈结束后会被释放的内存,所以并不在内存profile中。
- 内存profiling是基于抽样的,默认是每1000次堆内存分配,执行1次profile记录。
- 因为内存profiling是基于抽样和它跟踪的是已分配的内存,而不是使用中的内存,(比如有些内存已经分配,看似使用,但实际以及不使用的内存,比如内存泄露的那部分),所以不能使用内存profiling衡量程序总体的内存使用情况。
- 在进行堆分配时,内存分析会记录调用栈跟踪,栈分配被认为是无成本的,并且在内存 profile 中不被追踪。
- 使用内存profiling不能够发现内存泄露
2.3 火焰图
(1.) 安装go-torch
# 下载并复制flamegraph.pl到/usr/local/bin下
git clone https://github.com/brendangregg/FlameGraph.git
cp FlameGraph/flamegraph.pl /usr/local/bin
# 安装go-torch
go get -v github.com/uber/go-torch
(2.) 使用go-torch查看
// 使用
go-torch -u http://10.11.209.102:9911 -t 30
go-torch -u http://10.0.2.15:6060 --suffix=/debug/pprof/block -p > torch.svg
go-torch cpu.prof // top list
3. 使用goland自带的基准测试分析
goland2020版本,提供了基准测试,可以进行内存、CPU的分析:
如果golang版本较低,可能没有自带的运行按钮,可以使用以下命令执行:
go test -cpuprofile cpu.prof -memprofile mem.prof -bench .
// 对生成的文件,使用go tool进行分析
go tool pprof cpu.pprof
top10 -cum // 查看top10占用情况
list xxx // 查看具体某个函数的内存
go tool pprof -http=":8080" cpu.pprof // 使用web界面进行分析
4.自行写脚本查看内存占用情况
4.1 使用shell命令来实时查看内存占用情况
pidstat -r -p $pid 1
vim prog_mem.sh
#!/bin/bash
prog_name="your_programe_name"
prog_mem=$(pidstat -r -u -h -C $prog_name |awk 'NR==4{print $12}')
time=$(date "+%Y-%m-%d %H:%M:%S")
echo $time" memory(Byte):"$prog_mem >>~/record/prog_mem.log
(2.) 使用crontab建立定时任务,每分钟记录1次.
crontab -e
*/1 * * * * /record/prog_mem.sh
(3.) 将输出的日志信息prog_mem.log进行分析,可以使用可视化工具进行查看。
4.2 使用golang程序开启内存、CPU采集
go tool pprof -http=:6060 cpu.prof
func enableCpuProfile(cpuProfilePath string) (closer func()) {
closer = func() {}
if cpuProfilePath != "" {
f, err := os.Create(cpuProfilePath)
if err != nil {
log.Fatal("could not create cpu profile: %v", err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
log.Fatal("error: %v", err)
}
closer = pprof.StopCPUProfile
}
runtime.SetBlockProfileRate(20)
return
}
func enableMemProfile(memProfilePath string) (closer func()) {
closer = func() {}
if memProfilePath != "" {
closer = func() {
f, err := os.Create(memProfilePath)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
defer f.Close()
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("could not write memory profile: ", err)
}
}
}
return
}
也可以简化程序,在main中加入这两行代码,在程序退出时,会生成pprof文件。
# 这里profile引用包:github.com/pkg/profile
defer profile.Start(profile.CPUProfile).Stop() // CPU
defer profile.Start(profile.MemProfile).Stop() // 内存
defer profile.Start(profile.GoroutineProfile).Stop() // goroutine
defer profile.Start(profile.BlockProfile).Stop() // 阻塞
defer profile.Start(profile.MutexProfile, profile.MemProfile).Stop() // 合并写法
5.常见的内存泄露
5.1 goroutine泄露
每个goroutine占用2KB内存,泄露1百万goroutine至少泄露2KB * 1000000 = 2GB内存。goroutine执行过程中还存在一些变量,如果这些变量指向堆内存中的内存,GC会认为这些内存仍在使用,不会对其进行回收,这些内存谁都无法使用,造成了内存泄露。
所以goroutine泄露有2种方式造成内存泄露:
- goroutine本身的栈所占用的空间造成内存泄露。
- goroutine中的变量所占用的堆内存导致堆内存泄露,这一部分是能通过heap profile体现出来的。
判断依据:在节点正常运行的情况下,隔一段时间获取goroutine的数量,如果后面获取的那次,某些goroutine比前一次多,如果多获取几次,是持续增长的,就极有可能是goroutine泄露。
定位:
http://ip:port/debug/pprof/goroutine?debug=1 //查看当前阻塞在此goroutine的数量
http://ip:port/debug/pprof/goroutine?debug=2 // 可以查看goroutine阻塞的时长
总结:
goroutine泄露常见的场景是channel阻塞,无法继续向下执行,导致此goroutine关联的内存都无法释放,进一步造成内存泄露。
channel的读或者写:
- 无缓冲channel的阻塞通常是写操作因为没有读而阻塞
- 有缓冲的channel因为缓冲区满了,写操作阻塞
- 期待从channel读数据,结果没有goroutine写
- select操作,select里也是channel操作,如果所有case上的操作阻塞,goroutine也无法继续执行。
5.2 time.After 内存泄露
# 错误示例:
package main
import (
"time"
)
func main() {
ch := make(chan int, 10)
go func() {
var i = 1
for {
i++
ch <- i
}
}()
for {
select {
case x := <- ch:
println(x)
case <- time.After(3 * time.Minute):
println(time.Now().Unix())
}
}
}
在for循环每次select的时候,都会实例化一个一个新的定时器。该定时器在3分钟后,才会被激活,但是激活后已经跟select无引用关系,被gc给清理掉。time.After定时器在定时任务到达之前,会一直存在于时间堆中,不会释放资源,直到定时任务时间到达后才会释放资源。
在go代码中,在for循环里不要使用select + time.After的组合,可以使用time.NewTimer替代:
1 package main
2
3 import (
4 "time"
5 )
6
7 func main() {
8 ch := make(chan int, 10)
9
10 go func() {
11 for {
12 ch <- 100
13 }
14 }()
15
16 idleDuration := 3 * time.Minute
17 idleDelay := time.NewTimer(idleDuration)
18 defer idleDelay.Stop()
19
20 for {
21 idleDelay.Reset(idleDuration)
22
23 select {
24 case x := <- ch:
25 println(x)
26 case <-idleDelay.C:
27 return
28 }
29 }
30 }
6.开发建议:
var pool = sync.Pool{New: func() interface{} { return make([]byte, 4096) }}
func fn() {
buf := pool.Get().([]byte) // takes from pool or calls New
// do work
pool.Put(buf) // returns buf to the pool
}