让咱们假设你有一golang 程序,想改善其性能。有几种工具能够帮咱们完成这个任务。这些工具能够帮咱们识别程序中的热点(cpu,io,memory), 热点便是那些须要咱们集中精力于其上,能显著改善改善性能的地方。然而,另一种结果也是可能的,工具帮咱们识别出程序里的多种性能缺陷。好比,每次查询数据库,你都准备sql 语句,然而,你能够在程序启动时,只准备一次。另外一个例子,一个O(n^2)的算法莫名其妙的溜进,某些存在O(n) 算法的地方。为了识别出这些状况,你须要合理检查程序剖析所看到的结果。好比第一个例子中,有显著的时间花费在sql 语句准备阶段,这就是一个危险的信号。php
了解多种关于性能的边界因素也是重要的。好比,你的程序使用100Mbps网络链路通讯,它已经使用了链路90Mbps以上的带宽,那你的程序就没有太多性能改善空间。对于磁盘io,内存消耗,计算性任务也存在相似的状况。这些状况,铭记在心,接着咱们能够查看可用的工具。
java
注意:工具之间会相互干扰,好比,精准的内存剖析会误差cpu剖析,goroutine阻塞剖析影响调度器的追踪等,隔离地使用工具可以得到更精确的信息。如下阐述基于golang 1.3。linux
cpu 剖析器c++
go运行时内置cpu profiler,它能够显示哪些函数消耗了多少的百分比cpu时间,有三种方式能够访问它:golang
1.最简单的是go test 命令-cpuprofile标志,例如,如下命令:
web
$ go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http算法
剖析给定的benchmark,把cpu profile 信息写入‘cprof' 文件
sql
接着:
数据库
$ go tool pprof --text http.test cprof
数组
打印最热点的函数列表,有几种可用输出格式,最有用的几个:--text,--web,--list.运行 'go tool pprof' 获取完整列表
2.net/http/pprof 包,这个方案对于网络服务应用十分理想,你仅需导入net/http/pprof,就可以使用下面命令profile:
go tool pprof --text mybin http://myserver:6060:/debug/pprof/profile
3.手动profile 采集,须要引入runtime/pprof,在main函数添加下述代码:
if *flagCpuprofile != "" { f, err := os.Create(*flagCpuprofile) if err != nil { log.Fatal(err) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() }
剖析信息会写入指定的文件,与第一个选项一样方式可视化它。这里有一个使用--web 选项可视化的例子:
你可使用--list=funcname 选项查阅单个函数,列如如下profile 显示时间append 函数时间花费:
. . 93: func (bp *buffer) WriteRune(r rune) error { . . 94: if r < utf8.RuneSelf { 5 5 95: *bp = append(*bp, byte(r)) . . 96: return nil . . 97: } . . 98: . . 99: b := *bp . . 100: n := len(b) . . 101: for n+utf8.UTFMax > cap(b) { . . 102: b = append(b, 0) . . 103: } . . 104: w := utf8.EncodeRune(b[n:n+utf8.UTFMax], r) . . 105: *bp = b[:n+w] . . 106: return nil . . 107: }
当剖析器不能解开调用栈时,使用三个特殊条目使用:GC,System,ExternalCode。GC 表示花费在垃圾回收的时间;System表示花费在goroutine 调度器,栈管理及其余辅助运行时代码的时间;ExternalCode,表示花费在调用本地动态库时间。这里有一些关于如何解释profile结果的提示:
若是你看到大量时间花费在runtime.mallocgc 函数,程序可能作了过多的小内存分配。profile能告诉你这些分配来自哪里。
若是大量时间花费在channel,sync.Mutex,其余的同步原语,或者系统组件,程序可能遭受资源争用。考虑如下重新组织代码结构,消除最常访问的共享资源。经常使用的技术包括分片/分区, 局部缓冲/汇集, 写时拷贝等。
若是大量时间花费在syscall.Read/Write, 程序可能产生了太多小数据量读写操做,可考虑使用bufio 包裹os.File or net.Conn。
若是大量时间花费在GC 组件,要么是程序分配了太多的临时对象,要么是堆内存过小,致使垃圾收集操做运行频繁。
注意:在当前的darwin 平台,cpu profiler 不能正确工做 darwin 不能正常工做;window 平台须要安装cygwin,perl,graphviz,用来生成svg/web profile;在linux平台,你也可以使用perf system profiler,它不能解开go stack,但可剖析解开cgo/swig 代码和内核代码。
memory profiler
内存剖析器显示哪些函数分配堆内存,你能够采集它,使用’go test --memprofile', 或者net/http/ppro经由 http://myserver:6060:/debug/pprof/heap 或者调用 runtime/pprof.WriteHeapProfile。
你能够仅仅可视化profile 收集过程当中的活跃分配(传递 --inuse_space 标志,默认),或者自程序启动以来的全部分配(--alloc_space )。
你能够显示分配了多少字节或者多少个对象(--inuse/alloc_space or --inuse/alloc_objects )。屡次剖析过程当中,profiler 趋向于采样更大的对象。了解大对象影响内存消耗和gc 时间,大量小分配影响执行速度也在某种程度影响gc时间。对象能够是持久或临时的生命周期。若是在程序启动时,你有几个大的持久对象分配,它们极可能被采集到。这些对象影响内存消耗和gc时间,但不影响正常的执行速度。另外一方面,若是你有大量短生命周期对象,那么profile过程当中,它们几乎不能被呈现。但它们对执行速度有显著影响,由于它们被分配释放很频繁。
通常状况是,若是你要减小内存消耗,考虑使用--inuse_space 选项的profile;若是是想要改善执行速度,使用--alloc_objects 选项profile。有几个选项能够用来控制报告的粒度,--function函数级别(默认),--lines,--files,--adrresses,行级,文件级,指令地址级。
优化一般是应用特定的,如下是一些经常使用建议:
1.合并对象进入更大的对象。例如,使用bytes.Buffer替代*bytes.Buffer,做为结构体成员,这个能减小内存分配次数,减轻gc压力。
2.那些脱离它们声明做用域的局部变量,被提高的堆内存分配。编译器一般不能断定几个这样变量有相同的生命周期,因此要单独分配它们。能够把下面代码:
for k, v := range m { k, v := k, v // copy for capturing by the goroutine go func() { // use k and v }() }
替换为:
for k, v := range m { x := struct{ k, v string }{k, v} // copy for capturing by the goroutine go func() { // use x.k and x.v }() }
使用一次分配代替两次,但对代码可读性有消极影响,因此适度使用。
3.一个合并分配的特例,若是你了解使用中slice典型大小,slice的底层数组可使用预分配。
type X struct { buf []byte bufArray [16]byte // Buf usually does not grow beyond 16 bytes. } func MakeX() *X { x := &X{} // Preinitialize buf with the backing array. x.buf = x.bufArray[:0] return x }
4.使用占用空间小的数据类型, 好比,使用 int8 替代 int。
5.那些不包含任何指针的对象(注意:string,slice,map,chan 包含隐式指针),不会被垃圾收集器扫描。好比 1Gb byte slice 不会影响gc time,从活跃使用的对象中移除指针,能对gc time 产生正面影响。一些可能方式:使用索引替代指针,分割对象成两部分,其中一部分没有任何指针。
6.使用freelist 重用临时对象,减小分配次数。标准库包含的sync.Pool 类型容许在gc 之间屡次重用同一个对象。不过要意识到,就像任何手动内存管理模式同样,不正确地使用sync.Pool 能够致使 use-after-free bug。
Blocking Profile
goroutine 阻塞 profiler展现goroutine 阻塞等待同步原语(包括定时器channel),出如今代码中哪些地方。你可使用‘go test --blockprofile', net/http/pprof 经由 http://myserver:6060:/debug/pprof/block,或者调用 runtime/pprof.Lookup("block").WriteTo
阻塞剖析器默认没有被开启,’go test --blockprofile‘ 会为你自动开启,可是使用net/http/pprof, runtime/pprof,须要你手动开启。调用runtime.SetBlockProfileRate,开启阻塞剖析器,SetBlockProfileRate 控制blocking profile 中阻塞时间的报告粒度。
若是一个函数包含多个阻塞操做,了解到那个操做致使阻塞,就变得不太明晰,如此可使用--lines 标志辨别。
注意并非全部的阻塞都是很差的。当一个goroutine阻塞,底层工做线程会切换到另外一个goroutine。这样以来,协做的go环境的阻塞与非协做系统互斥器上的阻塞,就有着显著差别。(c++, java 线程库里,阻塞会致使线程空闲,和昂贵的线程上下文切换)
在time.Ticker 上阻塞一般没什么问题。若是一个goroutine在一个ticker上阻塞10 s,阻塞剖析中也会看到10s阻塞。在sync.WaitGroup 上阻塞,大多也没什么问题。例如,一个任务花费10s,goroutine在waitgroup等待,记帐10s。在sync.Cond 上阻塞是好是坏,取决于具体状况。消费者阻塞在channel 上,暗示生产者的慢速,或者缺乏能够作的工做。生产者阻塞在channel 上,暗示消费者的慢速,一般这也不是个问题。阻塞于channel基于的信号量,显示有多少goroutine被卡在信号量上。阻塞于sync.Mutex, sync,RWMutex, 通常不太好。你可使用--ignore 排除那些不感兴趣的阻塞事件。
goroutine的阻塞产生两种消极后果:
1. 程序不能与处理器线性比例伸缩。
2. 过多的goroutine阻塞与解阻塞,消耗太多cpu时间。
如下是一些提示帮助减小goroutine阻塞:
1. 在匹配生产者,消费者模型代码中,使用充足缓冲的 buffer channel,无缓冲channel实质上限制了程序的并行度。
2. 在有不少读取操做, 不多修改数据操做的场景,使用sync.RWMutex 代替 sync.Mutex。读者之间不会相互阻塞。
3. 某些场景甚至能够经过使用写时拷贝技术彻底移除mutex。若是被保护的数据结构修改的 不太频繁,制造一份拷贝是可行的:
type Config struct {
Routes map[string]net.Addr Backends []net.Addr } var config unsafe.Pointer // actual type is *Config // 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. 局部缓冲和批量更新能够帮助减小不可分区的数据结构上争用。
const CacheSize = 16 type Cache struct { buf [CacheSize]int pos int } func Send(c chan [CacheSize]int, cache *Cache, value int) { cache.buf[cache.pos] = value cache.pos++ if cache.pos == CacheSize { c <- cache.buf cache.pos = 0 } }
这个技术并不限于channel上,能够用在批量更新map, 批量分配内存。
6. 使用sync.Pool 的freelist ,而非基于channel,或mutex 保护的 freelist。sync.Pool 内部用了一些聪明技术减小阻塞。
Goroutine Profiler
goroutine 剖析器仅是让你看到进程全部活跃goroutine的当前堆栈,这个对于调试负载均衡及死锁问题,十分方便。
goroutine profile 仅对运行中的应用程序,显得合理,因此go test 命令作不到这点。你可使用 net/http/pprof 经由 http://myserver:6060:/debug/pprof/goroutine 或者调用 runtime/pprof.Lookup("goroutine").WriteTo 。可是最有用的方法是在浏览器里键入 http://myserver:6060:/debug/pprof/goroutine?debug=2, 你会看到相似程序崩溃时的堆栈追踪。注意显示 “syscall" 状态的goroutine 消费os 线程,其余goroutine不会。”io wait“ 状态的goroutine 也不消费os 线程,它们停靠在非阻塞的网络轮询器上。