设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

Go程序的性能调试问题

2015-3-30 20:49| 发布者: joejoe0332| 查看: 3204| 评论: 0|原作者: 社会主义好, Micooz, 暗夜在火星, ScriptKid, 霍啸林|来自: oschina

摘要: 假设你手上有个Go语言编写的程序,你打算提升它的性能。目前有一些工具可以为此提供帮助。这些工具能帮你发现包括CPU、IO和内存在内多种类型的热点。所谓热点,是指那些为了能显著提升性能而值得你去关注的地方。有 ...


内存分析器

  内存分析器展示了哪些函数申请了堆内存。你可以通过熟悉的途径来收集这些信息,一如使用CPU分析器:和 'go test --memprofile', 以及通过 http://myserver:6060:/debug/pprof/heap的net/http/pprof 或者通过调用runtime/pprof.WriteHeapProfile


  你仅仅可以可视化描述收集器当前时间内的申请(默认下--inuse_space标识指向pprof),或者自程序启动以来全部的申请(--alloc_space标识指向pprof)。前者对于在当前活动的程序通过net/http/pprof收集描述很有帮助,而后者则对在程序后端(否则你将会看到的几乎都是空的描述)收集描述有帮助。


  温馨提示:内存分析器采取抽样的方式,也就是说,它仅仅从一些内存分配的子集中收集信息。有可能对一个对象的采样与被采样对象的大小成比例。你可以通过使用go test --memprofilerate标识,或者通过程序启动时 的运行配置中的MemProfileRate变量来改变调整这个采样的比例。如果比例为1,则会导致全部申请的信息都会被收集,但是这样的话将会使得执行变慢。默认的采样比例是每512KB的内存申请就采样一次。


  你同样可以将分配的字节数或者分配的对象数形象化(分别是以--inuse/alloc_space和--inuse/alloc_objects为标志)。分析器倾向于在性能分析中对较大的对象采样。但是需要注意的是大的对象会影响内存消耗和垃圾回收时间,大量的小的内存分配会影响运行速度(某种程度上也会影响垃圾回收时间)。所以最好同时考虑它们。


  对象可以是持续的也可以是瞬时的。如果你在程序开始的时候需要分配几个大的持续对象,它们很有可能能被分析器取样(因为它们比较大)这些对象会影响内存消耗量和垃圾回收时间,但它们不会影响正常的运行速度(在它们上没有内存管理操作)。另一方面,如果你有大量持续期很短的对象,它们几乎不会表现在曲线中(如果你使用默认的--inuse_space模式)。但它们的确显著影响运行速度,因为它们被不断地分配和释放。所以再说一遍,最好同时考虑这两种类型的对象。


  所以,大体上,如果你想减小内存消耗量,那么你需要查看程序正常运行时--inuse_space收集的概要。如果你想提升程序的运行速度,就要查看在程序特征运行时间后或程序结束之后--alloc_objects收集的概要。


  报告间隔时间由几个标志控制,--functions让pprof报告在函数等级(默认)。--lines使pprof报告基于代码行等级,如果关键函数分布在不同的代码行上,这将变得很有用。同样还有--addresses和--files选项, 分别定位到精确的指令地址等级和文件等级。


  还有一个对内存概要很有用的选项,你可以直接在浏览器中查看它(需要你导入net/http/pprof包)。你打开http://myserver:6060/debug/pprof/heap?debug=1就会看到堆概要,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
heap profile: 4: 266528 [123: 11284472] @ heap/1048576
1: 262144 [4: 376832] @ 0x28d9f 0x2a201 0x2a28a 0x2624d 0x26188 0x94ca3 0x94a0b 0x17add6 0x17ae9f 0x1069d3 0xfe911 0xf0a3e 0xf0d22 0x21a70
#    0x2a201    cnew+0xc1    runtime/malloc.goc:718
#    0x2a28a    runtime.cnewarray+0x3a            runtime/malloc.goc:731
#    0x2624d    makeslice1+0x4d                runtime/slice.c:57
#    0x26188    runtime.makeslice+0x98            runtime/slice.c:38
#    0x94ca3    bytes.makeSlice+0x63            bytes/buffer.go:191
#    0x94a0b    bytes.(*Buffer).ReadFrom+0xcb        bytes/buffer.go:163
#    0x17add6    io/ioutil.readAll+0x156            io/ioutil/ioutil.go:32
#    0x17ae9f    io/ioutil.ReadAll+0x3f            io/ioutil/ioutil.go:41
#    0x1069d3    godoc/vfs.ReadFile+0x133            godoc/vfs/vfs.go:44
#    0xfe911    godoc.func·023+0x471            godoc/meta.go:80
#    0xf0a3e    godoc.(*Corpus).updateMetadata+0x9e        godoc/meta.go:101
#    0xf0d22    godoc.(*Corpus).refreshMetadataLoop+0x42    godoc/meta.go:141
 
2: 4096 [2: 4096] @ 0x28d9f 0x29059 0x1d252 0x1d450 0x106993 0xf1225 0xe1489 0xfbcad 0x21a70
#    0x1d252    newdefer+0x112                runtime/panic.c:49
#    0x1d450    runtime.deferproc+0x10            runtime/panic.c:132
#    0x106993    godoc/vfs.ReadFile+0xf3            godoc/vfs/vfs.go:43
#    0xf1225    godoc.(*Corpus).parseFile+0x75        godoc/parser.go:20
#    0xe1489    godoc.(*treeBuilder).newDirTree+0x8e9    godoc/dirtrees.go:108
#    0xfbcad    godoc.func·002+0x15d            godoc/dirtrees.go:100


  每个条目开头的数字("1: 262144 [4: 376832]")分别表示目前存活的对象,存活对象占据的内存, 分配对象的个数和所有分配对象占据的内存总量。


  优化工作经常和特定应用程序相关,但也有一些普遍建议。


