作者:陈煎鱼 2020-11-02 07:05:54
云计算
虚拟化 前段时间,某同学说某服务的容器因为超出内存限制,不断地重启,问我们是不是有内存泄露,赶紧排查,然后解决掉,省的出问题。
创新互联是一家专业提供大理州企业网站建设,专注与成都网站制作、成都做网站、外贸营销网站建设、HTML5、小程序制作等业务。10年已为大理州众多企业、政府机构等服务。创新互联专业网络公司优惠进行中。
本文转载自微信公众号「脑子进煎鱼了」,作者陈煎鱼。转载本文请联系脑子进煎鱼了公众号。
前段时间,某同学说某服务的容器因为超出内存限制,不断地重启,问我们是不是有内存泄露,赶紧排查,然后解决掉,省的出问题。
我们大为震惊,赶紧查看监控+报警系统和性能分析,发现应用指标压根就不高,不像有泄露的样子。
问题到底是出在哪里了呢,我们进入某个容器里查看了 top 的系统指标:
- PID VSZ RSS ... COMMAND
- 67459 2007m 136m ... ./eddycjy-server
看上去也没什么大开销的东西,就一个 Go 进程?就这?
再定眼一看,某同学就说 VSZ 那么高,而某云上的容器内存指标居然恰好和 VSZ 的值相接近,因此就怀疑是不是 VSZ 所导致的,觉得存在一定的关联关系。
这个猜测的结果到底是否正确呢?
基础知识
本篇文章将主要围绕 Go 进程的 VSZ 来进行剖析,看看到底它为什么那么 "高"。
第一节为前置的补充知识,大家可按顺序阅读。
什么是 VSZ
VSZ 是该进程所能使用的虚拟内存总大小,它包括进程可以访问的所有内存,其中包括了被换出的内存(Swap)、已分配但未使用的内存以及来自共享库的内存。
为什么要虚拟内存
在前面我们有了解到 VSZ 其实就是该进程的虚拟内存总大小,那如果我们想了解 VSZ 的话,那我们得先了解 “为什么要虚拟内存?”。
本质上来讲,在一个系统中的进程是与其他进程共享 CPU 和主存资源的。
因此在现代的操作系统中,多进程的使用非常的常见,如果太多的进程需要太多的内存,在没有虚拟内存的情况下,物理内存很可能会不够用,就会导致其中有些任务无法运行,更甚至会出现一些很奇怪的现象。
例如 “某一个进程不小心写了另一个进程使用的内存”,就会造成内存破坏,因此虚拟内存是非常重要的一个媒介。
虚拟内存包含了什么
虚拟内存,又分为:
每一个进程的虚拟内存都是独立的, 内部结构如下图所示。
在内核虚拟内存中,包含了内核中的代码和数据结构。
内核虚拟内存中的某些区域会被映射到所有进程共享的物理页面中去,因此你会看到 ”内核虚拟内存“ 中实际上是包含了 ”物理内存“ 的,它们两者存在映射关系。
而从应用场景上来讲,每个进程也会去共享内核的代码和全局数据结构,因此就会被映射到所有进程的物理页面中去。
虚拟内存的重要能力
为了更有效地管理内存并且减少出错,现代系统提供了一种对主存的抽象概念,也就是今天的主角,叫做虚拟内存(VM)。
虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件交互的地方,它为每个进程提供了一个大的、一致的和私有的地址空间,虚拟内存提供了三个重要的能力:
它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。
它为每个进程提供了一致的地址空间,从而简化了内存管理。
它保护了每个进程的地址空间不被其他进程破坏。
小结
上面发散的可能比较多,简单来讲,对于本文我们重点关注这些知识点,如下:
排查问题
在了解了基础知识后,我们正式开始排查问题,第一步我们先编写一个测试程序,看看没有什么业务逻辑的 Go 程序,它初始的 VSZ 是怎么样的。
测试
应用代码:
- func main() {
- r := gin.Default()
- r.GET("/ping", func(c *gin.Context) {
- c.JSON(200, gin.H{
- "message": "pong",
- })
- })
- r.Run(":8001")
- }
查看进程情况:
- $ ps aux 67459
- USER PID %CPU %MEM VSZ RSS ...
- eddycjy 67459 0.0 0.0 4297048 960 ...
从结果上来看,VSZ 为 4297048K,也就是 4G 左右,咋一眼看过去还是挺吓人的,明明没有什么业务逻辑,但是为什么那么高呢,真是令人感到好奇。
确认有没有泄露
在未知的情况下,我们可以首先看下 runtime.MemStats 和 pprof,确定应用到底有没有泄露。不过我们这块是演示程序,什么业务逻辑都没有,因此可以确定和应用没有直接关系。
- # runtime.MemStats
- # Alloc = 1298568
- # TotalAlloc = 1298568
- # Sys = 71893240
- # Lookups = 0
- # Mallocs = 10013
- # Frees = 834
- # HeapAlloc = 1298568
- # HeapSys = 66551808
- # HeapIdle = 64012288
- # HeapInuse = 2539520
- # HeapReleased = 64012288
- # HeapObjects = 9179
- ...
Go FAQ
接着我第一反应是去翻了 Go FAQ(因为看到过,有印象),其问题为 "Why does my Go process use so much virtual memory?",回答如下:
The Go memory allocator reserves a large region of virtual memory as an arena for allocations. This virtual memory is local to the specific Go process; the reservation does not deprive other processes of memory.
To find the amount of actual memory allocated to a Go process, use the Unix top command and consult the RES (Linux) or RSIZE (macOS) columns.
这个 FAQ 是在 2012 年 10 月 提交 的,这么多年了也没有更进一步的说明,再翻了 issues 和 forum,一些关闭掉的 issue 都指向了 FAQ,这显然无法满足我的求知欲,因此我继续往下探索,看看里面到底都摆了些什么。
查看内存映射
在上图中,我们有提到进程虚拟内存,主要包含了你的代码、数据、堆、栈段和共享库,那初步怀疑是不是进程做了什么内存映射,导致了大量的内存空间被保留呢,为了确定这一点,我们通过如下命令去排查:
- $ vmmap --wide 67459
- ...
- ==== Non-writable regions for process 67459
- REGION TYPE START - END [ VSIZE RSDNT DIRTY SWAP] PRT/MAX SHRMOD PURGE REGION DETAIL
- __TEXT 00000001065ff000-000000010667b000 [ 496K 492K 0K 0K] r-x/rwx SM=COW /bin/zsh
- __LINKEDIT 0000000106687000-0000000106699000 [ 72K 44K 0K 0K] r--/rwx SM=COW /bin/zsh
- MALLOC metadata 000000010669b000-000000010669c000 [ 4K 4K 4K 0K] r--/rwx SM=COW DefaultMallocZone_0x10669b000 zone structure
- ...
- __TEXT 00007fff76c31000-00007fff76c5f000 [ 184K 168K 0K 0K] r-x/r-x SM=COW /usr/lib/system/libxpc.dylib
- __LINKEDIT 00007fffe7232000-00007ffff32cb000 [192.6M 17.4M 0K 0K] r--/r-- SM=COW dyld shared cache combined __LINKEDIT
- ...
- ==== Writable regions for process 67459
- REGION TYPE START - END [ VSIZE RSDNT DIRTY SWAP] PRT/MAX SHRMOD PURGE REGION DETAIL
- __DATA 000000010667b000-0000000106682000 [ 28K 28K 28K 0K] rw-/rwx SM=COW /bin/zsh
- ...
- __DATA 0000000106716000-000000010671e000 [ 32K 28K 28K 4K] rw-/rwx SM=COW /usr/lib/zsh/5.3/zsh/zle.so
- __DATA 000000010671e000-000000010671f000 [ 4K 4K 4K 0K] rw-/rwx SM=COW /usr/lib/zsh/5.3/zsh/zle.so
- __DATA 0000000106745000-0000000106747000 [ 8K 8K 8K 0K] rw-/rwx SM=COW /usr/lib/zsh/5.3/zsh/complete.so
- __DATA 000000010675a000-000000010675b000 [ 4K 4K 4K 0K] rw-
- ...
这块主要是利用 macOS 的 vmmap 命令去查看内存映射情况,这样就可以知道这个进程的内存映射情况,从输出分析来看,这些关联共享库占用的空间并不大,导致 VSZ 过高的根本原因不在共享库和二进制文件上,但是并没有发现大量保留内存空间的行为,这是一个问题点。
注:若是 Linux 系统,可使用 cat /proc/PID/maps 或 cat /proc/PID/smaps 查看。
查看系统调用
既然在内存映射中,我们没有明确的看到保留内存空间的行为,那我们接下来看看该进程的系统调用,确定一下它是否存在内存操作的行为,如下:
- $ sudo dtruss -a ./awesomeProject
- ...
- 4374/0x206a2: 15620 6 3 mprotect(0x1BC4000, 0x1000, 0x0) = 0 0
- ...
- 4374/0x206a2: 15781 9 4 sysctl([CTL_HW, 3, 0, 0, 0, 0] (2), 0x7FFEEFBFFA64, 0x7FFEEFBFFA68, 0x0, 0x0) = 0 0
- 4374/0x206a2: 15783 3 1 sysctl([CTL_HW, 7, 0, 0, 0, 0] (2), 0x7FFEEFBFFA64, 0x7FFEEFBFFA68, 0x0, 0x0) = 0 0
- 4374/0x206a2: 15899 7 2 mmap(0x0, 0x40000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0) = 0x4000000 0
- 4374/0x206a2: 15930 3 1 mmap(0xC000000000, 0x4000000, 0x0, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0) = 0xC000000000 0
- 4374/0x206a2: 15934 4 2 mmap(0xC000000000, 0x4000000, 0x3, 0x1012, 0xFFFFFFFFFFFFFFFF, 0x0) = 0xC000000000 0
- 4374/0x206a2: 15936 2 0 mmap(0x0, 0x2000000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0) = 0x59B7000 0
- 4374/0x206a2: 15942 2 0 mmap(0x0, 0x210800, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0) = 0x4040000 0
- 4374/0x206a2: 15947 2 0 mmap(0x0, 0x10000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0) = 0x1BD0000 0
- 4374/0x206a2: 15993 3 0 madvise(0xC000000000, 0x2000, 0x8) = 0 0
- 4374/0x206a2: 16004 2 0 mmap(0x0, 0x10000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0) = 0x1BE0000 0
- ...
在这小节中,我们通过 macOS 的 dtruss 命令监听并查看了运行这个程序所进行的所有系统调用,发现了与内存管理有一定关系的方法如下:
在此比较可疑的是 mmap 方法,它在 dtruss 的最终统计中一共调用了 10 余次,我们可以相信它在 Go Runtime 的时候进行了大量的虚拟内存申请。
我们再接着往下看,看看到底是在什么阶段进行了虚拟内存空间的申请。
注:若是 Linux 系统,可使用 strace 命令。
查看 Go Runtime
启动流程
通过上述的分析,我们可以知道在 Go 程序启动的时候 VSZ 就已经不低了,并且确定不是共享库等的原因,且程序在启动时系统调用确实存在 mmap 等方法的调用。
那么我们可以充分怀疑 Go 在初始化阶段就保留了该内存空间。那我们第一步要做的就是查看一下 Go 的引导启动流程,看看是在哪里申请的。
引导过程如下:
- graph TD
- A(rt0_darwin_amd64.s:8
_rt0_amd64_darwin) -->|JMP| B(asm_amd64.s:15
_rt0_amd64)- B --> |JMP|C(asm_amd64.s:87
runtime-rt0_go)- C --> D(runtime1.go:60
runtime-args)- D --> E(os_darwin.go:50
runtime-osinit)- E --> F(proc.go:472
runtime-schedinit)- F --> G(proc.go:3236
runtime-newproc)- G --> H(proc.go:1170
runtime-mstart)- H --> I(在新创建的 p 和 m 上运行 runtime-main)
注:来自@曹大的 《Go 程序的启动流程》和@全成的 《Go 程序是怎样跑起来的》,推荐大家阅读。
初始化运行环境
显然,我们要研究的是 runtime 里的 schedinit 方法,如下:
- func schedinit() {
- ...
- stackinit()
- mallocinit()
- mcommoninit(_g_.m)
- cpuinit() // must run before alginit
- alginit() // maps must not be used before this call
- modulesinit() // provides activeModules
- typelinksinit() // uses maps, activeModules
- itabsinit() // uses activeModules
- msigsave(_g_.m)
- initSigmask = _g_.m.sigmask
- goargs()
- goenvs()
- parsedebugvars()
- gcinit()
- ...
- }
从用途来看,非常明显, mallocinit 方法会进行内存分配器的初始化,我们继续往下看。
初始化内存分配器
mallocinit
接下来我们正式的分析一下 mallocinit 方法,在引导流程中, mallocinit 主要承担 Go 程序的内存分配器的初始化动作,而今天主要是针对虚拟内存地址这块进行拆解,如下:
- func mallocinit() {
- ...
- if sys.PtrSize == 8 {
- for i := 0x7f; i >= 0; i-- {
- var p uintptr
- switch {
- case GOARCH == "arm64" && GOOS == "darwin":
- p = uintptr(i)<<40 | uintptrMask&(0x0013<<28)
- case GOARCH == "arm64":
- p = uintptr(i)<<40 | uintptrMask&(0x0040<<32)
- case GOOS == "aix":
- if i == 0 {
- continue
- }
- p = uintptr(i)<<40 | uintptrMask&(0xa0<<52)
- case raceenabled:
- ...
- default:
- p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32)
- }
- hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())
- hint.addr = p
- hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
- }
- } else {
- ...
- }
- }
可能会有小伙伴问,为什么要判断是 32 位还是 64 位的系统,这是因为不同位数的虚拟内存的寻址范围是不同的,因此要进行区分,否则会出现高位的虚拟内存映射问题。而在申请保留空间时,我们会经常提到 arenaHint 结构体,它是 arenaHints链表里的一个节点,结构如下:
- type arenaHint struct {
- addr uintptr
- down bool
- next *arenaHint
- }
那么这里疯狂提到的 arena 又是什么东西呢,这其实是 Go 的内存管理中的概念,Go Runtime 会把申请的虚拟内存分为三个大块,如下:
image
在这里的话,你需要理解 arean 区域在 Go 内存里的作用就可以了。
mmap
我们刚刚通过上述的分析,已经知道 mallocinit 的用途了,但是你可能还是会有疑惑,就是我们之前所看到的 mmap 系统调用,和它又有什么关系呢,怎么就关联到一起了,接下来我们先一起来看看更下层的代码,如下:
- func sysAlloc(n uintptr, sysStat *uint64) unsafe.Pointer {
- p, err := mmap(nil, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE, -1, 0)
- ...
- mSysStatInc(sysStat, n)
- return p
- }
- func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer {
- p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0)
- ...
- }
- func sysMap(v unsafe.Pointer, n uintptr, sysStat *uint64) {
- ...
- munmap(v, n)
- p, err := mmap(v, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE, -1, 0)
- ...
- }
在 Go Runtime 中存在着一系列的系统级内存调用方法,本文涉及的主要如下:
看上去好像很有道理的样子,但是 mallocinit 方法在初始化时,到底是在哪里涉及了 mmap 方法呢,表面看不出来,如下:
- for i := 0x7f; i >= 0; i-- {
- ...
- hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())
- hint.addr = p
- hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
- }
实际上在调用 mheap_.arenaHintAlloc.alloc() 时,调用的是 mheap 下的 sysAlloc 方法,而 sysAlloc 又会与 mmap 方法产生调用关系,并且这个方法与常规的 sysAlloc 还不大一样,如下:
- var mheap_ mheap
- ...
- func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
- ...
- for h.arenaHints != nil {
- hint := h.arenaHints
- p := hint.addr
- if hint.down {
- p -= n
- }
- if p+n < p {
- v = nil
- } else if arenaIndex(p+n-1) >= 1<
- v = nil
- } else {
- v = sysReserve(unsafe.Pointer(p), n)
- }
- ...
- }
你可以惊喜的发现 mheap.sysAlloc 里其实有调用 sysReserve 方法,而 sysReserve 方法又正正是从 OS 系统中保留内存的地址空间的特定方法,是不是很惊喜,一切似乎都串起来了。
小结
在本节中,我们先写了一个测试程序,然后根据非常规的排查思路进行了一步步的跟踪怀疑,整体流程如下:
从结论上而言,VSZ(进程虚拟内存大小)与共享库等没有太大的关系,主要与 Go Runtime 存在直接关联,也就是在前图中表示的运行时堆(malloc)。转换到 Go Runtime 里,就是在 mallocinit 这个内存分配器的初始化阶段里进行了一定量的虚拟空间的保留。
而保留虚拟内存空间时,受什么影响,又是一个哲学问题。从源码上来看,主要如下:
总结
我们通过一步步地分析,讲解了 Go 会在哪里,又会受什么因素,去调用了什么方法保留了那么多的虚拟内存空间,但是我们肯定会忧心进程虚拟内存(VSZ)高,会不会存在问题呢,我分析如下:
思考
看到这里舒一口气,因为 Go VSZ 的高,并不会对我们产生什么非常实质性的问题,但是又仔细一想,为什么 Go 要申请那么多的虚拟内存呢?
总体考虑如下:
参考
High virtual memory allocation by golang
GO MEMORY MANAGEMENT
GoBigVirtualSize
GoProgramMemoryUse
曹大的 Go 程序的启动流程
全成大佬的 Go 程序是怎样跑起来的
欧神的 go-under-the-hood
当前文章:为什么Go占用那么多的虚拟内存?
转载源于:http://www.shufengxianlan.com/qtweb/news49/21899.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联