一次 Go 程序 out of memory 排查及反思


oom

前言

最近在搞數據導出模塊,在測試大文件下載的過程中,報了 Out of memory (OOM) 錯誤,因為之前沒有遇到過這類問題,導致此次排查問題花費了大半天,也走了不少彎路,特此復盤記錄。

現象描述

和往常一樣復制粘貼完最后一個下載接口,沒什么問題的話,導出模塊就可以收工了。

上服務器部署完,隨便測試了幾個小文件的下載,一切按照預期正常地運行。

隨后我點擊下載了一個 2.6G 的大文件,情況變得糟糕起來,通過 docker stats 觀察到其內存一直上漲到 7G 左右,然后容器 Exit 2,日志報 fatal error: runtime: out of memory

bigfile

stats

exit

此時我一點都不緊張,甚至有點高興,剛好這幾天在看 pprof,並且平時 curd 干多了,正好來點 bug 調解一下心情~

排查過程

現場日志

fatal error: runtime: out of memory

runtime stack:
runtime.throw(0x1447dc3, 0x16)
        runtime/panic.go:774 +0x72
runtime.sysMap(0xc13c000000, 0x130000000, 0x3202418)
        runtime/mem_linux.go:169 +0xc5
runtime.(*mheap).sysAlloc(0x31e12c0, 0x130000000, 0x7f4cfdffaae8, 0x43d067)
        runtime/malloc.go:701 +0x1cd
runtime.(*mheap).grow(0x31e12c0, 0x98000, 0xffffffff)
        runtime/mheap.go:1255 +0xa3
runtime.(*mheap).allocSpanLocked(0x31e12c0, 0x98000, 0x3202428, 0x7f4c00000000)
        runtime/mheap.go:1170 +0x266
runtime.(*mheap).alloc_m(0x31e12c0, 0x98000, 0x101, 0x7f4cfdffac08)
        runtime/mheap.go:1022 +0xc2
runtime.(*mheap).alloc.func1()
        runtime/mheap.go:1093 +0x4c
runtime.(*mheap).alloc(0x31e12c0, 0x98000, 0x7f4cfd010101, 0x43e155)
        runtime/mheap.go:1092 +0x8a
runtime.largeAlloc(0x12fffff10, 0xc000040101, 0xc001816180)
        runtime/malloc.go:1138 +0x97
runtime.mallocgc.func1()
        runtime/malloc.go:1033 +0x46
runtime.systemstack(0xc001d80180)
        runtime/asm_amd64.s:370 +0x66
runtime.mstart()                                       
        runtime/proc.go:1146

由於 go panic 時,將所有的 goroutine 調用棧都打印了出來,報錯信息鋪了好幾頁,而且第一行是一個 runtime error,所以當時我就粗略地看了一下,並沒有找到什么有用日志

分析代碼

// file download
filePath := path.Join(ms.Configs().GetString("system.exportPath"), filename)

f, _ := os.Open(filePath)
defer f.Close()
stat, _ := f.Stat()

c.Writer.WriteHeader(http.StatusOK)
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Length", fmt.Sprintf("%d", stat.Size()))
c.File(filePath)

核心代碼就這么幾行,而且還是從老項目中復制過來的,怎么可能出問題???

doge

此時我的內心開始甩鍋了,可能是環境的問題?老項目用的是 k8s,現在用的是 docker-compose、老項目用的是 go1.13 現在是 go1.15。

demo測試

由於項目依賴較多,本地運行不太方便,於是我寫了一個小 demo,打算不用 docker 直接跑起來(准備把鍋甩給 docker)

func main() {
	r := gin.Default()
	r.GET("/download", func(c *gin.Context) {
		filename := "cb6mp2mh843u.zip"
		filePath := path.Join("/data", filename)
		c.Writer.WriteHeader(http.StatusOK)
		c.Header("Content-Disposition", "attachment; filename="+filename)
		c.Header("Content-Type", "application/octet-stream")
		c.File(filePath)
	})
	r.Run("0.0.0.0:8686")
}

demo 跑起來后,多次測試,內存使用情況一切正常,此時我陷入了沉思......

pprof

沒辦法,只好用 pprof 了,剛好現學現用。

pprof

使用 pprof 采集了 heap 的數據,很快就找到了問題,項目使用的小組內基於 gin 封裝的一個輪子,為了記錄 response body 日志,將其 Dump 了下來,2.6G 的數據呀...

於是我順手加了個 if else 判斷,修了這個 bug

log

dump

復盤反思

所以當時我就粗略地看了一下,並沒有找到什么有用日志

程序出錯時報的日志,里面包含了最重要,直接的排查線索,一定要仔細地將每一行都看一遍,不能由於信息較多,就忽略它。

事后統計當時報錯的日志就 300 多行,12 個 goroutine,如果稍微仔細看一下,我相信很快就能定位到問題所在,也不用水這一篇文章了。

logs

可能是環境的問題?老項目用的是 k8s,現在用的是 docker-compose、老項目用的是 go1.13 現在是 go1.15

在出現問題時,程序員總是喜歡將鍋甩給環境、工具等,不是說這些基礎組件一定沒有 bug,而是對於大多數的 CRUD 程序來說,根本觸發不了底層組件的問題。

所以當沒有十足的證據來表明是基礎組件的問題時,我們還是老老實實地分析自己的代碼,別把路走偏了。

由於項目依賴較多,本地運行不太方便,於是我寫了一個小 demo

在寫 demo 時,一定要注意單一變量原則,項目中使用了組內基於 gin 封裝的一個小輪子,而 demo 中使用的是原生的 gin,最難受的是,最終排查到是小輪子的 bug。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM