自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

為什么 Go 占用那么多的虛擬內(nèi)存?

云計(jì)算 虛擬化
前段時(shí)間,某同學(xué)說(shuō)某服務(wù)的容器因?yàn)槌鰞?nèi)存限制,不斷地重啟,問(wèn)我們是不是有內(nèi)存泄露,趕緊排查,然后解決掉,省的出問(wèn)題。

 [[349727]]

本文轉(zhuǎn)載自微信公眾號(hào)「腦子進(jìn)煎魚(yú)了」,作者陳煎魚(yú)。轉(zhuǎn)載本文請(qǐng)聯(lián)系腦子進(jìn)煎魚(yú)了公眾號(hào)。 

前段時(shí)間,某同學(xué)說(shuō)某服務(wù)的容器因?yàn)槌鰞?nèi)存限制,不斷地重啟,問(wèn)我們是不是有內(nèi)存泄露,趕緊排查,然后解決掉,省的出問(wèn)題。

我們大為震驚,趕緊查看監(jiān)控+報(bào)警系統(tǒng)和性能分析,發(fā)現(xiàn)應(yīng)用指標(biāo)壓根就不高,不像有泄露的樣子。

問(wèn)題到底是出在哪里了呢,我們進(jìn)入某個(gè)容器里查看了 top 的系統(tǒng)指標(biāo):

  1. PID       VSZ    RSS   ... COMMAND 
  2. 67459     2007m  136m  ... ./eddycjy-server 

看上去也沒(méi)什么大開(kāi)銷(xiāo)的東西,就一個(gè) Go 進(jìn)程?就這?

再定眼一看,某同學(xué)就說(shuō) VSZ 那么高,而某云上的容器內(nèi)存指標(biāo)居然恰好和 VSZ 的值相接近,因此就懷疑是不是 VSZ 所導(dǎo)致的,覺(jué)得存在一定的關(guān)聯(lián)關(guān)系。

這個(gè)猜測(cè)的結(jié)果到底是否正確呢?

基礎(chǔ)知識(shí)

本篇文章將主要圍繞 Go 進(jìn)程的 VSZ 來(lái)進(jìn)行剖析,看看到底它為什么那么 "高"。

第一節(jié)為前置的補(bǔ)充知識(shí),大家可按順序閱讀。

什么是 VSZ

VSZ 是該進(jìn)程所能使用的虛擬內(nèi)存總大小,它包括進(jìn)程可以訪問(wèn)的所有內(nèi)存,其中包括了被換出的內(nèi)存(Swap)、已分配但未使用的內(nèi)存以及來(lái)自共享庫(kù)的內(nèi)存。

為什么要虛擬內(nèi)存

在前面我們有了解到 VSZ 其實(shí)就是該進(jìn)程的虛擬內(nèi)存總大小,那如果我們想了解 VSZ 的話(huà),那我們得先了解 “為什么要虛擬內(nèi)存?”。

本質(zhì)上來(lái)講,在一個(gè)系統(tǒng)中的進(jìn)程是與其他進(jìn)程共享 CPU 和主存資源的。

因此在現(xiàn)代的操作系統(tǒng)中,多進(jìn)程的使用非常的常見(jiàn),如果太多的進(jìn)程需要太多的內(nèi)存,在沒(méi)有虛擬內(nèi)存的情況下,物理內(nèi)存很可能會(huì)不夠用,就會(huì)導(dǎo)致其中有些任務(wù)無(wú)法運(yùn)行,更甚至?xí)霈F(xiàn)一些很奇怪的現(xiàn)象。

例如 “某一個(gè)進(jìn)程不小心寫(xiě)了另一個(gè)進(jìn)程使用的內(nèi)存”,就會(huì)造成內(nèi)存破壞,因此虛擬內(nèi)存是非常重要的一個(gè)媒介。

虛擬內(nèi)存包含了什么

虛擬內(nèi)存,又分為:

  • 內(nèi)核虛擬內(nèi)存。
  • 進(jìn)程虛擬內(nèi)存。

每一個(gè)進(jìn)程的虛擬內(nèi)存都是獨(dú)立的, 內(nèi)部結(jié)構(gòu)如下圖所示。

在內(nèi)核虛擬內(nèi)存中,包含了內(nèi)核中的代碼和數(shù)據(jù)結(jié)構(gòu)。

