摘要:而數量過度增加的線程池使得內存資源緊張,導致JVM基於磁盤運行而搶鎖困難,搶鎖過程的拉長使得 沒有任何線程持有鎖 這個常規狀態下的瞬時狀態被拉長,JVM服務能力大打折扣,而duct平臺由於策略原因不能應對該問題的特殊情況導致其無法啓動切流,流量照常打入JVM。事後定位是指通過日誌監控等較緩慢的方式去對問題發生時刻定位,由於該問題的特殊性,日誌無法提供需要的信息以判斷故障,另外,日誌無法採集我們需要的信息,尤其是JVM內部線程和鎖的信息。

背景

近期,閒魚核心應用出現了一個比較難解決的問題。在該應用集羣中,會隨機偶現一兩個實例,其JVM運行在一個掛起的狀態,深入分析stack文件發現,此時jvm中有大量的線程在等待一把沒有任何線程持有的鎖。問題實例所在的機器負載相對正常機器要輕很多,而其線程數則大幅增多。由於該問題在邏輯上的衝突,加上週邊問題的複雜性,使得研究、分析、解決該問題變得相對來說困難與曲折。本文將系統性地介紹如何解決這個問題,並找出問題背後的原因。

問題分析

在實際解決這個問題的時候,我們發現不僅問題本身顯得不合常理,其周邊環境也相對來說不友善,給問題的分析與解決帶來了較大的困難。

  • 集羣中隨機出現。 問題隨機出現在該應用集羣中的一個或幾個實例中,無法提前預知其出現規律。

  • 單機出現時間不可預知 ,現場捕捉困難,捕捉風險大,一般發現已經爲事後,無現場第一手數據。從單個機器,或者單個實例看,則是出現概率非常低,出現時間完全隨機。這使得蹲點單臺機器以捕捉這個問題的思路幾乎行不通,策略擴大至整個集羣又可能出現穩定性及性能問題。

  • 問題出現頻率低。 出現頻率大概在一到兩天一次。

  • 問題表現複雜。 該問題的表現很複雜,不僅從第一眼看去不合常理,JVM內部出現了大量線程在等待一把沒有任何線程持有的鎖。另外,問題機器的負載非常低,基本上在5%以內,相當於空載,而JVM中線程數卻非常多,最多發現過接近4k個線程。

  • 問題周邊環境複雜。 該問題出現前後,應用先後引入了rxjava、協程,應用爲早期應用,服務結構複雜,而log4j問題又和網上大量的文章情形不符。

  • 驗證困難。 理論分析完成後,無法在線上復現及驗證,安全性、穩定性、數據等都不允許直接在線上驗證。

解決方案

解決這個問題的主要按照以下六步,一步步排除法,最終定位並解決問題。按照先易後難,先直接後理論,先數據後源碼的順序,總結出來以下六步,大體上投入時間逐步增加,難度也逐漸增加。

step1. 代碼bug查找

代碼問題指的是業務代碼本身邏輯問題把JVM帶入了某種故障狀態。問題的分析及排除很簡單,通過觀察應用日誌即可。

step2. 現場捕獲

定位了問題,問題也就解決了一半。

一般來說,定位問題主要有兩個分類,即時定位,事後定位。

前一種是指我們實時直接監控JVM信息,在關鍵信息異常時,即發生動作。配合週期性的信息採集,基本可以對問題發生時刻前後數據精準採集和對比,做法一般是採用JVM代理方式或JMS方式。JVM代理分爲C語言和Java語言代理,C語言代理運行在JVM層,可以做到即時Java代碼發生故障故障,依然可以正常採集信息。Java代理相對C語言代理來說編寫起來方便,實際上C語言部分任務還是通過JNI接口構造Java對象執行的。JMS方式可以實時採集各種指標,也是目前監控主要採用等方式。缺點是對應用的侵入性非常大,不適合解決問題用。

事後定位是指通過日誌監控等較緩慢的方式去對問題發生時刻定位,由於該問題的特殊性,日誌無法提供需要的信息以判斷故障,另外,日誌無法採集我們需要的信息,尤其是JVM內部線程和鎖的信息。

