协程-泄露
什么是goroutine leak
goroutine leak,是go協(xié)程泄漏,什么是go協(xié)程泄漏,通俗來(lái)說(shuō),開(kāi)啟了一個(gè)goroutine,用完后,我們要正確讓其結(jié)束。如果它沒(méi)用了,還沒(méi)結(jié)束,那就是goroutine leak。
泄漏的goroutine占用一部分cpu,還可能占著一些其他資源,從而影響主協(xié)程效率,有時(shí)甚至產(chǎn)生異常。
我們看下面的一個(gè)例子。
例子中我們的主協(xié)程需要通過(guò)某遠(yuǎn)程服務(wù)查詢到一個(gè)結(jié)果。使用一個(gè)multiQuery的函數(shù)啟動(dòng)多個(gè)協(xié)程,分別向不同的服務(wù)器發(fā)起查詢,只要收到一個(gè)服務(wù)器返回,multiQeury就返回結(jié)果
package mainimport ("fmt""math/rand""time" )func queryFromSrc(src string) (ret string) {nanoSec := time.Now().Nanosecond()rand.Seed(int64(nanoSec))sec := (rand.Int31() % 10) + 1// time sleep simulates dns lookup and querytime.Sleep(time.Second * time.Duration(sec))ret = fmt.Sprintf("src=%s use sec=%d", src, sec)fmt.Println("a query ok, ret=", ret)return ret }func multiQuery() (ret string) {res := make(chan string, 3)go func() {res <- queryFromSrc("ns1.dnsserver.com")}()go func() {res <- queryFromSrc("ns2.dnsserver.com")}()go func() {res <- queryFromSrc("ns3.dnsserver.com")}()return <-res }func main() {fmt.Println("start multi query:")res := multiQuery()fmt.Println("res=", res)//time.Sleep(time.Second * 20) }本案例使用了一個(gè)帶緩沖區(qū)的channel,multiQuery中的三個(gè)并行g(shù)o func不分先后從遠(yuǎn)程獲取一個(gè)結(jié)果返回。獲取的結(jié)果寫(xiě)入channel res,在第一個(gè)結(jié)果收到后,multiQuery就返回。返回的結(jié)果肯定是三個(gè)go func中最快返回的。(go func 中的queryFromSrc使用time.Sleep(random)來(lái)模擬不同請(qǐng)求延時(shí))。顯然,當(dāng)?shù)谝粋€(gè)結(jié)果返回后,multiQuery函數(shù)就結(jié)束了,而其他兩個(gè)go func還在等待返回。
如果我們使用不帶緩沖區(qū)的channel,兩個(gè)慢的goroutine將會(huì)卡在嘗試去發(fā)送他們的結(jié)果到同一個(gè)channel,而這個(gè)channel將沒(méi)有任何一個(gè)goroutine去讀。因?yàn)閙ultiQeury已經(jīng)執(zhí)行結(jié)束。這種情況叫做goroutine leak。與gc回自動(dòng)回收的變量不同,泄漏的goroutine不會(huì)自動(dòng)被回收。
所以編程中一定要注意,不使用的goroutine要讓其正確地終止
GC
在runtime的doc中描述了,通過(guò)設(shè)置環(huán)境變量GODEBUG='gctrace=1'可以讓go的運(yùn)行時(shí)把gc信息打印到stderr。
GODEBUG='gctrace=1' ./sentinel-agent >gc.log &gc.log的輸出如下:
gc781(1): 1+2385+17891+0 us, 60 -> 60 MB, 21971 (3503906-3481935) objects, 13818/14/7369 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc782(1): 1+1794+18570+1 us, 60 -> 60 MB, 21929 (3503906-3481977) objects, 13854/1/7315 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc783(1): 1+1295+20499+0 us, 59 -> 59 MB, 21772 (3503906-3482134) objects, 13854/1/7326 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc781:從程序啟動(dòng)開(kāi)始,第781次gc(1):參與gc的線程個(gè)數(shù)
1+2385+17891+0:分別是1)stop-the-world的時(shí)間,即暫停所有g(shù)oroutine;2)清掃標(biāo)記對(duì)象的時(shí)間;3)標(biāo)記垃圾對(duì)象的時(shí)間;4)等待線程結(jié)束的耗時(shí)。單位都是us,4者之和就是gc暫停的整體耗時(shí)
60 -> 60 MB:gc后,堆上存活對(duì)象占用的內(nèi)存,以及整個(gè)堆大小(包括垃圾對(duì)象)
21971 (3503906-3481935) objects:gc后,堆上的對(duì)象數(shù)量,gc前分配的對(duì)象以及本次釋放的對(duì)象
13818/14/7369 sweeps:描述對(duì)象清掃階段。一共有13818個(gè)memory span,其中14在后臺(tái)被清掃,7369在stop-the-world期間被清掃
0(0) handoff,0(0) steal:描述并行標(biāo)記階段的負(fù)載均衡特性。當(dāng)前在不同線程間傳送操作數(shù)和總傳送操作數(shù),以及當(dāng)前steal操作數(shù)和總steal操作數(shù)
0/0/0 yields:描述并行標(biāo)記階段的效率。在等待其他線程的過(guò)程中,一共有0次yields操做
經(jīng)過(guò)觀察gc的輸出,發(fā)現(xiàn)當(dāng)前堆上對(duì)象總數(shù)不斷增多,沒(méi)有減少的趨勢(shì),這說(shuō)明存在對(duì)象的泄露,從而導(dǎo)致內(nèi)存泄露
memory profile
根據(jù)golang官網(wǎng)profile指南?http://blog.golang.org/profiling-go-programs?,在代碼中添加
import _ "net/http/pprof"func main() {go func() {http.ListenAndServe("localhost:6060", nil)}() }可以在運(yùn)行時(shí)對(duì)程序進(jìn)行profile,通過(guò)http訪問(wèn):
go tool pprof http://localhost:6060/debug/pprof/heap進(jìn)行memory profile,默認(rèn)是--inuse_space,顯示當(dāng)前活躍的對(duì)象(不包括垃圾對(duì)象)占用的空間。使用--alloc_space可以顯示所有分配的對(duì)象(包括垃圾對(duì)象)。不過(guò)這兩種方式都沒(méi)有發(fā)現(xiàn)異常
監(jiān)控goroutine個(gè)數(shù)
通過(guò)runtime.NumGoroutine()可以獲取當(dāng)前的goroutine的個(gè)數(shù)。通過(guò)給程序添加http server獲取一些統(tǒng)計(jì)信息來(lái)了解程序的運(yùn)行狀態(tài),這是Jeff Dean推崇的方法。通過(guò)添加下述代碼來(lái)實(shí)時(shí)查看goroutine的個(gè)數(shù)
// goroutine stats and pprofgo func() {http.HandleFunc("/goroutines", func(w http.ResponseWriter, r *http.Request) {num := strconv.FormatInt(int64(runtime.NumGoroutine()), 10)w.Write([]byte(num))});http.ListenAndServe("localhost:6060", nil)glog.Info("goroutine stats and pprof listen on 6060")}()通過(guò)命令:
curl localhost:6060/goroutines查詢當(dāng)前的goroutine的個(gè)數(shù)。通過(guò)不程序運(yùn)行期間,不斷查看,發(fā)現(xiàn)goroutine個(gè)數(shù)不斷增加,沒(méi)有銷毀的跡象
goroutine泄露
通過(guò)上面的觀察,發(fā)現(xiàn)存在goroutine泄露,即goroutine沒(méi)有正常退出。由于每輪(每隔10秒執(zhí)行一次)都會(huì)創(chuàng)建多個(gè)goroutine,如果不能正常退出,則會(huì)存在大量的goroutine。go的gc使用的是mark and sweep,會(huì)從全局變量、goroutine的棧為根集合掃描所有的存活對(duì)象,如果goroutine不退出,就會(huì)泄露大量?jī)?nèi)存。
在確定是由于goroutine沒(méi)有正常退出后,重新review代碼,發(fā)現(xiàn)了泄露的根本原因。在重構(gòu)前,在信號(hào)處理程序中,為了正常結(jié)束程序,對(duì)于每個(gè)goroutine都有一個(gè)channel,用于主goroutine等待所有g(shù)oroutine正常結(jié)束后再退出。主goroutine中,信號(hào)處理程序用于等待所有g(shù)oroutine的代碼:
waiters = make([]chan int, Num) for _, w := range waiters {<- w }執(zhí)行檢查邏輯的goroutine在結(jié)束后,會(huì)調(diào)用ag.w <- 1,用于向主goroutine發(fā)送消息。
重構(gòu)后,由于每輪都會(huì)創(chuàng)建goroutine,由于用于主goroutine和檢查邏輯的goroutine之間的channel的大小是1,所以所有創(chuàng)建的檢查goroutine都阻塞在ag.w <- 1上,不能正常退出。最后,把channel邏輯去掉,就不存在goroutine泄露了
總結(jié)