內(nèi)核虛擬內(nèi)存中的某些區(qū)域會(huì)被映射到所有進(jìn)程共享的物理頁(yè)面中去,因此你會(huì)看到 ”內(nèi)核虛擬內(nèi)存“ 中實(shí)際上是包含了 ”物理內(nèi)存“ 的,它們兩者存在映射關(guān)系。

而從應(yīng)用場(chǎng)景上來(lái)講,每個(gè)進(jìn)程也會(huì)去共享內(nèi)核的代碼和全局?jǐn)?shù)據(jù)結(jié)構(gòu),因此就會(huì)被映射到所有進(jìn)程的物理頁(yè)面中去。

虛擬內(nèi)存的重要能力

為了更有效地管理內(nèi)存并且減少出錯(cuò),現(xiàn)代系統(tǒng)提供了一種對(duì)主存的抽象概念,也就是今天的主角,叫做虛擬內(nèi)存(VM)。

虛擬內(nèi)存是硬件異常、硬件地址翻譯、主存、磁盤(pán)文件和內(nèi)核軟件交互的地方,它為每個(gè)進(jìn)程提供了一個(gè)大的、一致的和私有的地址空間,虛擬內(nèi)存提供了三個(gè)重要的能力:

它將主存看成是一個(gè)存儲(chǔ)在磁盤(pán)上的地址空間的高速緩存,在主存中只保存活動(dòng)區(qū)域,并根據(jù)需要在磁盤(pán)和主存之間來(lái)回傳送數(shù)據(jù),通過(guò)這種方式,它高效地使用了主存。

它為每個(gè)進(jìn)程提供了一致的地址空間,從而簡(jiǎn)化了內(nèi)存管理。

它保護(hù)了每個(gè)進(jìn)程的地址空間不被其他進(jìn)程破壞。

小結(jié)

上面發(fā)散的可能比較多,簡(jiǎn)單來(lái)講,對(duì)于本文我們重點(diǎn)關(guān)注這些知識(shí)點(diǎn),如下:

  • 虛擬內(nèi)存它是有各式各樣內(nèi)存交互的地方,它包含的不僅僅是 "自己",而在本文中,我們只需要關(guān)注 VSZ,也就是進(jìn)程虛擬內(nèi)存,它包含了你的代碼、數(shù)據(jù)、堆、棧段和共享庫(kù)。
  • 虛擬內(nèi)存作為內(nèi)存保護(hù)的工具,能夠保證進(jìn)程之間的內(nèi)存空間獨(dú)立,不受其他進(jìn)程的影響,因此每一個(gè)進(jìn)程的 VSZ 大小都不一樣,互不影響。
  • 虛擬內(nèi)存的存在,系統(tǒng)給各進(jìn)程分配的內(nèi)存之和是可以大于實(shí)際可用的物理內(nèi)存的,因此你也會(huì)發(fā)現(xiàn)你進(jìn)程的物理內(nèi)存總是比虛擬內(nèi)存低的多的多。

排查問(wèn)題

在了解了基礎(chǔ)知識(shí)后,我們正式開(kāi)始排查問(wèn)題,第一步我們先編寫(xiě)一個(gè)測(cè)試程序,看看沒(méi)有什么業(yè)務(wù)邏輯的 Go 程序,它初始的 VSZ 是怎么樣的。

測(cè)試