1. 将小对象组合成大对象。比如, 将 *bytes.Buffer 结构体成员替换为bytes。缓冲区 (你可以预分配然后通过调用bytes.Buffer.Grow为写做准备) 。这将减少很多内存分配(更快)并且减缓垃圾回收器的压力(更快的垃圾回收) 。

2. 离开声明作用域的局部变量促进堆分配。编译器不能保证这些变量拥有相同的生命周期,因此为他们分别分配空间。所以你也可以对局部变量使用上述的建议。比如:将

1
2
3
4
5
6
for k, v := range m {
   k, v := k, v   // copy for capturing by the goroutine
   go func() {
       // use k and v
   }()
}

替换为:

1
2
3
4
5
6
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. 组合内存分配的一个特殊情形是分片数组预分配。如果你清楚一个特定的分片的大小,你可以给末尾数组进行预分配:

1
2
3
4
5
6
7
8
9
10
11
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. 不包含任何指针的对象(注意 strings,slices,maps 和 chans 包含隐含指针)不会被垃圾回收器扫描到。比如,1GB 的分片实际上不会影响垃圾回收时间。因此如果你删除被频繁使用的对象指针,它会对垃圾回收时间造成影响。一些建议:使用索引替换指针,将对象分割为其中之一不含指针的两部分。

6. 使用释放列表来重用临时对象,减少内存分配。标准库包含的 sync.Pool 类型可以实现垃圾回收期间多次重用同一个对象。然而需要注意的是,对于任何手动内存管理的方案来说,不正确地使用sync.Pool 会导致 use-after-free bug。

你也可以使用Garbage Collector Trace(见后文)来获取更深层次的内存问题。



阻塞分析器

  阻塞分析器展示了goroutine在等待同步原语(包括计时器通道)被阻塞的位置。你可以用类似CPU分析器的方法来收集这些信息:通过'go test --blockprofile'net/http/pprof(经由h ttp://myserver:6060:/debug/pprof/block) 或者调用 runtime/pprof.Lookup("block").WriteTo

值得警示的是,阻塞分析器默认未激活。'go test --blockprofile' 将为你自动激活它。然而,如果你使用net/http/pprof 或者 runtime/pprof,你就需要手动激活它(否则分析器将不会被载入)。通过调用 runtime.SetBlockProfileRate 来激活阻塞分析器。SetBlockProfileRate 控制着由阻塞分析器报告的goroutine阻塞事件的比率。分析器力求采样出每指定微秒数内,一个阻塞事件的阻塞平均数。要使分析器记录每个阻塞事件,将比率设为1。


  如果一个函数包含了几个阻塞操作而且并没有哪一个明显地占有阻塞优势,那就在pprof中使用--lines标志。

  注意:并非所有的阻塞都是不利的。当一个goroutine阻塞时,底层的工作线程就会简单地转换到另一个goroutine。所以Go并行环境下的阻塞 与非并行环境下的mutex的阻塞是有很大不同的(例如典型的C++或Java线程库,当发生阻塞时会引起线程空载和高昂的线程切换)。为了让你感受一 下,我们来看几个例子。

   在 time.Ticker上发生的阻塞通常是可行的,如果一个goroutine阻塞Ticker超过十秒,你将会在profile中看到有十秒的阻塞,这 是很好的。发生在sync.WaitGroup上的阻塞经常也是可以的,例如,一个任务需要十秒,等待WaitGroup完成的goroutine会在 profile中生成十秒的阻塞。发生在sync.Cond上的阻塞可好可坏,取决于情况不同。消费者在通道阻塞表明生产者缓慢或不工作。生产者在通道阻塞,表明消费者缓慢,但这通常也是可以的。在基于通道的信号量发生阻塞,表明了限制在这个信号量上的goroutine的数量。发生在sync.Mutex或sync.RWMutex上的阻塞通常是不利的。你可以在可视化过程中,在pprof中使用--ignore标志来从profile中排除已知的无关阻塞。


  goroutine的阻塞会导致两个消极的后果:

  1. 程序与处理器之间不成比例,原因是缺乏工作。调度器追踪工具可以帮助确定这种情况。

  2. 过多的goroutine阻塞/解除阻塞消耗了CPU时间。CPU分析器可以帮助确定这种情况(在系统组件中找)。

这里是一些通常的建议,可以帮助减少goroutine阻塞:

  1. 在生产者--消费者情景中使用充足的缓冲通道。无缓冲的通道实际上限制了程序的并发可用性。

  2. 针对于主要为读取的工作量,使用sync.RWMutex而不是sync.Mutex。因为读取操作在sync.RWMutex中从来不会阻塞其它的读取操作。甚至是在实施级别。

  3. 在某些情况下,可以通过使用copy-on-write技术来完全移除互斥。如果受保护的数据结构很少被修改,可以为它制作一份副本,然后就可以这样更新它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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. 本地缓存和更新的批处理有助于减少对不可分解的数据结构的争夺。下面你将看到如何分批处理向通道发送的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
    }
}

这种技术并不仅限于通道,它还能用于批量更新映射(map)、批量分配等等。

6. 针对freelists,使用sync.Pool代替基于通道的或互斥保护的freelists,因为sync.Pool内部使用智能技术来减少阻塞。



酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部