終于解決了這個線上偶現(xiàn)的panic問題
不知道其他人是不是這樣,反正老許最怕聽到的詞就是“偶現(xiàn)”,至于原因我不多說,懂的都懂。
下面直接看panic信息。
runtime?error:?invalid?memory?address?or?nil?pointer?dereference
panic(0xbd1c80,?0x1271710)
????????/root/.go/src/runtime/panic.go:969?+0x175
github.com/json-iterator/go.(*Stream).WriteStringWithHTMLEscaped(0xc00b0c6000,?0x0,?0x24)
????????/go/pkg/mod/github.com/json-iterator/[email protected]/stream_str.go:227?+0x7b
github.com/json-iterator/go.(*htmlEscapedStringEncoder).Encode(0x12b9250,?0xc0096c4c00,?0xc00b0c6000)
????????/go/pkg/mod/github.com/json-iterator/[email protected]/config.go:263?+0x45
github.com/json-iterator/go.(*structFieldEncoder).Encode(0xc002e9c8d0,?0xc0096c4c00,?0xc00b0c6000)
????????/go/pkg/mod/github.com/json-iterator/[email protected]/reflect_struct_encoder.go:110?+0x78
github.com/json-iterator/go.(*structEncoder).Encode(0xc002e9c9c0,?0xc0096c4c00,?0xc00b0c6000)
????????/go/pkg/mod/github.com/json-iterator/[email protected]/reflect_struct_encoder.go:158?+0x3f4
github.com/json-iterator/go.(*structFieldEncoder).Encode(0xc002eac990,?0xc0096c4c00,?0xc00b0c6000)
????????/go/pkg/mod/github.com/json-iterator/[email protected]/reflect_struct_encoder.go:110?+0x78
github.com/json-iterator/go.(*structEncoder).Encode(0xc002eacba0,?0xc0096c4c00,?0xc00b0c6000)
????????/go/pkg/mod/github.com/json-iterator/[email protected]/reflect_struct_encoder.go:158?+0x3f4
github.com/json-iterator/go.(*OptionalEncoder).Encode(0xc002e9f570,?0xc006b18b38,?0xc00b0c6000)
????????/go/pkg/mod/github.com/json-iterator/[email protected]/reflect_optional.go:70?+0xf4
github.com/json-iterator/go.(*onePtrEncoder).Encode(0xc002e9f580,?0xc0096c4c00,?0xc00b0c6000)
????????/go/pkg/mod/github.com/json-iterator/[email protected]/reflect.go:219?+0x68
github.com/json-iterator/go.(*Stream).WriteVal(0xc00b0c6000,?0xb78d60,?0xc0096c4c00)
????????/go/pkg/mod/github.com/json-iterator/[email protected]/reflect.go:98?+0x150
github.com/json-iterator/go.(*frozenConfig).Marshal(0xc00012c640,?0xb78d60,?0xc0096c4c00,?0x0,?0x0,?0x0,?0x0,?0x0)
首先我堅信一條,開源的力量值得信賴。因此老許第一波操作就是,分析業(yè)務(wù)代碼是否有邏輯漏洞。很明顯,同事也是值得信賴的,因此果斷猜測是某些未曾設(shè)想到的數(shù)據(jù)觸發(fā)了邊界條件。接下來就是保存現(xiàn)場的常規(guī)操作。
如標(biāo)題所說,這是偶現(xiàn)的panic問題,因此按照上面的分類采用符合當(dāng)前技術(shù)棧的方法保存現(xiàn)場即可。接下來就是坐等收獲的季節(jié),而這一等就是好多天。中間數(shù)次收到告警,卻沒有符合預(yù)期的現(xiàn)場。
這個時候我不僅不慌,甚至還有點小激動。某某曾曰:“要敢于質(zhì)疑,敢于挑戰(zhàn)權(quán)威”,一念至此便一發(fā)不可收拾,我老許又要為開源事業(yè)做出貢獻(xiàn)了嘛!說干就敢干,懷著小心思開始閱讀json-iterator的源碼。
剛開始研讀我便明白了一個道理, “當(dāng)上帝關(guān)了這扇門,一定會為你打開另一扇門”這句話是騙人的。老許只覺得上帝不僅關(guān)上了所有的門甚至還關(guān)上了所有的窗。下面我們看看他到底是怎么關(guān)門的。
func?(cfg?*frozenConfig)?Marshal(v?interface{})?([]byte,?error)?{
?stream?:=?cfg.BorrowStream(nil)
?defer?cfg.ReturnStream(stream)
?stream.WriteVal(v)
?if?stream.Error?!=?nil?{
??return?nil,?stream.Error
?}
?result?:=?stream.Buffer()
?copied?:=?make([]byte,?len(result))
?copy(copied,?result)
?return?copied,?nil
}
//?WriteVal?copy?the?go?interface?into?underlying?JSON,?same?as?json.Marshal
func?(stream?*Stream)?WriteVal(val?interface{})?{
?if?nil?==?val?{
??stream.WriteNil()
??return
?}
?//?省略其他代碼
}
根據(jù)panic棧知道是因為空指針造成了panic,而(*frozenConfig).Marshal函數(shù)內(nèi)部已經(jīng)做了非空判斷。到此,老許真的已經(jīng)別無他法只得戰(zhàn)略性放棄解決此次panic。畢竟,這個影響也沒那么大,而且程序員哪有修的完的bug呢。經(jīng)過這樣一番安慰,心里確實容易接受多了。
事實上,在較長一段時間內(nèi)我都有意識地忽略這個問題,畢竟沒有找到問題的根因。這個問題在線上一直持續(xù)到一個說不上來什么日子的日子,總而言之就是興致來了,我再次看了兩眼,而這兩眼很關(guān)鍵!
func?doReq()?{
????req?:=?paramsPool.Get().(*model.Params)
????//?defer?1
????defer?func()?{
?????reqBytes,?_?:=?json.Marshal(req)
?????//?省略其他打印日志的代碼
????}()
????//?defer?2
????defer?paramsPool.Put(req)
????//?req初始化以及發(fā)起請求和其他操作
}
“注:
”
上述代碼變量命名已經(jīng)被老許通用化處理。 項目中實際代碼遠(yuǎn)比上述復(fù)雜,但上述代碼依舊是造成本次問題的最小原型。
上面代碼中paramsPool是sync.Pool類型的變量,而sync.Pool想必大家都很熟悉。sync.Pool是為了復(fù)用已經(jīng)使用過的對象(協(xié)程安全),減少內(nèi)存分配和降低GC壓力。
type?test?struct?{
?a?string
}
var?sp?=?sync.Pool{
?New:?func()?interface{}?{
??return?new(test)
?},
}
func?main()?{
?t?:=?sp.Get().(*test)
?fmt.Println(unsafe.Pointer(t))
?sp.Put(t)
?t1?:=?sp.Get().(*test)
?t2?:=?sp.Get().(*test)
?fmt.Println(unsafe.Pointer(t1),?unsafe.Pointer(t2))
}
根據(jù)上述代碼和輸出結(jié)果知,t1變量和t變量地址一致,因此他們是復(fù)用對象。此時再回顧上面的doReq函數(shù)就很容易發(fā)現(xiàn)問題的根因。
defer 2和defer 1順序反了!!!
defer 2和defer 1順序反了!!!
defer 2和defer 1順序反了!!!
sync.Pool提供的Get和Put方法是協(xié)程安全的,但是高并發(fā)調(diào)用doReq函數(shù)時json.Marshal(req)和請求初始化會存在并發(fā)問題,極有可能引起panic的并發(fā)調(diào)用時間線如下圖所示。
既然已經(jīng)找到原因,解決起來就容易多了,只需調(diào)整defer 2和defer 1的調(diào)用順序即可。老許將修改后的代碼發(fā)布到線上后也確實再沒有出現(xiàn)panic。造成這次事故的根本原因是一個微乎其微的細(xì)節(jié),所以我們平時在開發(fā)中還是要謹(jǐn)慎加謹(jǐn)慎,避免因為這種小白錯誤造成不可挽回的損失。另外一個經(jīng)驗之談就是,開發(fā)和查問題時盡量不要鉆牛角尖,適當(dāng)?shù)耐nD可能會有意想不到的奇效。
最后,衷心希望本文能夠?qū)Ω魑蛔x者有一定的幫助。
推薦閱讀