應(yīng)用代碼:

  1. func main() { 
  2.  r := gin.Default() 
  3.  r.GET("/ping", func(c *gin.Context) { 
  4.   c.JSON(200, gin.H{ 
  5.    "message""pong"
  6.   }) 
  7.  }) 
  8.  r.Run(":8001"

查看進(jìn)程情況:

  1. $ ps aux 67459 
  2. USER      PID  %CPU %MEM      VSZ    RSS   ... 
  3. eddycjy 67459   0.0  0.0  4297048    960   ... 

從結(jié)果上來(lái)看,VSZ 為 4297048K,也就是 4G 左右,咋一眼看過(guò)去還是挺嚇人的,明明沒(méi)有什么業(yè)務(wù)邏輯,但是為什么那么高呢,真是令人感到好奇。

確認(rèn)有沒(méi)有泄露

在未知的情況下,我們可以首先看下 runtime.MemStats 和 pprof,確定應(yīng)用到底有沒(méi)有泄露。不過(guò)我們這塊是演示程序,什么業(yè)務(wù)邏輯都沒(méi)有,因此可以確定和應(yīng)用沒(méi)有直接關(guān)系。

  1. # runtime.MemStats 
  2. # Alloc = 1298568 
  3. # TotalAlloc = 1298568 
  4. # Sys = 71893240 
  5. # Lookups = 0 
  6. # Mallocs = 10013 
  7. # Frees = 834 
  8. # HeapAlloc = 1298568 
  9. # HeapSys = 66551808 
  10. # HeapIdle = 64012288 
  11. # HeapInuse = 2539520 
  12. # HeapReleased = 64012288 
  13. # HeapObjects = 9179 
  14. ... 

Go FAQ

接著我第一反應(yīng)是去翻了 Go FAQ(因?yàn)榭吹竭^(guò),有印象),其問(wèn)題為 "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.

這個(gè) FAQ 是在 2012 年 10 月 提交 的,這么多年了也沒(méi)有更進(jìn)一步的說(shuō)明,再翻了 issues 和 forum,一些關(guān)閉掉的 issue 都指向了 FAQ,這顯然無(wú)法滿(mǎn)足我的求知欲,因此我繼續(xù)往下探索,看看里面到底都擺了些什么。

查看內(nèi)存映射

在上圖中,我們有提到進(jìn)程虛擬內(nèi)存,主要包含了你的代碼、數(shù)據(jù)、堆、棧段和共享庫(kù),那初步懷疑是不是進(jìn)程做了什么內(nèi)存映射,導(dǎo)致了大量的內(nèi)存空間被保留呢,為了確定這一點(diǎn),我們通過(guò)如下命令去排查:

  1. $ vmmap --wide 67459 
  2. ... 
  3. ==== Non-writable regions for process 67459 
  4. REGION TYPE                      START - END             [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL 
  5. __TEXT                 00000001065ff000-000000010667b000 [  496K   492K     0K     0K] r-x/rwx SM=COW          /bin/zsh 
  6. __LINKEDIT             0000000106687000-0000000106699000 [   72K    44K     0K     0K] r--/rwx SM=COW          /bin/zsh 
  7. MALLOC metadata        000000010669b000-000000010669c000 [    4K     4K     4K     0K] r--/rwx SM=COW          DefaultMallocZone_0x10669b000 zone structure 
  8. ... 
  9. __TEXT                 00007fff76c31000-00007fff76c5f000 [  184K   168K     0K     0K] r-x/r-x SM=COW          /usr/lib/system/libxpc.dylib 
  10. __LINKEDIT             00007fffe7232000-00007ffff32cb000 [192.6M  17.4M     0K     0K] r--/r-- SM=COW          dyld shared cache combined __LINKEDIT 
  11. ...         
  12.  
  13. ==== Writable regions for process 67459 
  14. REGION TYPE                      START - END             [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL 
  15. __DATA                 000000010667b000-0000000106682000 [   28K    28K    28K     0K] rw-/rwx SM=COW          /bin/zsh 
  16. ...    
  17. __DATA                 0000000106716000-000000010671e000 [   32K    28K    28K     4K] rw-/rwx SM=COW          /usr/lib/zsh/5.3/zsh/zle.so 
  18. __DATA                 000000010671e000-000000010671f000 [    4K     4K     4K     0K] rw-/rwx SM=COW          /usr/lib/zsh/5.3/zsh/zle.so 
  19. __DATA                 0000000106745000-0000000106747000 [    8K     8K     8K     0K] rw-/rwx SM=COW          /usr/lib/zsh/5.3/zsh/complete.so 
  20. __DATA                 000000010675a000-000000010675b000 [    4K     4K     4K     0K] rw- 
  21. ... 

這塊主要是利用 macOS 的 vmmap 命令去查看內(nèi)存映射情況,這樣就可以知道這個(gè)進(jìn)程的內(nèi)存映射情況,從輸出分析來(lái)看,這些關(guān)聯(lián)共享庫(kù)占用的空間并不大,導(dǎo)致 VSZ 過(guò)高的根本原因不在共享庫(kù)和二進(jìn)制文件上,但是并沒(méi)有發(fā)現(xiàn)大量保留內(nèi)存空間的行為,這是一個(gè)問(wèn)題點(diǎn)。

注:若是 Linux 系統(tǒng),可使用 cat /proc/PID/maps 或 cat /proc/PID/smaps 查看。

查看系統(tǒng)調(diào)用

既然在內(nèi)存映射中,我們沒(méi)有明確的看到保留內(nèi)存空間的行為,那我們接下來(lái)看看該進(jìn)程的系統(tǒng)調(diào)用,確定一下它是否存在內(nèi)存操作的行為,如下:

  1. $ sudo dtruss -a ./awesomeProject 
  2. ... 
  3.  4374/0x206a2:     15620       6      3 mprotect(0x1BC4000, 0x1000, 0x0)   = 0 0 
  4. ... 
  5.  4374/0x206a2:     15781       9      4 sysctl([CTL_HW, 3, 0, 0, 0, 0] (2), 0x7FFEEFBFFA64, 0x7FFEEFBFFA68, 0x0, 0x0)   = 0 0 
  6.  4374/0x206a2:     15783       3      1 sysctl([CTL_HW, 7, 0, 0, 0, 0] (2), 0x7FFEEFBFFA64, 0x7FFEEFBFFA68, 0x0, 0x0)   = 0 0 
  7.  4374/0x206a2:     15899       7      2 mmap(0x0, 0x40000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)   = 0x4000000 0 
  8.  4374/0x206a2:     15930       3      1 mmap(0xC000000000, 0x4000000, 0x0, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)   = 0xC000000000 0 
  9.  4374/0x206a2:     15934       4      2 mmap(0xC000000000, 0x4000000, 0x3, 0x1012, 0xFFFFFFFFFFFFFFFF, 0x0)   = 0xC000000000 0 
  10.  4374/0x206a2:     15936       2      0 mmap(0x0, 0x2000000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)   = 0x59B7000 0 
  11.  4374/0x206a2:     15942       2      0 mmap(0x0, 0x210800, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)   = 0x4040000 0 
  12.  4374/0x206a2:     15947       2      0 mmap(0x0, 0x10000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)   = 0x1BD0000 0 
  13.  4374/0x206a2:     15993       3      0 madvise(0xC000000000, 0x2000, 0x8)   = 0 0 
  14.  4374/0x206a2:     16004       2      0 mmap(0x0, 0x10000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)   = 0x1BE0000 0 
  15. ... 

在這小節(jié)中,我們通過(guò) macOS 的 dtruss 命令監(jiān)聽(tīng)并查看了運(yùn)行這個(gè)程序所進(jìn)行的所有系統(tǒng)調(diào)用,發(fā)現(xiàn)了與內(nèi)存管理有一定關(guān)系的方法如下:

  • mmap:創(chuàng)建一個(gè)新的虛擬內(nèi)存區(qū)域,但這里需要注意,就是當(dāng)系統(tǒng)調(diào)用 mmap 時(shí),它只是從虛擬內(nèi)存中申請(qǐng)了一段空間出來(lái),并不會(huì)去分配和映射真實(shí)的物理內(nèi)存,而當(dāng)你訪問(wèn)這段空間的時(shí)候,才會(huì)在當(dāng)前時(shí)間真正的去分配物理內(nèi)存。那么對(duì)應(yīng)到我們實(shí)際應(yīng)用的進(jìn)程中,那就是 VSZ 的增長(zhǎng)后,而該內(nèi)存空間又未正式使用的話(huà),物理內(nèi)存是不會(huì)有增長(zhǎng)的。
  • madvise:提供有關(guān)使用內(nèi)存的建議,例如:MADV_NORMAL、MADV_RANDOM、MADV_SEQUENTIAL、MADV_WILLNEED、MADV_DONTNEED 等等。
  • mprotect:設(shè)置內(nèi)存區(qū)域的保護(hù)情況,例如:PROT_NONE、PROT_READ、PROT_WRITE、PROT_EXEC、PROT_SEM、PROT_SAO、PROT_GROWSUP、PROT_GROWSDOWN 等等。
  • sysctl:在內(nèi)核運(yùn)行時(shí)動(dòng)態(tài)地修改內(nèi)核的運(yùn)行參數(shù)。

在此比較可疑的是 mmap 方法,它在 dtruss 的最終統(tǒng)計(jì)中一共調(diào)用了 10 余次,我們可以相信它在 Go Runtime 的時(shí)候進(jìn)行了大量的虛擬內(nèi)存申請(qǐng)。

我們?cè)俳又驴?,看看到底是在什么階段進(jìn)行了虛擬內(nèi)存空間的申請(qǐng)。

注:若是 Linux 系統(tǒng),可使用 strace 命令。

查看 Go Runtime

啟動(dòng)流程

通過(guò)上述的分析,我們可以知道在 Go 程序啟動(dòng)的時(shí)候 VSZ 就已經(jīng)不低了,并且確定不是共享庫(kù)等的原因,且程序在啟動(dòng)時(shí)系統(tǒng)調(diào)用確實(shí)存在 mmap 等方法的調(diào)用。

那么我們可以充分懷疑 Go 在初始化階段就保留了該內(nèi)存空間。那我們第一步要做的就是查看一下 Go 的引導(dǎo)啟動(dòng)流程,看看是在哪里申請(qǐng)的。

引導(dǎo)過(guò)程如下:

  1. graph TD 
  2. A(rt0_darwin_amd64.s:8<br/>_rt0_amd64_darwin) -->|JMP| B(asm_amd64.s:15<br/>_rt0_amd64) 
  3. --> |JMP|C(asm_amd64.s:87<br/>runtime-rt0_go) 
  4. --> D(runtime1.go:60<br/>runtime-args) 
  5. --> E(os_darwin.go:50<br/>runtime-osinit) 
  6. --> F(proc.go:472<br/>runtime-schedinit) 
  7. --> G(proc.go:3236<br/>runtime-newproc) 
  8. --> H(proc.go:1170<br/>runtime-mstart) 
  9. --> I(在新創(chuàng)建的 p 和 m 上運(yùn)行 runtime-main) 
  • runtime-osinit:獲取 CPU 核心數(shù)。
  • runtime-schedinit:初始化程序運(yùn)行環(huán)境(包括棧、內(nèi)存分配器、垃圾回收、P等)。
  • runtime-newproc:創(chuàng)建一個(gè)新的 G 和 綁定 runtime.main。
  • runtime-mstart:?jiǎn)?dòng)線程 M。

