许多人在刚开始接触 Go 语言时,经常会有的疑惑就是“为什么一个 Hello world 会占用如此之多的内存?”。Understanding Go Lang Memory Usage 很好的解释了这个问题。不过“简介”就是“简介”,更加深入的内容恐怕要读者自己去探索了。另外,文章写到最后,作者飘了,估计引起了一些公愤,于是又自己给自己补刀,左一刀,右一刀……
————翻译分隔线————
理解 Go 语言的内存使用
2014年12月22日,星期一
温馨提示:这仅是关于 Go 语言内存的简介,俗话说不入虎穴、焉得虎子,读者可以进行更加深入的探索。
大多数 Go 开发者都会尝试像这样简单的 hello world 程序:
package main import ( "fmt" "time" ) func main() { fmt.Println("hi") time.Sleep(30 * time.Second) }
然后他们就完全崩溃了。
f!*%!$%@# 138 G?!这个笔记本也只有 16 G 内存!
虚拟内存 vs 常驻内存
Go 管理内存的方式可能与你以前使用的方式不太一样。它会在一开始就保留一大块 VIRT,而 RSS 与实际内存用量接近。
RSS 和 VIRT 之间有什么区别呢?
VIRT 或者虚拟地址空间大小是程序映射并可以访问的内存数量。
RSS 或者常驻大小是实际使用的内存数量。
如果你对 Go 到底是怎么实现的感兴趣,来看看这个:
https://github.com/golang/go/blob/master/src/runtime/malloc1.go
// 在 64 位设备中,从单一的连续保留地址中分配。 // 当前来说 128 GB (MaxMem) 应当是足够了。 // 实际上我们保留了 136 GB(因为最终位映射会使用 8 GB)
务必注意,如果你使用 32 位的架构,内存保留机制是完全不同的。
垃圾收集
现在我们已经清楚了常驻内存和共享内存的区别,可以来谈谈 Go 进行垃圾收集的机制,以便了解我们的程序是如何工作的。
设想你正在编写一个长期运行的后台服务,就让它是一个 web 应用服务或者某些更复杂的东西。通常来说,在整个运行周期都会需要分配内存。了解如何处理这些内存是必要的。
通常,每 2 分钟会执行一次垃圾收集。如果某个片段持续 5 分钟都没有被使用,回收器会将其释放。
因此,如果你认为内存使用会降低,那么 7 分钟之后再去确认吧。
需要注意的是,当前 gc 是非压缩的,也就是说如果你在某个页面有一个字节正在使用,回收器会拒绝释放这个页面。
最后,也是最重要的,Go 1.3 的 goroutine 栈有 8k/pop 的空间不会被释放,它们随后会被重用。不用担心,Go 在 GC 的部分还有很大的改进空间。因此,如果你的代码会产生大量的 goroutine,并且 RES 居高不下的话,这可能就是原因。
好了,现在我们已经知道了程序从外部看到的样子以及期望 GC 做的工作。
分析内存使用
现在通过一个小例子来看看如何了解内存使用。在这个例子中,我们将分配 10 组 100 兆字节的内存。
然后会用多种方式来了解内存的使用。
一个方法是通过 runtime 包的 ReadMemStats 函数。
另一个方法是通过 pprof 包提供的 web 接口。这允许我们远程获得程序的 pprof 数据,稍候会详细解释。
还有一种方法是我们必须介绍的是 Dave Cheney 提到的,使用 gctrace 调试环境变量。
注意:这些都在 64 位 Linux 下的 Go 1.4 环境下完成。
package main import ( "log" "net/http" _ "net/http/pprof" "runtime" "sync" ) func bigBytes() *[]byte { s := make([]byte, 100000000) return &s } func main() { var wg sync.WaitGroup go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() var mem runtime.MemStats runtime.ReadMemStats(&mem) log.Println(mem.Alloc) log.Println(mem.TotalAlloc) log.Println(mem.HeapAlloc) log.Println(mem.HeapSys) for i := 0; i < 10; i++ { s := bigBytes() if s == nil { log.Println("oh noes") } } runtime.ReadMemStats(&mem) log.Println(mem.Alloc) log.Println(mem.TotalAlloc) log.Println(mem.HeapAlloc) log.Println(mem.HeapSys) wg.Add(1) wg.Wait() }
在使用 pprof 查看内存的时候,通常会用到两个选项。
一个选项是“-alloc_space”,用于告诉你已经分配了多少内存。
另一个是“-inuse_space”,用于获得正在使用的内存的数量。
可以运行 pprof 并将其指向我们内建的 web 服务来获得最高的内存消耗。
并且还可以使用 list 来了解那里使用了这些内存:
使用
vagrant@vagrant-ubuntu-raring-64:~/blahdo$ go tool pprof -inuse_space blahdo http://localhost:6060/debug/pprof/heap Fetching profile from http://localhost:6060/debug/pprof/heap Saved profile in /home/vagrant/pprof/pprof.blahdo.localhost:6060.inuse_objects.inuse_space.025.pb.gz Entering interactive mode (type "help" for commands) (pprof) top5 190.75MB of 191.25MB total (99.74%) Dropped 3 nodes (cum <= 0.96MB) flat flat% sum% cum cum% 190.75MB 99.74% 99.74% 190.75MB 99.74% main.main 0 0% 99.74% 190.75MB 99.74% runtime.goexit 0 0% 99.74% 190.75MB 99.74% runtime.main (pprof) quit
分配
vagrant@vagrant-ubuntu-raring-64:~/blahdo$ go tool pprof -alloc_space blahdo http://localhost:6060/de bug/pprof/heap Fetching profile from http://localhost:6060/debug/pprof/heap Saved profile in /home/vagrant/pprof/pprof.blahdo.localhost:6060.alloc_objects.alloc_space.027.pb.gz Entering interactive mode (type "help" for commands) (pprof) top5 572.25MB of 572.75MB total (99.91%) Dropped 3 nodes (cum <= 2.86MB) flat flat% sum% cum cum% 572.25MB 99.91% 99.91% 572.25MB 99.91% main.main 0 0% 99.91% 572.25MB 99.91% runtime.goexit 0 0% 99.91% 572.25MB 99.91% runtime.main
排行榜已经相当不错了,不过更好的是 list 命令,可以在上下文中看到消耗是如何影响程序的其他部分的。
(pprof) list Total: 572.75MB ROUTINE ======================== main.main in /home/vagrant/blahdo/main.go 572.25MB 572.25MB (flat, cum) 99.91% of Total . . 23: var mem runtime.MemStats . . 24: runtime.ReadMemStats(&mem) . . 25: log.Println(mem.Alloc) . . 26: . . 27: for i := 0; i < 10; i++ { 572.25MB 572.25MB 28: s := bigBytes() . . 29: if s == nil { . . 30: log.Println("oh noes") . . 31: } . . 32: } . . 33:
聪明的读者可能已经发现在上面的内存使用报告中,存在一些差异。为什么会这样呢?
让我们来看看进程:
vagrant@vagrant-ubuntu-raring-64:~$ ps aux | grep blahdo vagrant 4817 0.2 10.7 699732 330524 pts/1 Sl+ 00:13 0:00 ./blahdo
现在来看看日志输出:
./vagrant@vagrant-ubuntu-raring-64:~/blahdo$ ./blahdo 2014/12/23 00:19:37 279672 2014/12/23 00:19:37 336152 2014/12/23 00:19:37 279672 2014/12/23 00:19:37 819200 2014/12/23 00:19:37 300209920 2014/12/23 00:19:37 1000420968 2014/12/23 00:19:37 300209920 2014/12/23 00:19:37 500776960
最后,来看看使用 gctrace 的效果:
vagrant@vagrant-ubuntu-raring-64:~/blahdo$ GODEBUG=gctrace=1 ./blahdo gc1(1): 1+0+95+0 us, 0 -> 0 MB, 21 (21-0) objects, 2 goroutines, 15/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc2(1): 0+0+81+0 us, 0 -> 0 MB, 52 (53-1) objects, 3 goroutines, 20/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc3(1): 0+0+77+0 us, 0 -> 0 MB, 151 (169-18) objects, 4 goroutines, 25/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc4(1): 0+0+110+0 us, 0 -> 0 MB, 325 (393-68) objects, 4 goroutines, 33/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc5(1): 0+0+138+0 us, 0 -> 0 MB, 351 (458-107) objects, 4 goroutines, 40/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields 2014/12/23 02:27:14 277960 2014/12/23 02:27:14 332680 2014/12/23 02:27:14 277960 2014/12/23 02:27:14 884736 gc6(1): 1+0+181+0 us, 0 -> 95 MB, 599 (757-158) objects, 6 goroutines, 52/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc7(1): 1+0+454+19 us, 95 -> 286 MB, 438 (759-321) objects, 6 goroutines, 52/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc8(1): 1+0+167+0 us, 190 -> 477 MB, 440 (762-322) objects, 6 goroutines, 54/1/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc9(1): 2+0+191+0 us, 190 -> 477 MB, 440 (765-325) objects, 6 goroutines, 54/1/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields 2014/12/23 02:27:14 300206864 2014/12/23 02:27:14 1000417040 2014/12/23 02:27:14 300206864 2014/12/23 02:27:14 500842496 GC forced gc10(1): 3+0+1120+22 us, 190 -> 286 MB, 455 (789-334) objects, 6 goroutines, 54/31/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields scvg0: inuse: 96, idle: 381, sys: 477, released: 0, consumed: 477 (MB) GC forced gc11(1): 2+0+270+0 us, 95 -> 95 MB, 438 (789-351) objects, 6 goroutines, 54/39/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields scvg1: 0 MB released scvg1: inuse: 96, idle: 381, sys: 477, released: 0, consumed: 477 (MB) GC forced gc12(1): 85+0+353+1 us, 95 -> 95 MB, 438 (789-351) objects, 6 goroutines, 54/37/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
由于大多数运维工具是站在操作系统的角度来对待你的程序,那么了解程序内部实际发生了什么就变得尤为重要了。
更多选项可以参考 runtime 包。
摘录如下:
- RES – 将会显示在当前时刻进程的内存用量,但可能不包含任何尚未换入或已经换出的页面。
- mem.Alloc – 已经被配并仍在使用的字节数
- mem.TotalAlloc – 从开始运行到现在分配的内存总数
- mem.HeapAlloc – 堆当前的用量
- mem.HeapSys – 包含堆当前和已经被释放但尚未归还操作系统的用量
更进一步说,明白 pprof 仅仅是获取了样本,而不是真正的值,是非常重要的。
通常在处理这个情况的时候,不要聚焦于数字本身,而着眼于解决问题。
我们坚信要测量一切,但是同时觉得“现代”运维工具是相当糟糕的,并聚焦于问题的影响,而不是真正的问题。
如果你的车不能发动了,你可能认为这是个问题,但它不是。这甚至不是邮箱空了的表象。真正的问题在于你没有给邮箱加油,但你关注的是最初的问题导致的一系列的结果。
如果对于 Go 程序你只关注来自 ps 的的 RES 值,它可能告诉你这里出问题了,除非你更进一步挖掘,否则没有任何线索可以解决这个问题。我们希望能更正它。
更正:
最后一段未进行编辑。它并不是用来贬低运维或 devops 人员。其目的在于展示应用级别的度量和系统级别的度量。我们已经意识到这里的表达有误,并且对此道歉。我们只是觉得已有的“运维”工具没有为开发者提供充分的信息来修复他们的问题。
我们同时认为当前应用级别的度量工具仍然匮乏。
运维人员扮演着至关重要的角色,对于他们的工作我们至诚的感谢。事实上,是开发人员糟糕的代码把事情搞麻烦了,这也是我们正在着手解决的问题。
最终更正
与其让运维人员拥有超过 300 个图表包括表格、计数器、点线图和直方图。作为编写软件的我们,应当更加关注找到真正的问题,并提出真正的解决方案。