標籤:style blog http io ar color os sp java
本文來自本人獨立部落格,為獲得更佳閱讀體驗,請點擊 這裡
----------------------------------------------------------------------------------------------------
「推斷的前提是以事實為依據。」
這兩天碰到一個線上系統的偶爾出現突然堆記憶體暴漲,這倒不是個什麼疑難雜症, 只是過程中有些思路覺得可以借鑒參考,故總結下並寫下來。
現象
記憶體情況可以看看下面這張監控圖。
一天偶爾出現幾次,期間一般幾分鐘不等。 當這種情況出現時,我們檢查錯誤記錄檔,發現有下面兩幾種 OOM 錯誤。
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: Java heap space
與之相伴的還有個錯誤記錄檔,是訪問 redis 拋出的異常
JedisConnectionException: java.net.SocketException: Broken pipe
我們一共觀察到的現象大概就是這麼多了,觀察了兩天感覺現象發生沒什麼規律性,期間也不長,一會兒應用 又會自動就恢複正常了。
診斷
通過上面的現象,負責系統開發和維護的童鞋認為可能是網路不穩定, java.net.SocketException: Broken pipe
這個異常看起來確實是串連 redis 的長串連中斷了, 而出現這個問題的應用,正好是我們新部署在一個新的 IDC,它需要訪問在老 IDC 部署的 redis, 而在老 IDC 部署的應用則沒出現過此類現象。
雖然兩個 IDC 之間通過高寬頻光纖串連作成了區域網路,但依然比同一 IDC 內相比要慢上一些,再加上這個伴生 的應用拋出的網路異常,讓人容易判斷是網路環境的穩定性可能有區別導致應用行為的差別。 只是串連 redis 的長串連中斷和應用拋出 OOM 有什麼關聯?我咋一想沒覺得有必然聯絡。 而且負責網路監控的同事也確定兩個 IDC 之間在發生應用異常時網路很穩定,完全沒有丟包現象,頻寬也足夠。 因此將其原因推斷於網路不穩定,就顯得讓人不太能理解,難以信服。
而且在 OOM 中能自己恢複的應用就不是記憶體泄露,應該屬於記憶體溢出。 大概可能就是應用申請的記憶體短時間內超出了 JVM 堆的容量,導致拋出 OOM,從上面拋出的兩種類型的 OOM 看確實像這種情況,特別是提示 GC overhead limit exceeded
這個說明就更指向了代碼可能有問題。 只是如何找到哪段代碼有問題,這個只好先通過在 OOM 時 dump 記憶體來分析了,在應用啟動時加入下面啟動參數 來捕捉 OOM 現場。
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=mem.dump
弄到了記憶體 dump 檔案後,用 jhat 或 MAT 分析,順利找到了某個線程在當時申請了 1.6G 記憶體,再順著 線程棧找到了調用方法,一看源碼立刻明白了,代碼所在方法提供了對外的介面服務,方法參數來自外部輸入,沒有 對輸入參數作安全性判斷,而是直接根據輸入參數確定邊界建立了一個超級大的數組(2000多萬個整數),導致立刻觸發 了 OOM 並持續 FullGC 一段時間後被直接回收了,所以記憶體曲線才會像中那樣。
再想想
現象中還有個串連 redis 的網路異常,這又是怎麼回事? 再回到代碼去看,原來那個拼出來的 2000 多萬個整數元素數組,是作為訪問 redis 的命令參數(hmget
)。 到這裡,瞬間明白了是吧,這麼長的參數做過服務端網路編程的都明白,協議解析時超過一個合理長度估計就會被拒絕 被認為是惡意的用戶端,而導致服務端拒絕該用戶端,拒絕的行為一般都是關閉串連。
再去扒了下 redis 的文檔,確實找到了這樣的說明:
Query buffer hard limit Every client is also subject to a query buffer limit. This is a non-configurable hard limit that will close the connection when the client query buffer (that is the buffer we use to accumulate commands from the client) reaches 1 GB, and is actually only an extreme limit to avoid a server crash in case of client or server software bugs.
上面就是說 redis 最大能接受的命令長度是寫死在代碼裡地,就是 1 GB,超過這個自然被拒絕了。 更多關於細節參看 redis 官方文檔
總結
我覺得從這個案例中收穫了兩點感悟:
- 現象並不那麼可靠,不能頭痛醫頭腳痛醫腳。
- 先從懷疑自己的代碼開始。
第一點,應該是個常識了,醫生診斷的例子充分說明了這點。 第二點,為什麼要先從懷疑自己代碼開始呢,簡單來說就是應用的業務代碼通常是測試和驗證最不充分的代碼。 業務應用依賴的環境不論是硬體(主機、網路、交換器)的還是軟體的(作業系統、JVM、三方庫)這些通常都比業務代碼 經過更多地測試和廣泛地應用驗證,所以要先從懷疑自己開始,除非有非常明確地證據指向其他方面, 個人經驗大部分時候這都是找到問題的最短路徑。
一個 redis 異常訪問引發 oom 的案例分析