記一次go內(nèi)存消減的過程

go內(nèi)存問題說明:

工作中,短暫接手了一個(gè)go編程的應(yīng)用程序,其中有有一個(gè)升級(jí)組件,在升級(jí)過程、比較md5等場(chǎng)景中約占用內(nèi)存500M。

使用工具gops、pprof

gops: goland Attach to Process 工具。通過gops可以連接到本地的進(jìn)程中,進(jìn)行斷點(diǎn)調(diào)試。

獲得途徑:通過github 獲得gops源碼,然后進(jìn)行編譯,編譯期間遇到幾個(gè)問題通過修改源碼中的go.mod一一修改,得到gops.exe。放在GoPath下,完成。

pprof:go通過 go tool pprof能夠觀察到go應(yīng)用程序的CPU、Memory的情況,可以通過graphviz將 pprof中的得到的信息,形成可視化的圖像,方便追蹤問題。

初步探索:

問題原因是同事使用升級(jí)組件時(shí),內(nèi)存會(huì)飆漲,等到10分鐘左右,內(nèi)存才會(huì)降下來。

首先,我們從網(wǎng)上了解到,go是一個(gè)擁有g(shù)c的語言。同時(shí),隨著版本的更迭,go的gc愈加的完善。所以,不要輕易懷疑一種語言,懷疑自己的代碼。

go的垃圾回收采用的是 標(biāo)記-清理(Mark-and-Sweep)算法:先標(biāo)記出需要回收的內(nèi)存對(duì)象,然后清理掉。
go回收內(nèi)存慢的原因可能有以下幾個(gè):
1.go在沒有長(zhǎng)時(shí)間觸發(fā)gc的情況下,可能需要有一個(gè)時(shí)間間隔才會(huì)主動(dòng)觸發(fā)一次gc;
2.go的gc機(jī)制,不是立即將內(nèi)存返回給系統(tǒng),而是告訴系統(tǒng),這些內(nèi)存沒有人使用了,但系統(tǒng)不會(huì)立即回收,而是延緩回收,已提供給go需要內(nèi)存是更快的分配速度。

即,go大體上可以看做內(nèi)含一個(gè)內(nèi)存池的存在。猜測(cè)程序爆發(fā)500M的內(nèi)存是因?yàn)榉逯祪?nèi)存上去導(dǎo)致的。

初次交手

起初,在瀏覽go的資料時(shí),關(guān)于string和[]byte的轉(zhuǎn)換,是我比較關(guān)注的一個(gè)點(diǎn)。
go中 string和[]byte的相互轉(zhuǎn)換的時(shí)候,需要重新分配一次內(nèi)存,使用拷貝進(jìn)行操作。

程序中,關(guān)于文件操作的Read、Write方法,使用了OS.WriteString等方法,通過string、[]byte的多次轉(zhuǎn)換來實(shí)現(xiàn)功能。

結(jié)果:程序內(nèi)存有下降,不過峰值下降不夠明顯,且不夠穩(wěn)定。

神來之筆 - pprof

僅僅通過外部的猜測(cè),就想要解決程序的內(nèi)存問題,顯然有些天方夜譚。這個(gè)時(shí)候需要思考和一定順手的工具。

pprof在這個(gè)時(shí)候進(jìn)入我的視野。

.runtime/pprof:采集程序(非 Server)的運(yùn)行數(shù)據(jù)進(jìn)行分析
.net/http/pprof:采集 HTTP Server 的運(yùn)行時(shí)數(shù)據(jù)進(jìn)行分析

.net/http/pprof可以通過程序?qū)?shù)據(jù)發(fā)送到本地端口,可以利用本地瀏覽器進(jìn)行可視化追蹤。添加的代碼如下:

import (
    _"net/http/pprof"
)

func main () {
    go func() {
        http.ListenAndServe("0.0.0.0:8899", nil)
    }()
    ...
}

隨后啟動(dòng)程序,走流程測(cè)試。

cmd命令:go tool pprof -alloc_space/-inuse_space http://localhost:8899/debug/pprof/heap

執(zhí)行結(jié)果如下圖:


pprof-cmd.jpg

之后可以使用pprof的指令。 這里我使用的是 web,即打開本地瀏覽器,可以看到具體的內(nèi)存流向圖如下。


流向圖.png

可以看出內(nèi)存的保障和一個(gè)函數(shù)有關(guān):bytes.makeSlice

罪魁禍?zhǔn)?/h3>

根據(jù)pprof所得的流程圖可以得到,在bytes.makeSlice的上一級(jí)調(diào)用時(shí) ioutil.ReadAll。
找到ioutil.ReadAll的的源碼,發(fā)現(xiàn)會(huì)調(diào)用buffer.grow這個(gè)底層增長(zhǎng)函數(shù)。且在調(diào)用ioutil.ReadAll函數(shù)的地方,[]btyes的大小沒有一個(gè)初始估計(jì),導(dǎo)致go會(huì)通過*2的方式去申請(qǐng)足夠大小的內(nèi)存,讀取數(shù)據(jù),這一定程度上導(dǎo)致了內(nèi)存的損耗。

措施:在ioutil.ReadAll函數(shù)調(diào)用前,先預(yù)估文件的大小,然后調(diào)用。
結(jié)果:內(nèi)存下降了在170M左右。

結(jié)果

170M峰值內(nèi)存,在目前來看是調(diào)節(jié)的一個(gè)極限,因?yàn)槌绦蛟谶\(yùn)行過程中,會(huì)下載約100M的文件。而我觀察到的下載方式是通過全部讀取到內(nèi)存中,然后再寫入文件中。這個(gè)業(yè)務(wù)方式,直接導(dǎo)致峰值內(nèi)存一定會(huì)在100M以上,而且還有協(xié)成下載和其他的一些內(nèi)存使用。

內(nèi)存優(yōu)化,暫時(shí)告一段落。

心得

這是我初步接手go的程序,短短2周不到的了解,就可以看到內(nèi)存上面處理問題的諸多不足。這些有可能是因?yàn)榍叭说倪壿嬄┒矗灿泻芏嗍莋o本身底層的東西就沒有看透。
只想說:
1.語言無對(duì)錯(cuò),錯(cuò)在使用方法上
2.不是說高并發(fā)就是好的,真正的高并發(fā)也是應(yīng)該考慮實(shí)際應(yīng)用環(huán)境才可以的。
3.遇到事情,不能想什么其他的外路(ps:比如內(nèi)存高就重啟之類的)

以上是此次解決問題的一些想法。問題很簡(jiǎn)單,重要的是編程思路和不斷的學(xué)習(xí)+測(cè)試,記錄下來給以后的自己一個(gè)警醒。

最后貼一個(gè)獲得md5值的go代碼,大家可以看看他們的內(nèi)存差別。

  1.  f, err := os.Open(file)
         if err != nil {
            return false
         }
     h := md5.New()
     io.Copy(h,f)
     dst := string(h.Sum(nil)[:16])
    
  2.  data, err := ioutil.ReadFile(file)
     if err != nil {
         return
     }
     value = md5.Sum(data)
     return value, nil
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容