大家好,我是鴨血粉絲(大家會親切的喊我 「阿粉」),是一位喜歡吃鴨血粉絲的程序員,回想起之前線上出現 OOM 的場景,畢竟當時是第一次遇到這麼 緊髒 的大事,要好好記錄下來。
1 事情回顧
在某次週五,通過 Grafana 監控,發現線上環境突然出現CPU和內存飆升的情況:
但是看到網絡輸出和輸入流量都不是很高,所以網站被別人攻擊的概率不高,後來其它服務器的負荷居高不下。
阿粉先 dump 下當時的堆棧信息,保留現場,接着進行了簡單的分析,爲了穩住用戶,通知運維一臺一臺服務器進行重新啓動,讓大家繼續使用服務。
接着就開始分析和回顧事情了
2 開始分析
2.1 日誌分析
建議大家瞭解一些常用的 Linux 語法,例如 Grep
查詢命令,是日誌分析的一大利器,還能通過正則表達式查詢更多內容。
既然服務器在某個時間點出現了高負荷,於是就先去找一開始出現問題的服務器,去找耗時比較長的服務,例如我當時去找數據庫耗時的服務,由於發生 OOM 時的日誌已經被刷掉,於是我大致描述一下:
[admin@xxx xxxyyyy]$ grep '15:14:' common-dal-digest.log | grep -E '[0-9]{4,}ms'
2018-08-25 15:14:21,656 - [(xxxxMapper,getXXXListByParams,Y,1089ms)](traceId=5da451277e14418abf5eea18fd2b61bf)
很明顯,上述語句是 查詢在15:14那一分鐘內,在common-dal-digest.log文件中,耗時超過1000ms的SQL服務(查的是耗時超過10秒的服務)。
日誌中有個特殊的標誌 traceId,在請求鏈路中是唯一的,所以根據這個標誌能分析單請求的全鏈路操作,建議大家的日誌框架中也加上這種字段,讓服務可追溯和排查。
通過 traceId去查 http 保存的訪問日誌,定位在該時間點內,分發到該服務器上的用戶請求。還有根據該traceId,定位到整個調用流程所使用到的服務,發現的確十分耗時…
於是拿到了該請求具體信息,包括用戶的登錄手機號碼,因爲這個時候,其它幾臺服務器也出現了 CPU 和內存負載升高,於是根據手機號查詢了其它幾臺服務器的訪問日誌,發現同一個請求,該用戶也調用了很多次…
於是初步確認了某個耗時接口
2.2 使用 MAT 分析 dump 文件
官方介紹:
MAT
是Memory Analyzer
的簡稱,它是一款功能強大的 Java 堆內存分析器。可以用於查找內存泄露以及查看內存消耗情況。MAT 是基於Eclipse開發的,是一款免費的性能分析工具。讀者可以在 http://www.eclipse.org/mat/ 下載並使用 MAT。
在前面提到,出現問題時,順手保存了一份堆棧信息,使用工具打開後,效果圖如下所示:
整個應用的內存大小 1.6G,然後有一塊內存塊竟然佔用了 1.4G,佔比達到了 87.5%,這也太離譜了吧!
於是阿粉決定好好分析該對象的引用樹,右鍵選擇【class_reference】,查看對象列表,和觀察 GC 日誌,定位到具體的對象信息。
2.3 根本原因
通過日誌分析以及 dump 文件分析,都指向了某個文件導出接口,接着在代碼中分析該接口具體調用鏈路,發現導出的數據很多,而且老代碼進行計算的邏輯嵌套了很多 for 循環,而且是 for 循環調用數據庫,計算效率極低。
模擬了該用戶在這個接口的所調用數據量,需要查詢多個表,然後 for 循環中大概會計算個 100w+ 次,導致阻塞了其它請求,由於請求未結束,java 對象無法被 GC 回收,線上的服務器 CPU 和內存使用情況一直飆升。
3 性能優化
3.1 業務、代碼邏輯梳理
點開該段代碼的 git 提交記錄,發現是我在實習期寫的時候,我的內心是崩潰的,當時對業務不熟悉,直接循環調用了老代碼,而且也沒有測試過這麼大的數據量,所以 GG了。
然後我就開始做代碼性能優化,首先仔細梳理了一下整個業務流程,通過增加 SQL 查詢條件,減少數據庫 IO 和查詢返回的數據量,優化判斷條件,減少 for 嵌套、循環次數和計算量。
3.2 通過 VisualVM 進行對比
官方描述:
VisualVM
,能夠監控線程,內存情況,查看方法的CPU時間和內存中的對象,已被GC的對象,反向查看分配的堆棧(如100個String對象分別由哪幾個對象分配出來的).
該插件不需要另外下載,在 ${JAVA_HOME}/bin
目錄下就能找到,所以安裝了 jdk 就能使用它~
對比了新老代碼所佔用的 CPU 時間和內存狀態
優化前:
優化後:
通過上述優化之後,計算 1w 條數據量,進行導出成報表文件,在老代碼需要 48s,新代碼下降到了 8s,不過這是大數據量的情況下,實際用戶的數據沒有這麼多,所以基本上滿足了線上 99% 的用戶使用。(看到這個結果時,阿粉露出了「純潔的微笑」)
當然,由於這些數據是本地開發環境新增加的,與出現 OOM 問題的用戶數據量還有些差別,但通過優化後的代碼,已經在數據庫查詢的時候就過濾掉很多無效的數據,在 for 循環計算前也加了過濾條件,所以真正計算起來起來就降低了很多計算量!
恩,自己代碼優化好了,還要等測試爸爸們測試後纔敢上線,這次要瘋狂造數據!
4 技術總結
阿粉週末會自己做點飯🍚,喜歡看王剛老師的視頻,覺得最後出現的 「技術總結」 很棒,讓我能夠快速記住重要步驟
(但學習跟做飯一樣,還是得經過反覆看和實踐才能好好記住,哼兒哈兒,大夥知道阿粉想說的是啥了吧哈哈哈)
4.1 開發注意點
在開發初期,阿粉沒有考慮到性能問題,想着滿足需求就完成任務,但數據量一大起來,就有可能出現這些 OOM 問題,所以以後開發時,需要先提前考慮以下幾點:
- 梳理設計流程
- 考慮是否有性能問題
- 與產品經理商量控制查詢條件,減少查詢的範圍
- 與數據庫交互時,減少無效的查詢,合併查詢和合並更新操作
- 減少 for 循環,尤其注意多層 for 循環嵌套問題!
- 調用老代碼要注意=-=
4.2 應對故障注意點
出現問題時也不要驚慌,好好去解決它纔是王道,「在解決問題中學到更多技術」
可以參考以下步驟:
- 保留現場,dump 堆棧信息
- 進行限流,對出現問題的接口進行降級
- 如沒有降級措施,服務器本身無法快速恢復正常,聯繫運維重啓進行恢復的同時,記錄下有可能出現問題的數據,等待後面修復
- 日誌、堆棧信息分析
- 定位問題並解決
5 多說兩句
在定位到問題時,看到是阿粉寫的,原本以爲會受到批評,但領導並沒有責怪我,還叫阿粉之後好好改,避免下次出現這種問題,心情很開朗,希望各位小夥伴也能遇到這種開明的領導。(肯定不是阿粉的顏值高)
在這次問題排查過程中,熟悉了問題排查步驟,鞏固了 jdk 工具的使用方法和流程,也加深了對業務的理解程度,果然 「遇到問題能夠快速成長」
同時解決 BUG 後,阿粉的內心更開心了,下班後點了一份小碗鴨血粉絲。
各位小夥伴看完覺得有趣或者有用,來個點贊和關注,讓阿粉能升級吃大碗哈哈哈~