注:來(lái)自@曹大的 《Go 程序的啟動(dòng)流程》和@全成的 《Go 程序是怎樣跑起來(lái)的》,推薦大家閱讀。

初始化運(yùn)行環(huán)境

顯然,我們要研究的是 runtime 里的 schedinit 方法,如下:

  1. func schedinit() { 
  2.  ... 
  3.  stackinit() 
  4.  mallocinit() 
  5.  mcommoninit(_g_.m) 
  6.  cpuinit()       // must run before alginit 
  7.  alginit()       // maps must not be used before this call 
  8.  modulesinit()   // provides activeModules 
  9.  typelinksinit() // uses maps, activeModules 
  10.  itabsinit()     // uses activeModules 
  11.  
  12.  msigsave(_g_.m) 
  13.  initSigmask = _g_.m.sigmask 
  14.  
  15.  goargs() 
  16.  goenvs() 
  17.  parsedebugvars() 
  18.  gcinit() 
  19.   ... 

從用途來(lái)看,非常明顯, mallocinit 方法會(huì)進(jìn)行內(nèi)存分配器的初始化,我們繼續(xù)往下看。

初始化內(nèi)存分配器

mallocinit

接下來(lái)我們正式的分析一下 mallocinit 方法,在引導(dǎo)流程中, mallocinit 主要承擔(dān) Go 程序的內(nèi)存分配器的初始化動(dòng)作,而今天主要是針對(duì)虛擬內(nèi)存地址這塊進(jìn)行拆解,如下:

  1. func mallocinit() { 
  2.  ... 
  3.  if sys.PtrSize == 8 { 
  4.   for i := 0x7f; i >= 0; i-- { 
  5.    var p uintptr 
  6.    switch { 
  7.    case GOARCH == "arm64" && GOOS == "darwin"
  8.     p = uintptr(i)<<40 | uintptrMask&(0x0013<<28) 
  9.    case GOARCH == "arm64"
  10.     p = uintptr(i)<<40 | uintptrMask&(0x0040<<32) 
  11.    case GOOS == "aix"
  12.     if i == 0 { 
  13.      continue 
  14.     } 
  15.     p = uintptr(i)<<40 | uintptrMask&(0xa0<<52) 
  16.    case raceenabled: 
  17.     ... 
  18.    default
  19.     p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32) 
  20.    } 
  21.    hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc()) 
  22.    hint.addr = p 
  23.    hint.next, mheap_.arenaHints = mheap_.arenaHints, hint 
  24.   } 
  25.  } else { 
  26.       ... 
  27.  } 
  • 判斷當(dāng)前是 64 位還是 32 位的系統(tǒng)。
  • 從 0x7fc000000000~0x1c000000000 開(kāi)始設(shè)置保留地址。
  • 判斷當(dāng)前 GOARCH、GOOS 或是否開(kāi)啟了競(jìng)態(tài)檢查,根據(jù)不同的情況申請(qǐng)不同大小的連續(xù)內(nèi)存地址,而這里的 p 是即將要要申請(qǐng)的連續(xù)內(nèi)存地址的開(kāi)始地址。
  • 保存剛剛計(jì)算的 arena 的信息到 arenaHint 中。

