设为首页收藏本站

LUPA开源社区

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

Go程序的性能调试问题

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

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


Go协程分析器

Go协程分析器简单地提供给你当前进程中所有活跃的Go协程堆栈。它可以方便地调试负载平衡问题(参考下面的调度器追踪章节),或调试死锁。

这个配置仅仅对运行的程序有意义,所以去测试而不是揭露它. 你可以用net/http/pprof通过http://myserver:6060:/debug/pprof/goroutine来收集配置,并将之形象化为svg/pdf或通过调用runtime/pprof.Lookup("goroutine").WriteTo形象化。但最有用的方式是在你的浏览器中键入http://myserver:6060:/debug/pprof/goroutine?debug=2,它将会给出与程序崩溃时相同的符号化的堆栈。

需要注意的是:Go协程“syscall”将会消耗一个OS线程,而其他的Go协程则不会(除了名为runtime.LockOSThread的Go协程,不幸的是,它在配置中是不可见的)。同样需要注意的是在“IO wait”状态的Go协程同样不会消耗线程,他们停驻在非阻塞的网络轮询器(通常稍后使用epoll/kqueue/GetQueuedCompletionStatus来唤醒Go协程)。


垃圾收集器追踪

除了性能分析工具以外,还有另外几种工具可用——追踪器。它们可以追踪垃圾回收,内存分配和goroutine调度状态。要启用垃圾回收器(GC)追踪你需要将GODEBUG=gctrace=1加入环境变量,再运行程序:

$ GODEBUG=gctrace=1 ./myserver

然后程序在运行中会输出类似结果:

1
2
3
gc9(2): 12+1+744+8 us, 2 -> 10 MB, 108615 (593983-485368) objects, 4825/3620/0 sweeps, 0(0) handoff, 6(91) steal, 16/1/0 yields
gc10(2): 12+6769+767+3 us, 1 -> 1 MB, 4222 (593983-589761) objects, 4825/0/1898 sweeps, 0(0) handoff, 6(93) steal, 16/10/2 yields
gc11(2): 799+3+2050+3 us, 1 -> 69 MB, 831819 (1484009-652190) objects, 4825/691/0 sweeps, 0(0) handoff, 5(105) steal, 16/1/0 yields

来看看这些数字的意思。每个GC输出一行。第一个数字("gc9")是GC的编号(这是从程序开始后的第九个GC),在括号中的数字("(2)")是参与GC的工作线程的编号。随后的四个数字("12+1+744+8 us")分别是工作线程完成GC的stop-the-world, sweeping, marking和waiting时间,单位是微秒。接下来的两个数字("2 -> 10 MB")表示前一个GC过后的存活堆大小和当前GC开始前完整的堆(包括垃圾)的大小。再接下来的三个数字 ("108615 (593983-485368) objects")是堆中的对象总数(包括垃圾)和和分配的内存总数以及空闲内存总数。后面的三个数字("4825/3620/0 sweeps")表示清理阶段(对于前一个GC):总共有4825个存储器容量,3620立即或在后台清除,0个在stop-the-world阶段清除(剩余的是没有使用的容量)。再后面的四个数字("0(0) handoff, 6(91) steal")表示在平行的标志阶段的负载平衡:0个切换操作(0个对象被切换)和六个steal 操作(91个对象被窃取)最后的三个数字("16/1/0 yields")表示平行标志阶段的系数:在等候其它线程的过程中共有十七个yield操作。


GC 是 mark-and-sweep 类型。总的 GC 可以表示成:

Tgc = Tseq + Tmark + Tsweep

这里的 Tseq 是停止用户的 goroutine 和做一些准备活动(通常很小)需要的时间;Tmark 是堆标记时间,标记发生在所有用户 goroutine 停止时,因此可以显著地影响处理的延迟;Tsweep 是堆清除时间,清除通常与正常的程序运行同时发生,所以对延迟来说是不太关键的。

标记时间大概可以表示成:

Tmark = C1*Nlive + C2*MEMlive_ptr + C3*Nlive_ptr

这里的 Nlive 是垃圾回收过程中堆中的活动对象的数量,MEMlive_ptr 是带有指针的活动对象占据的内存总量,Nlive_ptr 是活动对象中的指针数量。

清除时间大概可以表示成:

Tsweep = C4*MEMtotal + C5*MEMgarbage

这里的 MEMtotal 是堆内存的总量,MEMgarbage 是堆中的垃圾总量。

下一次垃圾回收发生在程序被分配了一块与其当前所用内存成比例的额外内存时。这个比例通常是由 GOGC 的环境变量(默认值是100)控制的。如果 GOGC=100,而且程序使用了 4M 堆内存,当程序使用达到 8M 时,运行时(runtime)就会再次触发垃圾回收器。这使垃圾回收的消耗与分配的消耗保持线性比例。调整 GOGC,会改变线性常数和使用的额外内存的总量。

只有清除是依赖于堆总量的,且清除与正常的程序运行同时发生。如果你可以承受额外的内存开销,设置 GOGC 到以一个较高的值(200, 300, 500,等)是有意义的。例如,GOGC=300 可以在延迟相同的情况下减小垃圾回收开销高达原来的二分之一(但会占用两倍大的堆)。

GC 是并行的,而且一般在并行硬件上具有良好可扩展性。所以给 GOMAXPROCS 设置较高的值是有意义的,就算是对连续的程序来说也能够提高垃圾回收速度。但是,要注意,目前垃圾回收器线程的数量被限制在 8 个以内。


内存分配器跟踪

内存分配器跟踪只是简单地将所有的内存分配和释放操作转储到控制台。通过设置环境变量“GODEBUG=allocfreetrace=1”就可以开启该功能。输出看起来像下面的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
...

