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

标签: 程序开发 golang | 发表时间:2016-04-03 10:01 | 作者:admin
出处:http://blog.haohtml.com

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

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

转自: http://mikespook.com/2014/12/理解-go-语言的内存使用/

相关 [翻译 理解 go] 推荐:

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

- - 学习笔记
许多人在刚开始接触 Go 语言时,经常会有的疑惑就是“为什么一个 Hello world 会占用如此之多的内存. Understanding Go Lang Memory Usage 很好的解释了这个问题. 不过“简介”就是“简介”,更加深入的内容恐怕要读者自己去探索了. 另外,文章写到最后,作者飘了,估计引起了一些公愤,于是又自己给自己补刀,左一刀,右一刀…….

[翻译]十条有用的 Go 技术

- - Gopher beyond Eliphants
这里是我过去几年中编写的大量 Go 代码的经验总结而来的自己的最佳实践. 某个应用需要适配一个灵活的环境. 你不希望每过 3 到 4 个月就不得不将它们全部重构一遍. 许多人参与开发该应用,它应当可以被理解,且维护简单. 许多人使用该应用,bug 应该容易被发现并且可以快速的修复. 我用了很长的时间学到了这些事情.

[翻译] Go 编程语言,或者:为什么除了它,其他类C语言都是垃圾(一)

- 南洋 - python.cn(jobs, news)
这是关于 Robert Griesemer,Rob Pike 和 Ken Thompson 在 Google 从 2007 年开发的 Go 语言的回顾. 现在,Ian Taylor,Russ Cox 和 Andrew Gerrand 已经加入了核心团队. 这是一个类 C 语言,有一些动态脚本语言的特性,并且提供了一些关于并行和面向对象的新奇的(至少在通用语言领域)特性.

Go和HTTPS

- - Tony Bai
近期在构思一个产品,考虑到安全性的原因,可能需要使用到 HTTPS协议以及双向数字证书校验. 之前只是粗浅接触过HTTP( 使用Golang开 发微信系列). 对HTTPS的了解则始于那次 自行搭建ngrok服务,在那个过程中照猫画虎地为服务端生成了一些私钥和证书,虽然结果是好 的:ngrok服务成功搭建起来了,但对HTTPS、数字证书等的基本原理并未求甚解.

Valve宣布CS: GO

- 小D - Solidot
此前媒体曾报告说Valv邀请CSS玩家和社区代表访问其总部,现在谜团已经解开:Valv宣布了团队射击游戏Counter-Strike: Global Offensive,它将在2012年初登陆Steam(PC和Mac)、PS3和Xbox360. CS: GO将是12年前发布的CS的真正扩展,而不是类似CS:Source的引擎更新,它提供了新的地图、角色、武器,经典CS地图(如de_dust),新的游戏模式,配对比赛和排名榜等.

Go 语言初步

- wei - 云风的 BLOG
所谓认真玩,就是拿 Go 写点程序,前后大约两千行吧. 据说 Go 的最佳开发平台是 Mac OS ,我没有. Windows 版还没全部搞定,但是也可以用了. 如果你用 google 搜索,很容易去到一个叫 go-windows 的开源项目上. 如果你用这个,很多库都没有,而且语法也是老的. 我在 Windows 下甚至不能正确链接自己写的多个 package.

Go 1.1 的性能提升

- - 博客 - 伯乐在线
伯乐在线注:今天上午在微博推荐了英文原文,感谢 @Codefor 的热心翻译. 如果其他朋友也有不错的原创或译文,可以尝试 推荐给我们. 这是Go1.1发布后性能提升分析系列的第一篇文章. Go官方文档( 这里和 这里)报告说,用Go1.1重新编译你的代码就可以获得30%-40%的性能提升.

采访:关于 Go 语言和《Go Web编程》

- - 开源中国社区最新新闻
最近,在网上出现了一本名为《Go Web编程》的书籍,里面详细地讲述了使用Go语言进行Web编程的各个方面. 很特别的是,这本书是在GitHub上以开源的方式撰写的. 日前,InfoQ采访了这本书的作者谢孟军先生,请他来和大家谈谈Go语言以及他撰写的开源书籍. InfoQ:请您先简单和大家介绍一下自己.

《学习Go语言》0.4 中文版

- way - python.cn(jobs, news)
鱼哥(https://twitter.com/#!/smallfishxy)上个月勒令我要完成 0.4 版的翻译. 之前公司重组的时候,没顾上看英文版本的更新,结果这老外不声不响的做了如此之多的改动……. 于是只好人工 diff,一条一条的对比 commit 内容. 总算是跟进到了 0.4 这个 tag.

Skype 被強制安裝 EasyBits Go

- MorrisC - 高登工作室
這是由網友 LuLu CHEN 所提供的資訊,我自己的Skype是沒有出現這樣子的訊息,如果你也是被強制安裝此一程式的話,請照著本篇的方法將它移除吧. Spype是大家最常用的語音IM即時通訊軟體,這個月初Skype還以85億美元的天價嫁給了微軟,但是怎麼送給大家的禮物卻是一個大家不想要的流氓軟體.