可能會(huì)有小伙伴問(wèn),為什么要判斷是 32 位還是 64 位的系統(tǒng),這是因?yàn)椴煌粩?shù)的虛擬內(nèi)存的尋址范圍是不同的,因此要進(jìn)行區(qū)分,否則會(huì)出現(xiàn)高位的虛擬內(nèi)存映射問(wèn)題。而在申請(qǐng)保留空間時(shí),我們會(huì)經(jīng)常提到 arenaHint 結(jié)構(gòu)體,它是 arenaHints鏈表里的一個(gè)節(jié)點(diǎn),結(jié)構(gòu)如下:

  1. type arenaHint struct { 
  2.  addr uintptr 
  3.  down bool 
  4.  next *arenaHint 
  • addr:arena 的起始地址
  • down:是否最后一個(gè) arena
  • next:下一個(gè) arenaHint 的指針地址

那么這里瘋狂提到的 arena 又是什么東西呢,這其實(shí)是 Go 的內(nèi)存管理中的概念,Go Runtime 會(huì)把申請(qǐng)的虛擬內(nèi)存分為三個(gè)大塊,如下:

image

  • spans:記錄 arena 區(qū)域頁(yè)號(hào)和 mspan 的映射關(guān)系。
  • bitmap:標(biāo)識(shí) arena 的使用情況,在功能上來(lái)講,會(huì)用于標(biāo)識(shí) arena 的哪些空間地址已經(jīng)保存了對(duì)象。
  • arean:arean 其實(shí)就是 Go 的堆區(qū),是由 mheap 進(jìn)行管理的,它的 MaxMem 是 512GB-1。而在功能上來(lái)講,Go 會(huì)在初始化的時(shí)候申請(qǐng)一段連續(xù)的虛擬內(nèi)存空間地址到 arean 保留下來(lái),在真正需要申請(qǐng)堆上的空間時(shí)再?gòu)?arean 中取出來(lái)處理,這時(shí)候就會(huì)轉(zhuǎn)變?yōu)槲锢韮?nèi)存了。

