使用profiling工具能帮你发现包括CPU、IO和内存在内多种类型的热点。所谓热点,是指那些为了能显著提升性能而值得你去关注的地方。了解针对性能的不同边界因素也是比较重要的,比方说,如果一个程序使用100 Mbps带宽的网络进行通信,而目前已经占用了超过90 Mbps的带宽,为了提升它的性能,你拿这样的程序也没啥办法了。在磁盘IO、内存消耗和计算密集型任务方面,也有类似的边界因素。
我日常主要使用”runtime/pprof”包
Go 运行时包含了内建的CPU分析器,它用来展示某个函数耗费了多少CPU百分时间。这里有三种方式来使用它:
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") func main() { flag.Parse() if *cpuprofile != "" { f, err := os.Create(*cpuprofile) if err != nil { log.Fatal(err) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } ...
执行go tool pprof progress filename
最主要的命令是’topN’,查看profile里采样最多的n条记录。
(pprof) top10
Total: 2525 samples
298 11.8% 11.8% 345 13.7% runtime.mapaccess1_fast64
268 10.6% 22.4% 2124 84.1% main.FindLoops
251 9.9% 32.4% 451 17.9% scanblock
178 7.0% 39.4% 351 13.9% hash_insert
131 5.2% 44.6% 158 6.3% sweepspan
119 4.7% 49.3% 350 13.9% main.DFS
96 3.8% 53.1% 98 3.9% flushptrbuf
95 3.8% 56.9% 95 3.8% runtime.aeshash64
95 3.8% 60.6% 101 4.0% runtime.settype_flush
88 3.5% 64.1% 988 39.1% runtime.mallocgc
每一列的含义: 采样数,采样比,累计采样比,出现数,出现比,函数名
出现:(正在运行或等待调用函数返回)
如果想以第四/第五列来排序:
(pprof) top5 -cum
Total: 2525 samples
0 0.0% 0.0% 2144 84.9% gosched0
0 0.0% 0.0% 2144 84.9% main.main
0 0.0% 0.0% 2144 84.9% runtime.main
0 0.0% 0.0% 2124 84.1% main.FindHavlakLoops
268 10.6% 10.6% 2124 84.1% main.FindLoops
注:In fact the total for `main.FindLoops` and `main.main` should have been 100%, but each stack sample only includes the bottom 100 stack frames; during about a quarter of the samples, the recursive `main.DFS` function was more than 100 frames deeper than `main.main` so the complete trace was truncated.
web命令 生成SVG格式状态分析图,如下:
箭头表示调用关系,箭头上的数字表示调用的次数
查看具体某个函数的源码:
(pprof) list DFS
Total: 2525 samples
ROUTINE ====================== main.DFS in /home/rsc/g/benchgraffiti/havlak/havlak1.go
119 697 Total samples (flat / cumulative)
3 3 240: func DFS(currentNode *BasicBlock, nodes []*UnionFindNode, number map[*BasicBlock]int, last []int, current int) int {
1 1 241: nodes[current].Init(currentNode, current)
1 37 242: number[currentNode] = current
. . 243:
1 1 244: lastid := current
89 89 245: for _, target := range currentNode.OutEdges {
9 152 246: if number[target] == unvisited {
7 354 247: lastid = DFS(target, nodes, number, last, lastid+1)
. . 248: }
. . 249: }
7 59 250: last[number[currentNode]] = lastid
1 1 251: return lastid
可以看到采样情况精确到代码行。通过样本数,我们可以定位到热点行,然后考虑适合的优化策略; 除了递归占用较多的运行,还有242,250,246占用了采样占比也不少,当可以用slice或array的时候就不要用map。
var memprofile = flag.String("memprofile", "", "write memory profile to this file") ... if *memprofile != "" { f, err := os.Create(*memprofile) if err != nil { log.Fatal(err) } pprof.WriteHeapProfile(f) f.Close() return }
查看命令同cpuprofile
(pprof) top5
Total: 82.4 MB
56.3 68.4% 68.4% 56.3 68.4% main.FindLoops
17.6 21.3% 89.7% 17.6 21.3% main.(*CFG).CreateNode
8.0 9.7% 99.4% 25.6 31.0% main.NewBasicBlockEdge
0.5 0.6% 100.0% 0.5 0.6% itab
0.0 0.0% 100.0% 0.5 0.6% fmt.init
这里显示了函数当前大致分配的内存。第一行main.FindLoops分配了56.3M/82.4MB的内存。
类似 CPU profiling,通过 list 命令查看函数具体的内存分配情况:
(pprof) list FindLoops
Total: 82.4 MB
ROUTINE ====================== main.FindLoops in /home/rsc/g/benchgraffiti/havlak/havlak3.go
56.3 56.3 Total MB (flat / cumulative)
...
1.9 1.9 268: nonBackPreds := make([]map[int]bool, size)
5.8 5.8 269: backPreds := make([][]int, size)
. . 270:
1.9 1.9 271: number := make([]int, size)
1.9 1.9 272: header := make([]int, size, size)
1.9 1.9 273: types := make([]int, size, size)
1.9 1.9 274: last := make([]int, size, size)
1.9 1.9 275: nodes := make([]*UnionFindNode, size, size)
. . 276:
. . 277: for i := 0; i < size; i++ {
9.5 9.5 278: nodes[i] = new(UnionFindNode)
. . 279: }
...
. . 286: for i, bb := range cfgraph.Blocks {
. . 287: number[bb.Name] = unvisited
29.5 29.5 288: nonBackPreds[i] = make(map[int]bool)
. . 289: }
...
一眼就可以看出哪里可以优化
(pprof) web mallocgc
很容易看出从FindLoops产生大量的内存分配,虽然golang是自带垃圾回收的
当两个goroutine并发访问同一个变量,且至少一个goroutine对变量进行写操作时,就会发生数据竞争(data race)。为了协助诊断这种bug,Go提供了一个内置的数据竞争检测工具。通过传入-race选项,go tool就可以启动竞争检测。
$ go test -race mypkg // to test the package
$ go run -race mysrc.go // to run the source file
$ go build -race mycmd // to build the command
$ go install -race mypkg // to install the package
//testrace.go package main import "fmt" import "time" func main() { var i int = 0 go func() { for { i++ fmt.Println("subroutine: i = ", i) time.Sleep(1 * time.Second) } }() for { i++ fmt.Println("mainroutine: i = ", i) time.Sleep(1 * time.Second) } }
执行可以看到如下输出
$go run -race testrace.go
mainroutine: i = 1
WARNING: DATA RACE
Read by goroutine 5:
main.func·001()
/Users/brant/Test/Go/testrace.go:10 +0×49
Previous write by main goroutine:
main.main()
/Users/brant/Test/Go/testrace.go:17 +0xd5
Goroutine 5 (running) created at:
main.main()
/Users/brant/Test/Go/testrace.go:14 +0xaf
==================
subroutine: i = 2
mainroutine: i = 3
subroutine: i = 4
mainroutine: i = 5
subroutine: i = 6
…
GODEBUG=gctrace=1 ./myProgram (2 > ./gc.prof) 设置环境变量
从统计情况来看,gc延时在20ms级,对于业务没什么影响。目前利用提前分配缓存的方式,做对比测试
缓存前:
gc24(4): 11+4+15320+1 us, 65 -> 131 MB, 997302 (7313677-6316375) objects, 10528/3680/0 sweeps, 11(701) handoff, 16(134) steal, 136/84/84 yields
缓存后:
gc9(4): 19+7+22589+2 us, 255 -> 510 MB, 3118992 (3119052-60) objects, 34311/0/0 sweeps, 21(925) handoff, 17(182) steal, 157/82/99 yields
说明:
第几次(参与gc的工作线程数);
gc时间(stop the world)+ sweeping + marking + waiting for worker threads to finish
内存(heap)变化;
对象数(共分配—共释放);
描述sweep阶段: total 10528 memory spans /3620 were swept on demand or in background/0 were swept during stop-the-world phase ,剩下是未使用的spans
类似cpu profiler,我们用runtime/pprof.Lookup("block").WriteTo. 来记录程序运行中的block信息。
并非所有的阻塞都是不利的。当一个goroutine阻塞时,底层的工作线程就会简单地转换到另一个goroutine。所以Go并行环境下的阻塞 与非并行环境下的mutex的阻塞是有很大不同的。
在 time.Ticker上发生的阻塞通常是可行的,如果一个goroutine阻塞Ticker超过十秒,你将会在profile中看到有十秒的阻塞,这 是很好的。发生在sync.WaitGroup上的阻塞经常也是可以的,例如,一个任务需要十秒,等待WaitGroup完成的goroutine会在 profile中生成十秒的阻塞。发生在sync.Cond上的阻塞可好可坏,取决于情况不同。消费者在通道阻塞表明生产者缓慢或不工作。生产者在通道阻塞,表明消费者缓慢,但这通常也是可以的。在基于通道的信号量发生阻塞,表明了限制在这个信号量上的goroutine的数量。发生在sync.Mutex或sync.RWMutex上的阻塞通常是不利的。你可以在可视化过程中,在pprof中使用--ignore标志来从profile中排除已知的无关阻塞。
过多的goroutine阻塞/解除阻塞消耗了CPU时间,帮助减少goroutine阻塞。
1.在生产者--消费者情景中使用充足的缓冲通道。无缓冲的通道实际上限制了程序的并发可用性。
2. 针对于主要为读取的工作量,使用sync.RWMutex而不是sync.Mutex。因为读取操作在sync.RWMutex中从来不会阻塞其它的读取操作。甚至是在实施级别。
3.在某些情况下,可以通过使用copy-on-write技术来完全移除互斥。如果受保护的数据结构很少被修改,可以为它制作一份副本,然后就可以这样更新它:
type Config struct { Routes map[string]net.Addr Backends []net.Addr } var gConfig Config var config unsafe.Pointer // actual type is *Config config=unsafe.Pointer(&gConfig) // Worker goroutines use this function to obtain the current config. func CurrentConfig() *Config { return (*Config)(atomic.LoadPointer(&config)) } // Background goroutine periodically creates a new Config object // as sets it as current using this function. func UpdateConfig(cfg *Config) { atomic.StorePointer(&config, unsafe.Pointer(cfg)) }
这种模式防止在更新时写入阻塞读取。
4.分割是另一种用于减少共享可变数据结构竞争和阻塞的通用技术。下面是一个展示如何分割哈希表(hashmap)的例子:
type Partition struct { sync.RWMutex m map[string]string } const partCount = 64 var m [partCount]Partition func Find(k string) string { idx := hash(k) % partCount part := &m[idx] part.RLock() v := part.m[k] part.RUnlock() return v }
5.使用缓存池
func (p *Pool) Get() interface{}
func (p *Pool) Put(x interface{})
Alloc Trace
GODEBUG=allocfreetrace=1
记录每次内存分配和释放情况
tracealloc(0xc208062500, 0x100, array of parse.Node)
goroutine 16 [running]:
runtime.mallocgc(0x100, 0x3eb7c1, 0x0)
runtime/malloc.goc:190 +0x145 fp=0xc2080b39f8
runtime.growslice(0x31f840, 0xc208060700, 0x8, 0x8, 0x1, 0x0, 0x0, 0x0)
runtime/slice.goc:76 +0xbb fp=0xc2080b3a90
text/template/parse.(*Tree).parse(0xc2080820e0, 0xc208023620, 0x0, 0x0)
text/template/parse/parse.go:289 +0x549 fp=0xc2080b3c50
...
tracefree(0xc208002d80, 0x120)
goroutine 16 [running]:
runtime.MSpan_Sweep(0x73b080)
runtime/mgc0.c:1880 +0x514 fp=0xc20804b8f0
runtime.MCentral_CacheSpan(0x69c858)
runtime/mcentral.c:48 +0x2b5 fp=0xc20804b920
runtime.MCache_Refill(0x737000, 0xc200000012)
runtime/mcache.c:78 +0x119 fp=0xc20804b950
...
说明:内存块地址,内存大小,类型,goroutine id,堆栈
goroutine是非常轻量级的,它就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈, Go运行库最多会启动$GOMAXPROCS个线程来运行goroutine
GODEBUG=schedtrace=1000 ./myserver
SCHED 1004ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=4 runqueue=8 [0 1 0 3]
SCHED 2005ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=5 runqueue=6 [1 5 4 0]
SCHED 3008ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=4 runqueue=10 [2 2 2 1]
时间跨度,max process,idle process,工作线程数,空闲线程,runable_gorouting全局队列长度, 可执行的 goroutine 的预处理器队列的长度