Go进程内存占用那些事(二)

0x01 最简单的Go程序

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("hello")
    for {
        // sleep一下,避免CPU占用过高。
        time.Sleep(1 * time.Second)
    }
}

不含调试符号的二进制大小为1229464Byte,约1.2MiB。

编译出二进制,同样通过readelf工具检查下。看到Program Header中有3个可加载段。

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x0815e7 0x0815e7 R E 0x1000
  LOAD           0x082000 0x0000000000482000 0x0000000000482000 0x091cd0 0x091cd0 R   0x1000
  LOAD           0x114000 0x0000000000514000 0x0000000000514000 0x017ea0 0x0497f0 RW  0x1000

根据Flg可以判断分别对应代码、常量、变量。内存大小分别为:517.47KiB、583.20KiB、293.98KiB。
大约是1.2MiB。

Go的堆和通常意义的进程堆不是等同的。大小关系为

Go堆 < 进程堆

这个分析的目的是:

  1. 找出Go进程堆除了Go堆,还包含了哪些内容?
  2. 为什么看到的Go进程的RSS值,远大于Go的堆?

0x02 详细分析

再通过readelf -W -S main查看,可以确认变量区保存了以下section
类型段见文档elf中解释的。

PROGRBITS:指完全由elf文件中的内容定义。
NOBITS:不占用文件的实际空间,但有真实的偏移。告诉加载器,不用真的去读文件内容。

Section 大小(Bytes) 类型 说明
.go.buildinfo 224 PROGBITS Go的构建信息
.noptrdata 67104 PROGBITS 非指针类型数据,大约65KiB
.data 30608 PROGBITS 可能是指针类型的数据,大约29KiB
.bss 183KiB NOBITS 未初始化全局静态,猜测是给Go runtime使用C栈部分使用的。
.noptrbss 14KiB NOBITS

除了C语言已有段,Go还有自己的自定义Section如下。
都包含在了常量段中。

段名 解释 备注
.typelink 类型链接段 modules中记录在types类型表中的偏移,或者是typemap中的key
.itablink 接口链接段 大小跟接口数量有关
.gosymtab 符号段 strip后,这个段为空
.gopclntab Go中用于栈回溯用到的pc和栈帧映射表,每个module有自己的表 程序代码越多,此段越大。

go中的module并不是package。go支持plugin,所以一般编译出来的程序只有一个module。

readelf看到的可加载的Section有12个,但查看main的/proc/<pid>/smap可以看到多达22个Section。

查看smap中的段

用途 来源 权限 对应Section
00400000-00482000 代码段 二进制中的代码 r-xp
00482000-00514000 常量段 二进制中的 r--p
00514000-0052c000 变量段 二进制中 rw-p .go.buildinfo .noptrdata .bss 一段
0052c000-0055e000 变量段 二进制中 rw-p .bss一段和noptrbss
c000000000-c000400000 根据寄存器的内容看是栈 rw-p
c000400000-c004000000 也是栈。 ---p
7fcce0548000-7fcce2800000 mspan
7fcce2800000-7fcce2c00000 mspan rw-p
7fcce2c00000-7fcce2c04000 mspan rw-p
7fcce2c04000-7fccf317d000 mspan ---p
7fccf317d000-7fccf317e000 mspan rw-p
7fccf317e000-7fcd0502d000 mspan ---p
7fcd0502d000-7fcd0502e000 mspan rw-p
7fcd0502e000-7fcd07403000 mspan rw-p
7fcd07403000-7fcd07404000 mspan rw-p
7fcd07404000-7fcd0787d000 mspan ---p
7fcd0787d000-7fcd0787e000 mspan rw-p
7fcd0787e000-7fcd078fd000 mspan ---p
7fcd078fd000-7fcd0795d000 mspan rw-p
7ffdd88fd000-7ffdd891f000 C栈和system栈 rw-p
7ffdd898c000-7ffdd8990000 vvar r--p
7ffdd8990000-7ffdd8992000 vdso r-xp

注:

  1. ---p类型的是从背后看但还没映射物理页的区域。
  2. mspan为笔者加的,实际看smaps内容,没有这样的标识。笔者通过dlv等手段判定的。mspan是Go中管理Go堆的数据结构。

Q:为什么mspan和堆区隔了比较大的空间?
A:mspan是通过指定mmap flag为 mmap(NULL, 8, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0)分配的。而堆区有严格的地址空间约束,是指定起始地址通过mmap向操作系统主动占用的。这样做的好处是可以很方便通过地址判断属于哪个内存区域。

待分配的空间通过runtime.mheap_.arenaHints(单向链表)管理的。1.20之后增加了userArena。通过dlv调试,可以确认c000000000-c000400000栈段是通过runtime.sysAlloc分配的,即指定起始地址的mmap方式分配的。分配之后就会从runtime.mheap_.arenaHints中移除掉。

通过dlv中看到的内容如下:

(dlv) p %x runtime.mheap_.arenaHints
*runtime.arenaHint {
        _: runtime/internal/sys.NotInHeap {
                _: runtime/internal/sys.nih {},},
        addr: c004000000,
        down: %!x(bool=false),
        next: *runtime.arenaHint {
                _: (*"runtime/internal/sys.NotInHeap")(0x7fcd079219c8),
                addr: 1c000000000,
                down: %!x(bool=false),
                next: *(*runtime.arenaHint)(0x7fcd079219b0),},}

§ 0x03 小结

通过以上分析得到的Go进程的内存RSS占用组成如下:

总体来说,对Go内存影响最大的两个因素:

  1. 业务越复杂,代码、常量、全局变量占用的就越多,特别是常量区的.gopclntab增长最为明显。以kubectl为例,42MiB的二进制,这个段占据了大概12MiB。
  2. 业务并发越多,协程越多,Go堆的占用就越多。因为Go堆包含了用户协程栈。

§ 0x04 为什么Go进程RSS高?

另外,因为用Go开发大多是一些容器应用,要求这些程序静态编译,结果就是Go的代码段不可能与其他Go应用复用,所以Go的RSS会较高。

还有,因为Go自己实现了栈管理,所以回溯栈(打堆栈、GC场景)时需要依赖pclntab,这部分信息如上所述,也占据很大内存空间,同样不同Go程序也不能复用,所以更加剧了Go的内存占用。

从上述两个因素出发,如何优化内存占用呢?

4.1 减小代码段

动态编译后面社区大概率是不会支持了,见issue。所以想通过这种方式减少Go的代码段,不可行。
Go相比Python这种脚本语言有个问题,一个package下的源文件,即便里面的某个接口你用不到,只要你用到了其中一个,其他的源文件就被编译进二进制中。

如果可以识别哪部分占比大,同时又不用的话,就可以这样本地化之后,通过//go:build的构建标签来优化。查看二进制中哪个package贡献大小,可以使用如下的工具查看: https://github.com/goccy/go-graphviz

4.2 GC调优

另外一个思路就是GC调优。
虽然GO只有一个GOGC的调优参数,但关联较多,需要额外一个文档才能说清楚。

§ 0x05 参考

  1. mmap与brk区别 https://www.cnblogs.com/vinozly/p/5489138.html
  2. Go二进制可视化 https://github.com/goccy/go-graphviz
posted @   lin2learn  阅读(158)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示