記一次線上 OOM 和性能優化

大家好,我是鴨血粉絲(大家會親切的喊我 「阿粉」),是一位喜歡吃鴨血粉絲的程序員,回想起之前線上出現 OOM 的場景,畢竟當時是第一次遇到這麼 緊髒 的大事,要好好記錄下來。

1 事情回顧

在某次週五,通過 Grafana 監控,發現線上環境突然出現CPU和內存飆升的情況:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zFcBaEhi-1577887891603)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/grafana.jpg)]

但是看到網絡輸出和輸入流量都不是很高,所以網站被別人攻擊的概率不高,後來其它服務器的負荷居高不下。

阿粉先 dump 下當時的堆棧信息,保留現場,接着進行了簡單的分析,爲了穩住用戶,通知運維一臺一臺服務器進行重新啓動,讓大家繼續使用服務。

接着就開始分析和回顧事情了

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jqs8luV5-1577887891605)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/%E4%B8%BA%E4%BB%80%E4%B9%88.jpg)]

2 開始分析

2.1 日誌分析

建議大家瞭解一些常用的 Linux 語法,例如 Grep 查詢命令,是日誌分析的一大利器,還能通過正則表達式查詢更多內容。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-sEKPFMLP-1577887891605)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/grep.png)]

既然服務器在某個時間點出現了高負荷,於是就先去找一開始出現問題的服務器,去找耗時比較長的服務,例如我當時去找數據庫耗時的服務,由於發生 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 文件

官方介紹:

MATMemory Analyzer 的簡稱,它是一款功能強大的 Java 堆內存分析器。可以用於查找內存泄露以及查看內存消耗情況。MAT 是基於Eclipse開發的,是一款免費的性能分析工具。讀者可以在 http://www.eclipse.org/mat/ 下載並使用 MAT。

在前面提到,出現問題時,順手保存了一份堆棧信息,使用工具打開後,效果圖如下所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ISvXZRN8-1577887891606)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/MAT.jpg)]

整個應用的內存大小 1.6G,然後有一塊內存塊竟然佔用了 1.4G,佔比達到了 87.5%,這也太離譜了吧!

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-rl5YdNn0-1577887891607)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/%E8%BF%99%E5%88%B0%E5%BA%95%E6%98%AF%E4%B8%BA%E4%BB%80%E4%B9%88.jpg)]

於是阿粉決定好好分析該對象的引用樹,右鍵選擇【class_reference】,查看對象列表,和觀察 GC 日誌,定位到具體的對象信息。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mCQDFGFN-1577887891607)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/class_reference.jpg)]

2.3 根本原因

通過日誌分析以及 dump 文件分析,都指向了某個文件導出接口,接着在代碼中分析該接口具體調用鏈路,發現導出的數據很多,而且老代碼進行計算的邏輯嵌套了很多 for 循環,而且是 for 循環調用數據庫,計算效率極低。

模擬了該用戶在這個接口的所調用數據量,需要查詢多個表,然後 for 循環中大概會計算個 100w+ 次,導致阻塞了其它請求,由於請求未結束,java 對象無法被 GC 回收,線上的服務器 CPU 和內存使用情況一直飆升。

3 性能優化

3.1 業務、代碼邏輯梳理

點開該段代碼的 git 提交記錄,發現是我在實習期寫的時候,我的內心是崩潰的,當時對業務不熟悉,直接循環調用了老代碼,而且也沒有測試過這麼大的數據量,所以 GG了。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UsXSAFzd-1577887891608)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/%E5%A7%94%E5%B1%88.jpg)]

然後我就開始做代碼性能優化,首先仔細梳理了一下整個業務流程,通過增加 SQL 查詢條件,減少數據庫 IO 和查詢返回的數據量,優化判斷條件,減少 for 嵌套、循環次數和計算量。

3.2 通過 VisualVM 進行對比

官方描述:

