記一次細思極恐的FastJson差點引發的大面積故障

阿里妹導讀
作者記錄了一次FastJson差點引發的大面積故障的排查過程和解決方案。
在短短不到兩年的開發生涯裡,加上這次,印象中已經碰到過至少3次FastJson的問題了。而且FastJson不同版本之間的差異很大,各位同學在使用時一定注意不要踩坑。
下面講一下我碰到的這個細思極恐的問題。
一、首先講講工程背景
我們的工程是Kotlin與Java混編的,再附加偶爾寫寫Groovy,在團隊中不斷熟悉後發現各語言在程式設計中各有利弊。
  • Java就不說了,阿里在國內Java應用上堪稱鼻祖,集團各種工具對Java的支援都比較完備;
  • Kotlin有很多語法上的優勢,同樣的程式碼Java 10行,Kotlin可能只需要5行,此外支援協程(coroutines),編寫非阻塞非同步程式碼看起來像是同步的,能很好地處理IO密集型任務。但是Kotlin畢竟非正統,集團很多工具對Kotlin的支援性比較一般;
  • groovy的語法規則更加特殊,工程裡用的不是特別多,所以基本都是需要開發時用大模型去解決,沒有特意去研究;
二、問題現象
突然有一天組裡同學告訴我預發有大量報錯,是不是我改了架構層面的程式碼引起的。我心想肯定不是啊,我這是增量程式設計,並沒有改動太多原來的程式碼。
但是處於謹慎起見,還是把分支踢掉吧,重新部署了一下,發現同樣的問題在工程重啟一小段時間後又發生了。
下面是問題報錯,這會導致工程執行時大部分涉及反序列化的鏈路中斷,而工程到處用了FastJson,影響面可想而知。
三、排查過程
1. 懷疑FastJson版本
既然是FastJson的拋錯,那是不是有人改動了工程依賴,於是開始查pom檔案的改動,一時也想不到別的原因可能導致該報錯。
查了一圈並沒有發現可疑的地方,於是上AppInsights中看了一下FastJson相關類的載入情況,發現這裡有個1.2.68_noneautotype版本的包,而工程其實統一用的是1.2.83_noneautotype,會不會是這裡引起的呢?
後來登到線上機器對比了一下發現沒有這個版本,那有可能就是別的包引入的,於是看了一下出問題module的pom檔案,發現果然有這個版本的FastJson,再定位了下發現是rass-sdk-core引入了這個包,scope為compile,打包時會將這個包編譯進工程中。
很果斷的將該包中的FastJson排掉,那問題鐵定解決了,觀察了日誌發現沒有再拋錯,我就沒再管。到了下午同學又來反饋有問題,我一看還真是,難道是搞錯了。
再次對比了一下預發和線上,發現還真是我搞錯了,線上也有1.2.68_noneautotype。
我有點鬱悶,為了定位這個bug,翻遍了預發每個人的分支,為了排包前後部署過很多次。此時這個問題已經困擾了我1天多了,想撂挑子放棄,到時候誰發線上時注意下好了。
但又一想這是我們辛苦維護的龐大工程,不能因為這種莫名其妙的問題引發大面積故障,到時候辛苦半年多白乾(上價值,給自己增加點動力)。理智告訴我哪怕手頭的需求先放一放,還得繼續查,雖然只是預發。
2. 懷疑kotlin相關依賴
跟同學確認了JDK近期也沒有變動後,接下來開始到處翻資料。
大概能確認是反序列化Kotlin的data class實體時出現的問題。類似下面這
GitHub上關於該問題的討論也比較多,總結一下解法有:
1.FastJson和kotlin版本不相容
2.工程中需要引入kotlin-reflect
3.需要對data class的非空欄位預設初始化
4.……
第三個欄位初始化問題不太可能,因為受影響的class從來沒改動過,不可能好端端出問題。估計問題應該還是出在工程環境上。
於是去工程裡看了一下kotlin-reflect依賴,仍然是正常引入的。
又去看了幾遍近期所有分支的所有改動,也沒有任何相關變動,慢慢地開始頭麻,環境問題一向是最難查的。。。。
3. 翻閱到相似的例子
接下來就還是一直在網上翻閱其他資料,後來在掘金找到一篇:
https://juejin.cn/post/6929740384734019597
文章提到的問題現象跟我們的還挺像的,都是一開始正常,過了一會兒就又出現問題。
文章對FastJson剖析挺深,感覺我們的工程不至於這麼奇葩吧。還提到了什麼getKoltinConstructorParameters、Unit變數、kotlin_error標記位問題。核心原因是有個靜態標記位被置為異常狀態。
雖然抽象,但死馬當活馬醫,試試看吧。
先看工程裡有沒有列印這個NPE吧,SLS日誌搜了半天都沒搜到,去原始碼看了一下,原來這裡只打印了異常堆疊,那不會被採集到SLS中,不過伺服器日誌裡可能會有。
媽耶還真給我搜到了,再仔細讀了一下打堆疊的地方,會將kotlin_error置為true,而且關鍵的地方在於這玩意兒是個static volatile變數!!具有靜態共享特徵(static)和多執行緒可見性(volatile),意味著整個工程使用的都是被修改後的值。
4. ☆ 鎖定關鍵報錯位置
繼續進QuestionCardProxyApi 228行 找到報錯源,發現確實有個toJsonString。對於invokeResumeReq這個物件,我一眼看到有個古怪的賦值,this.resumeBody被賦值為{}。但這個賦值有何神奇之處,哪能這麼個小玩意兒導致全域性異常啊??
這段日誌程式碼是12-17新增的,時間看了下還挺符合。再看resumeBody其實是個Java類的一個object型別欄位,kotlin中這樣對其賦值,編譯器倒也沒報錯
其實到這個地方已經感覺要水落石出了,再一看master,媽耶這個程式碼上線了,但線上卻沒有任何報錯,執行一切正常。
但基本能確定是這裡無疑了,就趕緊拉寫這段程式碼的同學看(悄悄說,仁兄一開始提給我的bug),確認線上還沒開始灰度,無任何流量後放心了。他在家裡加班修,我繼續看原理。
首先肯定本意是想將resumeBody置空,誤用了{},{}在kotlin中被編譯器解釋為一個lambda表示式。
再看debug資訊,這裡resumeBody實際上被解釋為 ()->kotlin.Unit 這樣一個lambda函式,arity表示函式的引數數量為0.
也就是說:{}是一個沒有任何入參,並且返回值預設為Unit型別的一個函式(沒有明確返回值時預設Unit型別)。
為了更清晰地瞭解這個語法,可以看下面這個例子:這裡定義了一個函式式變數x,入參為s,出參為s+“000”。在使用時可以直接以函式的方式呼叫x,最終得到y=111000
FastJson自然無法正確解析這樣的一個Object欄位,而最令人細思極恐的問題是 解析這個物件報了錯,卻把kotlin_error置為true,而後有沒有地方將其復原為false,導致所有的反序列化都進到這裡,返回不正確的結果,錯誤結果被外層攔截丟擲default constructor not found異常。這個影響面是巨大的,會導致整個工程崩潰。
具體程式碼解釋:
1.一開始 kotlin_error !=true ,會進到 kotlin_kclass_getConstructors.invoke獲取類構造器,這裡拋錯了,把kotlin_error改為true;
2.往後所有相關的FastJson序列化、反序列化重新進到這裡都會return null,導致類構造器獲取失敗;
3.外層沒拿到paramNames,直接拋了異常。
5. 有問題的程式碼(自測)
有kotlin執行環境的同學可以嘗試執行下面的程式碼自測。
注意:經測試這段程式碼在FastJson 2.0.53版本可以正常執行,其他版本同學們可以再自測下。
classKotlinErrorTest { @Test fun testFastJson(){ val jsonObj = JSONObject().apply {this["accessorUuid"] = "accessorUuid"this["orgId"] = 4343Lthis["resumeTaskId"] = "resumeUrl"this["resumeBody"] = {} }// 這裡 toJSONString 破壞了 kotlin_error 這個變數 JSON.toJSONString(jsonObj)// 定義一個物件A,並序列化 val testA = A("1") val strA = JSON.toJSONString(testA)// 反序列化拿data class A的欄位時會直接返回null,導致 default constructor not found val objA = JSON.parseObject(strA, A::class.java) println(objA) } data class A( val a: String ) @Test fun testLambda() { val x = { s: String -> s + "000" } val y = x("111") println(y) }}
四、總結&反思
至此,問題定位清楚並徹底解決了。這次bug是工作以來碰到的最抽象的一個,耗時兩天。雖然有點低效,但找到原因後俺非常激動,逮住組裡同學細細講了一番,估計這麼抽象的問題工作多年的大佬也不一定能遇到。
再次膜拜一下掘金大佬:https://juejin.cn/post/6929740384734019597
另外也反思了以下幾個問題:
1.工程中Java、kotlin、groovy等多語言混編,對開發同學的語法掌握程度有較高要求,有時候各語言間會混淆,特別是判空、變數定義規則等。這些語言的線上空指標我都嘗過,有點酸爽。這次同學出現語法問題,其實也是有點語言混淆了,如果純Java,鐵定不會甩個{}上去;
2.這次線上無異常,得益於灰度開關(哥們可得感謝我,還沒放量,否則一旦流量進來,涉及kotlin的鏈路全部中斷就寄了);
3.FastJson是有很多漏洞的,使用時仍然要高度注意。任何框架都是不能完全信任的,畢竟程式碼都是人寫出來的,Bug要大家一起合力發現hhh;
4.寫Bug是每個開發同學都會經歷的,朋友經常嘲笑我寫Bug。但實際上你寫過的Bug越多,說明越有經驗,吃一塹長一智,下次不要再犯就好了。
以上內容分享給大家,歡迎大家指導和建議~~
CentOS到Alinux作業系統遷移
2020年12月08日,CentOS官方宣佈了停止維護CentOS Linux的計劃,作業系統遷移解決方案為企業提供ECS例項執行的作業系統EOL(生命週期結束)後的替換或升級服務。   
點選閱讀原文檢視詳情。

相關文章