golang 核心开发者 Dmitry Vyukov(1.1 调度器作者) 关于性能剖析
让我们假设你有一golang 程序,想改善其性能。有几种工具可以帮我们完成这个任务。这些工具可以帮我们识别程序中的热点(cpu,io,memory), 热点即是那些需要我们集中精力于其上,能显著改善改善性能的地方。然而,另外一种结果也是可能的,工具帮我们识别出程序里的多种性能缺陷。比如,每次查询数据库,你都准备sql 语句,然而,你可以在程序启动时,只准备一次。另一个例子,一个O(n^2)的算法莫名其妙的溜进,某些存在O(n) 算法的地方。为了识别出这些情况,你需要合理检查程序剖析所看到的结果。比如第一个例子中,有显著的时间花费在sql 语句准备阶段,这就是一个危险的信号。
了解多种关于性能的边界因素也是重要的。比如,你的程序使用100Mbps网络链路通信,它已经使用了链路90Mbps以上的带宽,那你的程序就没有太多性能改善空间。对于磁盘io,内存消耗,计算性任务也存在类似的情况。这些情况,铭记在心,接着我们可以查看可用的工具。
注意:工具之间会相互干扰,比如,精准的内存剖析会偏差cpu剖析,goroutine阻塞剖析影响调度器的追踪等,隔离地使用工具能够获得更精确的信息。以下阐述基于golang 1.3。
cpu 剖析器
go运行时内置cpu profiler,它可以显示哪些函数消耗了多少的百分比cpu时间,有三种方式可以访问它:
1.最简单的是go test 命令-cpuprofile标志,例如,以下命令:
$ go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http
剖析给定的benchmark,把cpu profile 信息写入‘cprof' 文件
接着:
$ 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 线程,它们停靠在非阻塞的网络轮询器上。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。