VisualVM,能夠監控線程,內存情況,查看方法的CPU時間和內存中的對象,已被GC的對象,反向查看分配的堆棧(如100個String對象分別由哪幾個對象分配出來的).

該插件不需要另外下載,在 ${JAVA_HOME}/bin 目錄下就能找到,所以安裝了 jdk 就能使用它~

對比了新老代碼所佔用的 CPU 時間和內存狀態

優化前:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-U3bV2LSJ-1577887891608)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/optimize_before.jpg)]

優化後:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vqyzuxvB-1577887891609)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/optimize_after.jpg)]

通過上述優化之後,計算 1w 條數據量,進行導出成報表文件,在老代碼需要 48s,新代碼下降到了 8s,不過這是大數據量的情況下,實際用戶的數據沒有這麼多,所以基本上滿足了線上 99% 的用戶使用。(看到這個結果時,阿粉露出了「純潔的微笑」)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XF57waPy-1577887891609)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/%E7%BA%AF%E6%B4%81%E7%9A%84%E5%BE%AE%E7%AC%91.gif)]

當然,由於這些數據是本地開發環境新增加的,與出現 OOM 問題的用戶數據量還有些差別,但通過優化後的代碼,已經在數據庫查詢的時候就過濾掉很多無效的數據,在 for 循環計算前也加了過濾條件,所以真正計算起來起來就降低了很多計算量!

恩,自己代碼優化好了,還要等測試爸爸們測試後纔敢上線,這次要瘋狂造數據!

4 技術總結

阿粉週末會自己做點飯🍚,喜歡看王剛老師的視頻,覺得最後出現的 「技術總結」 很棒,讓我能夠快速記住重要步驟

但學習跟做飯一樣,還是得經過反覆看和實踐才能好好記住,哼兒哈兒,大夥知道阿粉想說的是啥了吧哈哈哈)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cvX5c28x-1577887891613)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/%E6%88%91%E6%B7%B1%E6%9C%89%E5%90%8C%E6%84%9F.jpg)]

4.1 開發注意點

在開發初期,阿粉沒有考慮到性能問題,想着滿足需求就完成任務,但數據量一大起來,就有可能出現這些 OOM 問題,所以以後開發時,需要先提前考慮以下幾點:

  1. 梳理設計流程
  2. 考慮是否有性能問題
  3. 與產品經理商量控制查詢條件,減少查詢的範圍
  4. 與數據庫交互時,減少無效的查詢,合併查詢和合並更新操作
  5. 減少 for 循環,尤其注意多層 for 循環嵌套問題!
  6. 調用老代碼要注意=-=

4.2 應對故障注意點

出現問題時也不要驚慌,好好去解決它纔是王道,「在解決問題中學到更多技術」

可以參考以下步驟:

  1. 保留現場,dump 堆棧信息
  2. 進行限流,對出現問題的接口進行降級
  3. 如沒有降級措施,服務器本身無法快速恢復正常,聯繫運維重啓進行恢復的同時,記錄下有可能出現問題的數據,等待後面修復
  4. 日誌、堆棧信息分析
  5. 定位問題並解決

5 多說兩句

在定位到問題時,看到是阿粉寫的,原本以爲會受到批評,但領導並沒有責怪我,還叫阿粉之後好好改,避免下次出現這種問題,心情很開朗,希望各位小夥伴也能遇到這種開明的領導。(肯定不是阿粉的顏值高)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YlkufsM8-1577887891613)(http://www.justdojava.com/assets/images/2019/java/image_yjq/oom/%E6%88%91%E7%9C%9F%E5%A5%BD%E7%9C%8B.jpg)]

在這次問題排查過程中,熟悉了問題排查步驟,鞏固了 jdk 工具的使用方法和流程,也加深了對業務的理解程度,果然 「遇到問題能夠快速成長」

同時解決 BUG 後,阿粉的內心更開心了,下班後點了一份小碗鴨血粉絲。

各位小夥伴看完覺得有趣或者有用,來個點贊和關注,讓阿粉能升級吃大碗哈哈哈~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章