在這里的話(huà),你需要理解 arean 區(qū)域在 Go 內(nèi)存里的作用就可以了。

mmap

我們剛剛通過(guò)上述的分析,已經(jīng)知道 mallocinit 的用途了,但是你可能還是會(huì)有疑惑,就是我們之前所看到的 mmap 系統(tǒng)調(diào)用,和它又有什么關(guān)系呢,怎么就關(guān)聯(lián)到一起了,接下來(lái)我們先一起來(lái)看看更下層的代碼,如下:

  1. func sysAlloc(n uintptr, sysStat *uint64) unsafe.Pointer { 
  2.  p, err := mmap(nil, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE, -1, 0) 
  3.  ... 
  4.  mSysStatInc(sysStat, n) 
  5.  return p 
  6.  
  7. func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer { 
  8.  p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0) 
  9.  ... 
  10.  
  11. func sysMap(v unsafe.Pointer, n uintptr, sysStat *uint64) { 
  12.  ... 
  13.  munmap(v, n) 
  14.  p, err := mmap(v, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE, -1, 0) 
  15.   ... 

在 Go Runtime 中存在著一系列的系統(tǒng)級(jí)內(nèi)存調(diào)用方法,本文涉及的主要如下:

  • sysAlloc:從 OS 系統(tǒng)上申請(qǐng)清零后的內(nèi)存空間,調(diào)用參數(shù)是 _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE,得到的結(jié)果需進(jìn)行內(nèi)存對(duì)齊。
  • sysReserve:從 OS 系統(tǒng)中保留內(nèi)存的地址空間,這時(shí)候還沒(méi)有分配物理內(nèi)存,調(diào)用參數(shù)是 _PROT_NONE, _MAP_ANON|_MAP_PRIVATE,得到的結(jié)果需進(jìn)行內(nèi)存對(duì)齊。
  • sysMap:通知 OS 系統(tǒng)我們要使用已經(jīng)保留了的內(nèi)存空間,調(diào)用參數(shù)是 _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE。

看上去好像很有道理的樣子,但是 mallocinit 方法在初始化時(shí),到底是在哪里涉及了 mmap 方法呢,表面看不出來(lái),如下:

  1. for i := 0x7f; i >= 0; i-- { 
  2.  ... 
  3.  hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc()) 
  4.  hint.addr = p 
  5.  hint.next, mheap_.arenaHints = mheap_.arenaHints, hint 

實(shí)際上在調(diào)用 mheap_.arenaHintAlloc.alloc() 時(shí),調(diào)用的是 mheap 下的 sysAlloc 方法,而 sysAlloc 又會(huì)與 mmap 方法產(chǎn)生調(diào)用關(guān)系,并且這個(gè)方法與常規(guī)的 sysAlloc 還不大一樣,如下:

  1. var mheap_ mheap 
  2. ... 
  3. func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { 
  4.  ... 
  5.  for h.arenaHints != nil { 
  6.   hint := h.arenaHints 
  7.   p := hint.addr 
  8.   if hint.down { 
  9.    p -= n 
  10.   } 
  11.   if p+n < p { 
  12.    v = nil 
  13.   } else if arenaIndex(p+n-1) >= 1<<arenaBits { 
  14.    v = nil 
  15.   } else { 
  16.    v = sysReserve(unsafe.Pointer(p), n) 
  17.   } 
  18.   ... 

你可以驚喜的發(fā)現(xiàn) mheap.sysAlloc 里其實(shí)有調(diào)用 sysReserve 方法,而 sysReserve 方法又正正是從 OS 系統(tǒng)中保留內(nèi)存的地址空間的特定方法,是不是很驚喜,一切似乎都串起來(lái)了。

小結(jié)

在本節(jié)中,我們先寫(xiě)了一個(gè)測(cè)試程序,然后根據(jù)非常規(guī)的排查思路進(jìn)行了一步步的跟蹤懷疑,整體流程如下:

  • 通過(guò) top 或 ps 等命令,查看進(jìn)程運(yùn)行情況,分析基礎(chǔ)指標(biāo)。
  • 通過(guò) pprof 或 runtime.MemStats 等工具鏈查看應(yīng)用運(yùn)行情況,分析應(yīng)用層面是否有泄露或者哪兒高。
  • 通過(guò) vmmap 命令,查看進(jìn)程的內(nèi)存映射情況,分析是不是進(jìn)程虛擬空間內(nèi)的某個(gè)區(qū)域比較高,例如:共享庫(kù)等。
  • 通過(guò) dtruss 命令,查看程序的系統(tǒng)調(diào)用情況,分析可能出現(xiàn)的一些特殊行為,例如:在分析中我們發(fā)現(xiàn) mmap 方法調(diào)用的比例是比較高的,那我們有充分的理由懷疑 Go 在啟動(dòng)時(shí)就進(jìn)行了大量的內(nèi)存空間保留。
  • 通過(guò)上述的分析,確定可能是在哪個(gè)環(huán)節(jié)申請(qǐng)了那么多的內(nèi)存空間后,再到 Go Runtime 中去做進(jìn)一步的源碼分析,因?yàn)樵创a面前,了無(wú)秘密,沒(méi)必要靠猜。

從結(jié)論上而言,VSZ(進(jìn)程虛擬內(nèi)存大小)與共享庫(kù)等沒(méi)有太大的關(guān)系,主要與 Go Runtime 存在直接關(guān)聯(lián),也就是在前圖中表示的運(yùn)行時(shí)堆(malloc)。轉(zhuǎn)換到 Go Runtime 里,就是在 mallocinit 這個(gè)內(nèi)存分配器的初始化階段里進(jìn)行了一定量的虛擬空間的保留。

而保留虛擬內(nèi)存空間時(shí),受什么影響,又是一個(gè)哲學(xué)問(wèn)題。從源碼上來(lái)看,主要如下:

  • 受不同的 OS 系統(tǒng)架構(gòu)(GOARCH/GOOS)和位數(shù)(32/64 位)的影響。
  • 受內(nèi)存對(duì)齊的影響,計(jì)算回來(lái)的內(nèi)存空間大小是需要經(jīng)過(guò)對(duì)齊才會(huì)進(jìn)行保留。

總結(jié)

我們通過(guò)一步步地分析,講解了 Go 會(huì)在哪里,又會(huì)受什么因素,去調(diào)用了什么方法保留了那么多的虛擬內(nèi)存空間,但是我們肯定會(huì)憂(yōu)心進(jìn)程虛擬內(nèi)存(VSZ)高,會(huì)不會(huì)存在問(wèn)題呢,我分析如下:

  • VSZ 并不意味著你真正使用了那些物理內(nèi)存,因此是不需要擔(dān)心的。
  • VSZ 并不會(huì)給 GC 帶來(lái)壓力,GC 管理的是進(jìn)程實(shí)際使用的物理內(nèi)存,而 VSZ 在你實(shí)際使用它之前,它并沒(méi)有過(guò)多的代價(jià)。
  • VSZ 基本都是不可訪問(wèn)的內(nèi)存映射,也就是它并沒(méi)有內(nèi)存的訪問(wèn)權(quán)限(不允許讀、寫(xiě)和執(zhí)行)。

思考

看到這里舒一口氣,因?yàn)?Go VSZ 的高,并不會(huì)對(duì)我們產(chǎn)生什么非常實(shí)質(zhì)性的問(wèn)題,但是又仔細(xì)一想,為什么 Go 要申請(qǐng)那么多的虛擬內(nèi)存呢?

總體考慮如下:

  • Go 的設(shè)計(jì)是考慮到 arena 和 bitmap 的后續(xù)使用,先提早保留了整個(gè)內(nèi)存地址空間。
  • Go Runtime 和應(yīng)用的逐步使用,肯定也會(huì)開(kāi)始實(shí)際的申請(qǐng)和使用內(nèi)存,這時(shí)候 arena 和 bitmap 的內(nèi)存分配器就只需要將事先申請(qǐng)好的內(nèi)存地址空間保留更改為實(shí)際可用的物理內(nèi)存就好了,這樣子可以極大的提高效能。

參考

High virtual memory allocation by golang

GO MEMORY MANAGEMENT

GoBigVirtualSize

GoProgramMemoryUse

曹大的 Go 程序的啟動(dòng)流程

全成大佬的 Go 程序是怎樣跑起來(lái)的

歐神的 go-under-the-hood

 

責(zé)任編輯:武曉燕 來(lái)源: 腦子進(jìn)煎魚(yú)了
相關(guān)推薦

2019-12-02 14:22:01

浪費(fèi)云計(jì)算支出

2020-04-14 16:03:31

Linux虛擬內(nèi)存操作系統(tǒng)

2015-09-29 10:12:10

2017-08-14 18:00:13

共享單車(chē)摩拜

2023-01-24 16:13:22

編程語(yǔ)言JavaIT

2017-01-21 14:57:43

Linuxsystemd

2023-05-26 00:25:53

2017-09-18 14:39:31

溝通培訓(xùn)學(xué)習(xí)

2020-07-13 08:40:21

BAT模具設(shè)計(jì)

2011-12-31 14:47:10

Web App

2020-08-26 17:03:52

同型號(hào)顯卡產(chǎn)品

2013-06-17 10:45:34

2020-08-10 07:44:13

虛擬內(nèi)存交換內(nèi)存Linux

2024-04-18 11:53:59

通訊協(xié)議網(wǎng)絡(luò)

2020-04-24 08:15:51

代碼 if else數(shù)組

2022-08-02 09:02:17

虛擬內(nèi)存操作系統(tǒng)

2022-03-04 22:43:01

5G4G3G

2020-02-16 11:25:22

物聯(lián)網(wǎng)硬件技術(shù)

2014-07-14 09:51:09

創(chuàng)始人谷歌項(xiàng)目

2010-06-10 17:12:23

Linux 內(nèi)存監(jiān)控
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)