Go 細(xì)節(jié)篇|內(nèi)存回收又踩坑了
背景提要
分享一個(gè) GC 相關(guān)的踩坑實(shí)踐。公司線上某組件內(nèi)存資源泄漏,偶發(fā) oom 。通過 Go 的 pprof 排查,很快速定位到泄漏的數(shù)據(jù)結(jié)構(gòu) A ,結(jié)構(gòu) A 的相關(guān)資源是通過 Go 的 Finalizer 機(jī)制來釋放的。但詭異的來了,對(duì)照著代碼審視了多次之后,大家一致斷定,這段代碼絕對(duì)沒有泄漏的問題。但是,事實(shí)勝于雄辯,現(xiàn)實(shí)就是泄漏就在此處。想不通。。。
幾天之后,問題的轉(zhuǎn)機(jī)來自于另一個(gè)毫不相關(guān)的地方,我們發(fā)現(xiàn)了一個(gè)卡住的協(xié)程。最開始并不在意,因?yàn)殡m然卡住是異常的,但是泄漏的地點(diǎn)差了十萬八千里,兩者毫不相關(guān)。所以剛開始是忽略的。
后來實(shí)在是想不開,閑來無事,把這個(gè)異常點(diǎn)拿來看,才發(fā)現(xiàn)一點(diǎn)點(diǎn)線索。這個(gè)卡住的協(xié)程是一個(gè)結(jié)構(gòu)體 B 的釋放過程,和 A 一樣也是 Go 的 Finalizer 機(jī)制。我們踩的坑就于此有關(guān),很典型,出人意料,所以分享給大家。先復(fù)習(xí)一下 Finalizer 機(jī)制。
什么是 Go 的 Finalizer 機(jī)制?
那么什么是 Finalizer 機(jī)制呢?這個(gè)就必須要再提一嘴 Go 的 GC 機(jī)制了。這個(gè)是 Go 比較有特色的機(jī)制。在 Go 里程序員負(fù)責(zé)申請(qǐng)內(nèi)存,Go 的 runtime 的 GC 機(jī)制負(fù)責(zé)回收。
在這個(gè)過程,Go 語言還提供了一個(gè) Finalizer 機(jī)制,允許程序員在申請(qǐng)的時(shí)候指定一個(gè)回調(diào)函數(shù),在 GC 回收到這個(gè)結(jié)構(gòu)體內(nèi)存的時(shí)候,Go 會(huì)自動(dòng)調(diào)用一次這個(gè)回調(diào)函數(shù)。
這個(gè)非常實(shí)用的一個(gè)技巧,在文章《??編程思考:對(duì)象生命周期的問題??》里有分享。主要是比較安全的解決掉對(duì)象聲明周期的問題。因?yàn)槌绦騿T自己來管理資源的釋放,那很可能出 bug ,比如在有人用的時(shí)候調(diào)用釋放。通過 Finalizer 機(jī)制,則能保證一定是無人引用的結(jié)構(gòu)體內(nèi)存,才會(huì)執(zhí)行回調(diào)。
舉個(gè)例子:
上面的例子,給結(jié)構(gòu)體 TestStruct 的釋放設(shè)置了一個(gè) Finalizer 回調(diào)函數(shù)。然后在主動(dòng)調(diào)用 runtime.GC 來快速回收,童鞋可以體驗(yàn)一下。
Finalizer 這里竟然有個(gè)坑?
Finalizer 很好用這是事實(shí),但 Finalizer 機(jī)制也有限制條件,在官網(wǎng)上有如下聲明:
A single goroutine runs all finalizers for a program, sequentially. If a finalizer must run for a long time, it should do so by starting a new goroutine.
來自 https://golang.google.cn/pkg/runtime/#SetFinalizer ,什么意思?
說得是,Go 的 runtime 是用一個(gè)單 goroutine 來執(zhí)行所有的 Finalizer 回調(diào),還是串行化的。
劃重點(diǎn):一旦執(zhí)行某個(gè) Finalizer 出了問題,可能會(huì)影響到全局的 Finalizer 回調(diào)函數(shù)的執(zhí)行。
原來如此??!
我們這次就是精準(zhǔn)踩坑。在釋放 B 結(jié)構(gòu)體的時(shí)候,調(diào)用了一個(gè) Finalizer 回調(diào),然后把協(xié)程卡死了。導(dǎo)致后續(xù)所有的 Finalizer 回調(diào)都執(zhí)行不了,比如 A 的 Finalizer 就無法執(zhí)行,從而導(dǎo)致資源的泄漏和各種的異常。
舉個(gè)例子:
這里創(chuàng)建了一個(gè)極簡的例子,A,B, C 實(shí)例都設(shè)置了 Finalizer 回調(diào),故意讓其中一個(gè)阻塞住,會(huì)影響到剩下的 Finalizer 的執(zhí)行。
總結(jié)
- Go 提供的 Finalizer 機(jī)制,讓程序員創(chuàng)建的時(shí)候注冊回調(diào)函數(shù),能很好的幫助程序員解決資源安全釋放的問題;
- Finalizer 的執(zhí)行是全局單協(xié)程,且串行化執(zhí)行的。所以可能會(huì)因?yàn)槟骋淮蔚目ㄗ?dǎo)致全局的失效,切記;
- 排查內(nèi)存問題的時(shí)候,pprof 看現(xiàn)場很明確,但是根因可能是看似毫不相關(guān)的旮旯角落,有時(shí)候要把思維跳出來排查;