[翻译]理解 Go 语言的内存使用

许多人在刚开始接触 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)
}

然后他们就完全崩溃了。

high_mem

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 个图表包括表格、计数器、点线图和直方图。作为编写软件的我们,应当更加关注找到真正的问题,并提出真正的解决方案。

Join the Conversation

4 Comments

  1. ReadMemStats中哪个字段可以反映出RSS的值呢?或者相对比较吻合的

Leave a comment

Your email address will not be published. Required fields are marked *