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

【導讀】遇到 OOM 不要手慌腳亂,細心大膽用 pprof 看起來問題。本文介紹了一個 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)

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

此時我的內心開始甩鍋了,可能是環境的問題?老項目用的是 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。

轉自:

cnblogs.com/yahuian/p/once-go-programe-oom-fix.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/idmvr9mKZ8YL_I8WC645zg