在後續現象的觀察中,發現了一個比較普遍的現象,應用由正常轉爲故障需要一個漫長的時間,應用可能在臨界區停留相當長的時間,極端例子中應用在線程數提升後依然能夠正常運行接近24小時,之後發生了自恢復。另外,在和JVM組同學對接的時候,又被告知阿里jdk對C代理支持可能由於安全原因被關閉。基本上宣佈這個問題的研究進入了下一個階段。

step3. io hang

考慮到大部分實例業務日誌打印緩慢或者根本不再打印,可能原因是io方面出了問題,通過查看容器硬件監控及應用火焰圖,可以輕鬆將IO問題排除。

step4. 鎖分析

鎖問題主要包含 死鎖丟鎖死鎖 的特點很明顯,一旦發生死鎖,則與該鎖相關的線程都將停止。首先這點和大量實例運行緩慢不符,其次,這個問題可以輕易通過 stack文件 排除。 丟鎖 主要和 協程 有關,和死鎖相似,考慮到協程可能在切換過程中發生丟鎖,造成的現象和該問題很類似,即沒有線程持有的鎖。丟鎖最主要的問題也是不可恢復,一旦丟鎖,則JVM相關線程就永遠不可恢復,和該問題不符。另外,觀察大部分stack文件發現,此時JVM中的協程數量並不多,線程池Worker實例也在變化。

step5. 資源耗盡查看

資源耗盡是指JVM運行過程中由於部分資源緊張,程序雖然可以正常運行,但是限於部分資源緊張,必須等待其他線程釋放了持有資源後,當前線程纔可以繼續運行。資源包括 軟件資源硬件資源軟件資源 是指在JVM運行過程中,有設定上限的軟件資源,如堆、Reserved Code Cache、元數據區等,在實際觀察中,發現上述資源均未出現明顯的資源耗盡情況。 硬件資源 主要分析在JVM運行過程中,所在機器硬件資源如CPU、內存、網絡等硬件資源使用情況。其中,在觀察中發現,內存資源出現了明顯的問題,由於問題機器線程數大幅增加,導致問題機器JVM總使用內存超出了機器的物理內存。加上監控進程與機器本身的進程,很容易得出一個結論,JVM此時在將部分資源扇出至page頁。實際上,JVM此時在 部分基於硬盤運行 。如果此時JVM進行一個牽涉面很廣的搶鎖任務,那麼就有可能發生悲劇。而在該問題中,應用採用了log4j作爲日誌記錄工具,查看相關源碼可以看出,log4j採用了java monitor來控制日誌打印,防止日誌結構混亂及數據破壞。而作爲流量入口日誌,所有的業務線程都會進行進行打印,因此也會進行搶鎖。

查看HotSpot源碼,在退出臨界區時,首先要做的是把鎖狀態重置,也即對象頭重置及Montior對象當前owner置NULL,然後纔會喚醒所有相關線程搶鎖。如果此時內存放不下所有有關線程,隨着線程的喚醒,活躍線程會被扇出以提供內存空間。大量的扇入和扇出使得這個過程顯得很緩慢,也就出現了一個 沒有任何線程持有的鎖 ,實際上JVM此時在進行一個艱難的搶鎖任務。

step6. 框架層源碼閱讀

在前面步驟中大致定位了一個大的方向,線程數增加導致內存不足。接下來需要深入框架層去分析 引起線程數增加的可能原因 。先後對HSF、Modulet、Mtop、netty等框架進行了源碼級別的分析,主要跟蹤各個框架 線程分配策略 。其中,HSF默認設置的線程池模型擾動抗性很低。在HSF框架中,netty線程池將任務提交到HSF Provider線程池,HSF Provider線程池採用業務隔離設計,在一次對外服務中,HSF Provider大量調用HSF Consumer,而Consumer會被提交至Consumer線程池中執行。在該應用中,Provider和Consumer線程池容量比例大於200:1。

而根據業務實際,合理的比例應該在1:1附近。

失衡的線程池結構,極容易服務發生網絡抖動、迴環調用時使Consumer線程池服務能力下降,進而使整個應用實例對外服務能力下降。而有規律的故障不應該和無規律的抖動有關。