跟踪信息包括内存块地址、大小、类型、执行程序ID和堆栈踪迹。它可能更有助于调试,但也可以给内存分配优化提供非常详细的信息。


调度器追踪

调度器追踪可以提供对 goroutine 调度的动态行为的内视,并且允许调试负载平衡和可扩展性问题。要启用调度器追踪,可以带有环境变量 GODEBUG=schedtrace=1000 来运行程序(这个值的意思是输入的周期,单位 ms,这种情况下是每秒一次):

$ GODEBUG=schedtrace=1000 ./myserver

程序在运行过程中将会输出类似结果:

1
2
3
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]

第一个数字("1004ms")是从程序开始后的时间。Gomaxprocs 是当前的 GOMAXPROCS 值。 Idleprocs 是空载的处理器数(剩下的在执行 Go 代码)。Threads 是调度器产生的工作线程总数(线程有三种状态:执行 Go 代码(gomaxprocs-idleprocs),执行 syscalls/cgocalls,闲置)。Idlethreads是闲置的工作线程数。Runqueue 是运行的 goroutine 的全局队列长度。方括号中的数字("[0 1 0 3]")是可执行的 goroutine 的预处理器队列的长度。全局和局部队列的长度总和表示运行中可用的 goroutine 的总数。



注意:你可以随意组合追踪器,如:GODEBUG = gctrace = 1,allocfreetrace = 1,schedtrace = 1000。

注意:同样有详细的调度器追踪,你可以这样启用它:GODEBUG = schedtrace = 1000,scheddetail = 1。它将会输出每一个 goroutine、工作线程和处理器的详细信息。我们将不会在这里讨论它的格式,因为它主要是给调度器开发者使用;你可以在这里src/pkg/runtime/proc.c找到它的详细信息。

当一个程序不与 GOMAXPROCS 成线性比例和/或没有消耗 100% 的 CPU 时间,调度器追踪就显得非常有用。理想的情况是:所有的处理器都在忙碌地运行 Go 代码,线程数合理,所有队列都有充足的任务且任务是合理均匀的分布的:

gomaxprocs=8 idleprocs=0 threads=40 idlethreads=5 runqueue=10 [20 20 20 20 20 20 20 20]


不好的情况是上面所列的东西并没有完全达到。例如下面这个演示,没有足够的任务来保持所有的处理器繁忙:

gomaxprocs=8 idleprocs=6 threads=40 idlethreads=30 runqueue=0 [0 2 0 0 0 1 0 0]

注意:这里使用操作系统提供的实际CPU利用率作为最终的标准。在 Unix 系操作系统中是 top 命令。在 Windows 系统中是任务管理器。

你可以使用 goroutine 分析器来了解哪些 goroutine 块处于任务短缺状态。注意,只要所有的处理器处于忙绿状态,负载失衡就不是最坏的,它只会导致适度的负载平衡开销。


内存统计

Go 运行时可以通过 runtime.ReadMemStats 函数提供粗糙的内存统计。这个统计同样可以通过http://myserver:6060/debug/pprof/heap?debug=1  底部的net/http/pprof提供。统计资料,点击此处

一些值得关注的地方是:

1. HeapAlloc - 当前堆大小。

2. HeapSys - 总的堆大小。

3. HeapObjects - 堆中对象的总数。

4. HeapReleased - 释放到操作系统中的内存;如果内存超过五分钟没有使用,运行时将会把它释放到操作系统中,你可以通过 runtime/debug.FreeOSMemory 来强制改变这个过程。

5. Sys - 操作系统分配的总内存。

6. Sys-HeapReleased - 程序的有效内存消耗。

7. StackSys - goroutine 栈的内存消耗(注意:一些栈是从堆中分配的,因此没有计入这里,不幸的是,没有办法得到栈的总大小(https://code.google.com/p/go/issues/detail?id=7468))。

8. MSpanSys/MCacheSys/BuckHashSys/GCSys/OtherSys - 运行时为各种辅助用途分配的内存;它们没什么好关注的,除非过高的话。

9. PauseNs - 最后一次垃圾回收的持续时间。


堆倾卸器

最后一个可用的工具是堆倾卸器,它可以将整个堆的状态写入一个文件中,留作以后进行探索。它有助于识别内存泄露,并能够洞悉程序的内存消耗。
首先,你需要使用函数runtime/debug.WriteHeapDump函数编写倾卸器(dump):

1
2
3
 f, err := os.Create("heapdump")
 if err != nil { ... }
 debug.WriteHeapDump(f.Fd())

然后,你既可以将堆以图形化的表现形式保存为.dot文件,也可以将它转换为hprof格式。为了将它保存为.dot文件,你需要执行以下指令:

1
2
$ go get github.com/randall77/hprof/dumptodot
$ dumptodot heapdump mybinary > heap.dot

最后,使用Graphviz工具打开heap.dot文件。
为了将堆转换成hprof格式,需要执行以下指令:

1
2
3
$ go get github.com/randall77/hprof/dumptohprof
$ dumptohprof heapdump heap.hprof
$ jhat heap.hprof

最后,将浏览器导航到http://localhost:7000。

结束语

优化是一个开放的问题,你可以使用很多简单的方法来提高性能。然而,有时优化需要对程序进行完整地重新架构。但我们希望这些工具能够成为你工具箱中一个有价值的新增成员,至少你可以使用它们分析并理解到底发生了什么。

剖析Go程序》是一个很好的教程,它讲解了如何利用CPU和内存分析器来优化简单的程序。








酷毙

雷人

鲜花

鸡蛋

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

最新评论

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

返回顶部