Golang線上內(nèi)存爆掉問題排查(pprof)與解決
Golang線上內(nèi)存爆掉問題排查(pprof)
1 問題描述
某天,售后同事反饋,我們服務(wù)宕掉了,客戶無法預(yù)覽我們的圖片了。
- 我們預(yù)覽圖片是讀取存儲(chǔ)在我們S3服務(wù)的數(shù)據(jù),然后返回給前端頁面展示。
- 因?yàn)榭蛻舸嬖趲装費(fèi)的圖片,所以一旦請(qǐng)求并發(fā)一上來,很容易就把內(nèi)存打爆。
2 pprof分析
聲明: 涉及到數(shù)據(jù)敏感,以下代碼是我模擬線上故障的一個(gè)情況。
好在我們程序都有添加pprof監(jiān)控,于是直接通過go tool pprof分析:
①獲取堆內(nèi)存分配情況:go tool pprof http://xx/debug/pprof/heap
# localhost直接改成自己程序的IP:端口 go tool pprof http://localhost:80/debug/pprof/heap
②過濾出占用堆內(nèi)存前10的方法:top 10
# 過濾占用堆內(nèi)存排名前10方法 top 10
參數(shù)解析:
- flat:表示此函數(shù)分配、并由該函數(shù)持有的內(nèi)存空間。
- cum:表示由這個(gè)函數(shù)或它調(diào)用堆棧下面的函數(shù)分配的內(nèi)存總量。
③查看方法詳情:list testReadAll
可以看到我們自己程序的方法是main包下面的testAll方法占用了875MB多內(nèi)存。
# 查看方法詳情 list testReadAll
最后定位到ioutil.ReadAll這個(gè)方法占用了太多內(nèi)存。
- 熟悉的朋友都清楚,ioutil.ReadAll是直接將文件或者流數(shù)據(jù)一次性讀取到內(nèi)存里。如果文件過大或者多個(gè)請(qǐng)求同事讀取多個(gè)文件,會(huì)直接將服務(wù)器內(nèi)存打爆。
因?yàn)槲覀兊目蛻粲袔装費(fèi)的圖片,所以一旦并發(fā)以上來很可能打爆。因此這里需要改成流式的io.Copy
。
3 解決:改用流式io.Copy()
定位到問題后,直接改用流式方式給前端返回。
_, err = io.Copy(ctx.ResponseWriter(), file)
由于這次新人的失誤,加上測(cè)試數(shù)據(jù)量不夠大,導(dǎo)致出現(xiàn)線上問題,所以大家以后還是要多review代碼+增加壓力測(cè)試。
4 本地測(cè)試io.Copy與ioutil.ReadAll
編寫demo代碼
package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "io" "io/ioutil" "net/http" _ "net/http/pprof" "os" ) func main() { app := iris.New() go func() { http.ListenAndServe(":80", nil) }() //readAll app.Get("/readAll", testReadAll) //io.Copy app.Get("/ioCopy", func(ctx *context.Context) { file, err := os.Open("/Users/ziyi2/GolandProjects/MyTest/demo_home/io_copy_demo/xx.zip") if err != nil { panic(err) } defer file.Close() _, err = io.Copy(ctx.ResponseWriter(), file) if err != nil { panic(err) } }) app.Listen(":8080", nil) } func testReadAll(ctx *context.Context) { file, err := os.Open("/Users/ziyi2/GolandProjects/MyTest/demo_home/io_copy_demo/xx.zip") if err != nil { panic(err) } defer file.Close() //simulate onLine err bytes, err := ioutil.ReadAll(file) if err != nil { panic(err) } _, err = ctx.Write(bytes) if err != nil { panic(err) } }
打開資源監(jiān)視器,同時(shí)發(fā)起readAll請(qǐng)求,觀察內(nèi)存占用
- 發(fā)起readAll請(qǐng)求前
- 發(fā)送readAll請(qǐng)求
localhost:8080/readAll
我本地是模擬讀取差不多1G左右的文件,可以看到ioutil.ReadAll直接一次性將內(nèi)容讀取到了內(nèi)存。(一旦并發(fā)量上來,或者圖片文件超大,后果不敢想象)
再觀察io.Copy方法
- 發(fā)送ioCopy請(qǐng)求
localhost:8080/ioCopy
- 流式傳輸,最后程序內(nèi)存并沒有暴漲
結(jié)論:
ioutil.ReadAll:會(huì)將數(shù)據(jù)一次性加載到內(nèi)存。
io.Copy:流式拷貝,不會(huì)導(dǎo)致內(nèi)存暴漲
因此對(duì)于大文件或者數(shù)據(jù)量不確定的場(chǎng)景推薦使用io.Copy
拓展:pprof使用
① 引入pprof
- 引入pprof包
- 開啟一個(gè)協(xié)程監(jiān)聽
package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "net/http" _ "net/http/pprof" "os" ) func main() { app := iris.New() go func() { http.ListenAndServe(":80", nil) }() app.Listen(":8080", nil) }
②查看分析報(bào)告
1 瀏覽器直接訪問
http://IP:Port/debug/pprof
2 go tool 命令行直接分析
# 查看堆內(nèi)存信息 go tool pprof http://IP:Port/debug/pprof/heap # 查看cpu信息 go tool pprof http://IP:Port/debug/pprof/profile ## -seconds=5設(shè)置采樣時(shí)間為5s # go tool pprof -seconds=5 http://IP:Port/debug/pprof/profile # 查看協(xié)程信息 go tool pprof http://IP:Port/debug/pprof/goroutine # 查看代碼阻塞信息 go tool pprof http://IP:Port/debug/pprof/block # 需要查看什么信息將URL默認(rèn)的Type更換為對(duì)應(yīng)類型即可
-seconds=30 設(shè)置采樣30s,也可以自定義時(shí)間范圍。需要注意的是,對(duì)于profile而言,總是需要采樣一段時(shí)間,才可以看到數(shù)據(jù)。而其他歷史累計(jì)的數(shù)據(jù),則可以直接獲取從程序開始運(yùn)行到現(xiàn)在累積的數(shù)據(jù),也可以設(shè)置-seconds來獲取一段時(shí)間內(nèi)的累計(jì)數(shù)據(jù)。而其他實(shí)時(shí)變化的指標(biāo),設(shè)置這個(gè)參數(shù)沒什么用,只會(huì)讓你多等一會(huì)。
- allocs: A sampling of all past memory allocations【所有內(nèi)存分配,歷史累計(jì)】
- block: Stack traces that led to blocking on synchronization primitives【導(dǎo)致阻塞同步的堆棧,歷史累計(jì)】
- cmdline: The command line invocation of the current program【當(dāng)前程序命令行的完整調(diào)用路徑】
- goroutine: Stack traces of all current goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.【當(dāng)前所有運(yùn)行的goroutine堆棧信息,實(shí)時(shí)變化】
- heap: A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.【查看活動(dòng)對(duì)象的內(nèi)存分配情況,實(shí)時(shí)變化】
- mutex: Stack traces of holders of contended mutexes【導(dǎo)致互斥鎖競(jìng)爭(zhēng)持有者的堆棧跟蹤,歷史累計(jì)】
- profile: CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.【默認(rèn)進(jìn)行30s的CPU Profing,用于觀察CPU使用情況】
- threadcreate: Stack traces that led to the creation of new OS threads【查看創(chuàng)建新OS線程的堆棧跟蹤信息】
- trace: A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.【當(dāng)前程序執(zhí)行鏈路】
注意
:默認(rèn)情況下是不追蹤block和mutex的信息的,如果想要看這兩個(gè)信息,需要在代碼中加上兩行:
runtime.SetBlockProfileRate(1) // 開啟對(duì)阻塞操作的跟蹤,block runtime.SetMutexProfileFraction(1) // 開啟對(duì)鎖調(diào)用的跟蹤,mutex
3 導(dǎo)出為.out文件+命令行分析(推薦)
推薦使用導(dǎo)出文件方式,雖然步驟繁瑣,但是有文件落地,保證重要數(shù)據(jù)不會(huì)丟失
//導(dǎo)出為文件 curl -o heap.out http://IP:Port/debug/pprof/heap //解析文件并進(jìn)入命令行交互 go tool pprof heap.out //后續(xù)操作就和命令行直接分析如出一轍 //top 10 //list funcName
③參數(shù)解析
1 采樣類型
allocs:所有內(nèi)存分配,歷史累計(jì)
allocs: A sampling of all past memory allocations【所有內(nèi)存分配,歷史累計(jì)】
block:導(dǎo)致阻塞同步的堆棧信息,歷史累計(jì)(每發(fā)生一次阻塞取樣一次)
block: Stack traces that led to blocking on synchronization primitives【導(dǎo)致阻塞同步的堆棧,歷史累計(jì)】
- Block Goroutine阻塞事件的記錄 默認(rèn)每發(fā)生一次阻塞事件時(shí)取樣一次
cmdline:程序命令行的完整調(diào)用路徑
cmdline: The command line invocation of the current program【當(dāng)前程序命令行的完整調(diào)用路徑】
goroutine:當(dāng)前程序運(yùn)行的所有g(shù)oroutine,實(shí)時(shí)變化(獲取時(shí)取樣一次)
goroutine: Stack traces of all current goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.【當(dāng)前所有運(yùn)行的goroutine堆棧信息,實(shí)時(shí)變化】
- 活躍Goroutine信息的記錄 僅在獲取時(shí)取樣一次
heap:查看堆內(nèi)存分配情況,實(shí)時(shí)變化(每分配512K取樣一次)
heap: A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.【查看活動(dòng)對(duì)象的內(nèi)存分配情況,實(shí)時(shí)變化】
- Heap 堆內(nèi)存分配情況的記錄 默認(rèn)每分配512K字節(jié)時(shí)取樣一次
mutex:導(dǎo)致互斥鎖競(jìng)爭(zhēng)的堆棧跟蹤,歷史累計(jì)
mutex: Stack traces of holders of contended mutexes【導(dǎo)致互斥鎖競(jìng)爭(zhēng)持有者的堆棧跟蹤,歷史累計(jì)】
profile:CPU使用情況
profile: CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.【默認(rèn)進(jìn)行30s的CPU Profing,用于觀察CPU使用情況】
threadcreate:創(chuàng)建新線程的堆棧信息(獲取時(shí)取樣)
threadcreate: Stack traces that led to the creation of new OS threads【查看創(chuàng)建新OS線程的堆棧跟蹤信息】
- 系統(tǒng)線程創(chuàng)建情況的記錄 僅在獲取時(shí)取樣一次
trace:程序整個(gè)執(zhí)行鏈路
trace: A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.【當(dāng)前程序執(zhí)行鏈路】
注意
:默認(rèn)情況下是不追蹤block和mutex的信息的,如果想要看這兩個(gè)信息,需要在代碼中加上兩行:
runtime.SetBlockProfileRate(1) // 開啟對(duì)阻塞操作的跟蹤,block runtime.SetMutexProfileFraction(1) // 開啟對(duì)鎖調(diào)用的跟蹤,mutex
2 統(tǒng)計(jì)維度(以內(nèi)存取樣為例)
如果是初次接觸pprof,可能會(huì)疑惑flat、sum、cum代表什么意思
官網(wǎng)解析:
- The first two columns show the number of samples in which the function was running (as opposed to waiting for a called function to return), as a raw count and as a percentage of total samples.
- The third column shows the running total during the listing.
- The fourth and fifth columns show the number of samples in which the function appeared (either running or waiting for a called function to return). To sort by the fourth and fifth columns, use the -cum (for cumulative) flag.
- 官網(wǎng)地址:https://go.dev/blog/pprof
以獲取內(nèi)存為例:
flat:當(dāng)前函數(shù)分配的內(nèi)存,不包含它調(diào)用其他函數(shù)造成的內(nèi)存分配
flat%:當(dāng)前函數(shù)分配內(nèi)存占比
sum%:自己和前面所有的flat%累積值
cum:當(dāng)前函數(shù)及當(dāng)前函數(shù)調(diào)用其他函數(shù)的分配內(nèi)存的匯總
cum%:這個(gè)函數(shù)分配的內(nèi)存,以及它調(diào)用其他函數(shù)分配的內(nèi)存之和
以上就是Golang線上內(nèi)存爆掉問題排查(pprof)與解決的詳細(xì)內(nèi)容,更多關(guān)于Golang線上內(nèi)存爆掉的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用Golang讀取toml配置文件的代碼實(shí)現(xiàn)
在開發(fā)過程中,配置文件是必不可少的一部分,它使我們能夠在不更改代碼的情況下更改應(yīng)用程序的行為,TOML是一種簡(jiǎn)單易讀的配置文件格式,本文將介紹如何使用Golang來讀取TOML配置文件,需要的朋友可以參考下2024-04-04Golang使用反射的動(dòng)態(tài)方法調(diào)用詳解
Go是一種靜態(tài)類型的語言,提供了大量的安全性和性能。這篇文章主要和大家介紹一下Golang使用反射的動(dòng)態(tài)方法調(diào)用,感興趣的小伙伴可以了解一下2023-03-03golang?使用chromedp獲取頁面請(qǐng)求日志network
這篇文章主要為大家介紹了golang?使用chromedp獲取頁面請(qǐng)求日志network方法實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11gorm整合進(jìn)go-zero的實(shí)現(xiàn)方法
go-zero提供的代碼生成器里面,沒有提供orm框架操作,但是提供了遍歷的緩存操作,所以可以利用gorm當(dāng)作一個(gè)sql語句的生成器,把生成后的sql語句放到go-zero生成的模板中去執(zhí)行,對(duì)gorm整合進(jìn)go-zero的實(shí)現(xiàn)方法感興趣的朋友一起看看吧2022-03-03使用Go語言實(shí)現(xiàn)benchmark解析器
這篇文章主要為大家詳細(xì)介紹了如何使用Go語言實(shí)現(xiàn)benchmark解析器并實(shí)現(xiàn)及Web UI 數(shù)據(jù)可視化,文中的示例代碼講解詳細(xì),需要的小伙伴可以參考一下2025-04-04