迴環與問題出現頻率之間讀者可以通過概率論進行分析,假定100臺機器,則每次請求會有1/100的概率發生迴環,同理,每10000次請求就會發生雙迴環,1M次請求則是3迴環。在該問題中,概率論分析和實際情況是契合的。

在研究框架層的時候,發現了迴環調用對系統的危害。但是還有一個疑問需要回答, 迴環調用完成後,應用應該能恢復 ,而線上實際情況是,自恢復是個小概率事件。結合前一節可以得出一個結論,迴環調用使應用Consumer線程池處理能力下降,進而使 上游線程池水位逐漸提升直至被打滿 而數量過度增加的線程池使得內存資源緊張,導致JVM基於磁盤運行而搶鎖困難,搶鎖過程的拉長使得 沒有任何線程持有鎖 這個常規狀態下的瞬時狀態被拉長,JVM服務能力大打折扣,而duct平臺由於策略原因不能應對該問題的特殊情況導致其無法啓動切流,流量照常打入JVM。於是就形成了一個惡性循環,線程數提升導致JVM進入一種非常規狀態,服務能力下降,而流量照常,導致線程數很難下降。於是,JVM長時間運行在一個非常緩慢的狀態,從表現上來看就是 jvm掛起 。下表爲一個較有代表性的流量對比(實際上故障機狀態跨度非常大,這兩臺機器 較爲典型而已)

實驗驗證

接下來,本文采用阿里PAS壓測平臺,對預發機器進行了壓測驗證。由於線上問題複雜,無法復現線上的環境,只能對其誘因進行驗證。下表爲壓測過程中應用的性能表現。由於壓測模式限制,所支持的最大tps在超時的情況下非常低,如表所示只有80左右,考慮到壓測環境機器數量,迴環數量還要打折。

從下圖可以看出,平均RT爲2500ms左右,絕大多數請求都在超時狀態。

壓測結果表明,迴環不需要多高的流量,就能把應用實際服務能力大打折扣。考慮到線上還有其他類型的請求,填充在迴環之間,這會使線程池迅速打滿,並使得 處理迴環請求的時間加長 ,惡化應用從迴環調用中恢復的能力。

總結和思考

在JVM出現問題的時候,首先要 閱讀業務代碼 ,這個雖然看似作用不大,卻有可能以相當低廉的代價解決問題。之後,主要思路就是 捕獲現場 ,現場捕獲將極大程度上有助於問題的解決。如果該步驟不可行,或者成本相對較高,可以先去排查周邊原因。這主要包括 IO硬軟件資源 ,在執行這些排查的時候,要留意這些方面出問題的表現和實際問題的表現契合度。比較明顯的就是一旦死鎖、丟鎖,或者IO hang,則程序無法從故障狀態恢復,相關線程也不能繼續執行。這些特點可以協助排除部分大的方向。最後,對 資源耗盡 的排查,則是基於本文所述問題的一個基本特點,絕大多數JVM運行緩慢而不是停止運行。所以,資源緊張成爲一個解決問題的大方向,並最終定位了問題。深入到 框架層 主要是從理論上分析問題產生的原因,然後在結合實際情況,分析整個解決思路的正確性。讀者在遇到類似JVM問題時,可參考本文所述的方法與步驟,對實際問題進行分析與研究。

閒魚技術團隊不僅是阿里巴巴集團旗下閒置交易社區的創造者,更是移動與高併發大數據應用新技術的引導者與創新者。我們與 Google Flutter/Dart 小組密切合作,爲社區貢獻了多個高 star 的項目和大量 PR 。我們正在積極探索深度學習和視覺技術在互動、交易、社區場景的創新應用。閒魚技術與集團中間件團隊共同打造的 FaaS 平臺每天支持數以千萬級用戶的高併發訪問場景。  

就是現在! 客戶端/服務端java/架構/前端/質量 工程師 面向社會+校園招聘,base杭州阿里巴巴西溪園區,一起做有創想空間的社區產品、做深度頂級的開源項目,一起拓展技術邊界成就極致!

*投餵簡歷給小閒魚→ [email protected]

開源項目、峯會直擊、關鍵洞察、深度解讀

請認準 閒魚技術

相關文章