IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    mikespook发表于 2014-12-24 09:41:14
    love 0

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



沪ICP备19023445号-2号
友情链接