問題描述
之前組內一個線上服務的內存使用率穩定上揚, 查看監控,發現內存的使用趨勢如下圖,這種趨勢是典型的內存泄露,不解決的話服務會OOM。
問題定位
于是嘗試用pprof定位問題,俗話說:Go里面10次內存泄露9次是goroutine泄露。這次也不例外, 系統的內存泄露是由類似下面的代碼引起的:
package main
import (
"fmt"
"net/http"
"sync"
_ "net/http/pprof"
)
func bug(_ http.ResponseWriter, _ *http.Request) {
taskChan := make(chan int, 100)
for i := 0; i < 100; i++ {
taskChan <- i
}
consumer := func() {
for task := range taskChan {
fmt.Println(task)
}
}
for i := 0; i < 100; i++ {
go consumer()
}
}
func main() {
http.HandleFunc("/bug", bug)
http.ListenAndServe(":8000", nil)
}
這里的 bug
函數是一個典型的 生產者-消費者
模型,邏輯是這樣的:
- 聲明一個容量為100的隊列buffer
- 開一個循環,生產消息并發送至隊列
- 聲明消費者,消費者會對隊列里的元素進行rpc操作 (這里用fmt.Print代替)
- 開100個協程進行消費
讓我們嘗試用 pprof
工具定位一下問題,首先在瀏覽器里訪問若干次這個url: http://127.0.0.1:8000/bug
,然后訪問pprof的url查看系統運行情況:http://127.0.0.1:8000/debug/pprof/
, 發現系統中有大量的goroutine阻塞著(平時只有200~300個)。
點擊goroutine,進入 http://127.0.0.1:8000/debug/pprof/goroutine?debug=1
這個鏈接,此時系統中一共有1704個goroutine,其中有1700個goroutine阻塞在 /go/src/go-test/bug/chan_OOM.go:18
這一行:
把url參數改為 debug=2
,可以看到每個goroutine的信息,用剛剛的調用棧以及行數作為查找參數,隨意找一個goroutine查看詳細信息:
這里顯示ID為448的goroutine當前的狀態為 chan receive
,阻塞了12分鐘,阻塞在18行。
這個bug接口會被定時訪問,每次訪問都會新起100個goroutine。這些goroutine一直處于[chan receive] 的狀態無法釋放,導致goroutine占用的內存無法釋放,系統長期運行下去,最終服務無可用內存,OOM~
問題的原因以及解決
goroutine泄露大概有兩個場景:
channel操作阻塞導致runtime期間goroutine一直在阻塞等待;
goroutine有死循環;
這段代碼發生泄露的原因就是channel沒有關閉,goroutine一直引用channel,沒有得到退出信號,導致其一直存活。知道了這個原因之后,還是比較好改的,把channel關閉即可。
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"sync"
)
func bug(_ http.ResponseWriter, _ *http.Request) {
taskChan := make(chan int, 100)
for i := 0; i < 100; i++ {
taskChan <- i
}
consumer := func() {
for task := range taskChan {
fmt.Println(task)
}
}
for i := 0; i < 100; i++ {
go consumer()
}
}
func bugfix(_ http.ResponseWriter, _ *http.Request) {
taskChan := make(chan int, 100)
for i := 0; i < 100; i++ {
taskChan <- i
}
consumer := func() {
for task := range taskChan {
fmt.Println(task)
}
}
for i := 0; i < 100; i++ {
go consumer()
}
close(taskChan) // bugfix
}
func main() {
http.HandleFunc("/bug", bug)
http.HandleFunc("/bugfix", bugfix)
http.ListenAndServe(":8000", nil)
}
總結
俗話說: Go里面10次內存泄露9次是goroutine泄露引起的。而goroutine泄露大多是由于channel使用不當造成的。這個bug雖然不是我寫的,但是要引以為戒~ 平時多看看channel不同模型的使用案例,不然容易踩坑~