對于線上遇到的 “疑難雜癥”,需要通過理性的思維去分析問題、排查問題、定位問題、解決問題,同時(shí),如果解決掉所遇到的問題或瓶頸后,也可以在能力范圍之內(nèi)嘗試最優(yōu)解以及適當(dāng)考慮拓展性。
JVM線上環(huán)境常見故障與排查思路
JVM在線上環(huán)境往往會(huì)出現(xiàn)一下問題:
JVM內(nèi)存泄漏
JVM內(nèi)存溢出
業(yè)務(wù)線程死鎖
應(yīng)用程序一場宕機(jī)
線程堵塞/響應(yīng)速度變慢
CPU利用率飆升或100%
線上排查及其解決問題的思路
分析問題:根據(jù)理論知識 + 經(jīng)驗(yàn)分析問題,判斷問題可能出現(xiàn)的位置或可能引發(fā)問題的原因,將目標(biāo)縮小到一定范圍。
排查問題:基于上一步結(jié)果,從引發(fā)問題的“可疑性”角度出發(fā),從高到低依次進(jìn)行排查,進(jìn)一步排除一些選項(xiàng),將目標(biāo)范圍進(jìn)一步縮小。
定位問題:通過相關(guān)監(jiān)控?cái)?shù)據(jù)的輔助,以更細(xì)粒度的手段,將引發(fā)問題的原因定位到精確位置。
解決問題:判斷問題引發(fā)的愿意及其位置后,采取相關(guān)措施對其進(jìn)行修改。
常識最優(yōu)解:將原有的問題解決后,在能力范圍內(nèi),且環(huán)境允許的情況下,應(yīng)該適當(dāng)考慮問題的最優(yōu)解(可以從性能、拓展性、并發(fā)等角度出發(fā))。
作為 “新時(shí)代的程序構(gòu)建者”,那當(dāng)然得學(xué)會(huì)合理使用工具來幫助我們快速解決問題:
1.摘取或復(fù)制問題的關(guān)鍵片段
2.粘貼搜索
觀察返回結(jié)果中,選擇標(biāo)題與描述與自己問題較匹配的資料進(jìn)入。多看幾個(gè)后,根據(jù)其解決方案嘗試解決問題。成功解決后皆大歡喜,嘗試無果后 “找人 / 問群”。“外力” 無法解決問題時(shí)自己動(dòng)手,根據(jù)之前的步驟依次排查解決。
線上問題排查的方向
應(yīng)用程序本身導(dǎo)致的問題
程序內(nèi)部頻發(fā)觸發(fā)GC,造成系統(tǒng)出現(xiàn)長時(shí)間停頓,導(dǎo)致客戶端堆積大量請求;
JVM參數(shù)配置不合理,導(dǎo)致線上運(yùn)行失控,如堆內(nèi)存、各內(nèi)存區(qū)域太小等;
Java程序代碼存在缺陷,導(dǎo)致線上運(yùn)行出現(xiàn)Bug,如死鎖 / 內(nèi)存泄漏、溢出等;
程序內(nèi)部資源使用不合理,導(dǎo)致出現(xiàn)問題,如線程 / DB連接 / 網(wǎng)絡(luò)連接 / 堆外內(nèi)存等。
上下游內(nèi)部系統(tǒng)導(dǎo)致的問題
上游服務(wù)出現(xiàn)并發(fā)情況,導(dǎo)致當(dāng)前程序請求量急劇增加,從而引發(fā)問題拖垮系統(tǒng);
下游服務(wù)出現(xiàn)問題,導(dǎo)致當(dāng)前程序堆積大量請求拖垮系統(tǒng),如Redis宕機(jī) / DB阻塞等;
程序鎖部署的機(jī)器本身導(dǎo)致的問題
服務(wù)器機(jī)房網(wǎng)絡(luò)出現(xiàn)問題,導(dǎo)致網(wǎng)絡(luò)出現(xiàn)阻塞、當(dāng)前程序假死等故障;
服務(wù)器中因其他程序原因、硬件問題、環(huán)境因素等原因?qū)е孪到y(tǒng)不可用;
服務(wù)器因遭到入侵導(dǎo)致Java程序收到影響。
第三方RPC遠(yuǎn)程調(diào)用導(dǎo)致的問題
作為被調(diào)用者提供給第三方調(diào)用,第三方流量突增,導(dǎo)致當(dāng)前程序負(fù)載過重出現(xiàn)問題;
作為調(diào)用者調(diào)用第三方,但第三方出現(xiàn)問題,引發(fā)雪崩問題而造成當(dāng)前程序崩潰。
JVM線上事故問題合集
線上排查之前
在排查問題時(shí),誘發(fā)問題的原因也有可能來自上下游系統(tǒng)。因此,當(dāng)出現(xiàn)問題時(shí),首先得定位出現(xiàn)問題的節(jié)點(diǎn),然后針對節(jié)點(diǎn)進(jìn)行排錯(cuò)。但無論是那個(gè)節(jié)點(diǎn)(java應(yīng)用、DB、上下游等),出現(xiàn)問題的原因無非就幾個(gè)方向:代碼、CPU、磁盤、內(nèi)存以及網(wǎng)絡(luò)問題,所以遇到線上問題時(shí),合理采用OS與JVM提供的工具(df、free、top、jstack、jmap、ps等),將這些方面依次排查一遍即可。
不過需要額外注意:JVM 提供的大部分工具在使用時(shí)會(huì)影響性能,所以如果你的程序是以單機(jī)的模式部署,那最好在排查問題之前做好流量遷移(改 DNS、Nginx 配置等)。如果你的程序是以集群模式部署,那么可以將其中一臺機(jī)器隔離出來,用于保留現(xiàn)場,也為了更方便的調(diào)試問題。
同時(shí),如果線上的機(jī)器已經(jīng)無法提供正常服務(wù),那么在排查問題之前首先要做到的是 “及時(shí)止損”,可以采用版本回滾、服務(wù)降級、重啟應(yīng)用等手段讓當(dāng)前節(jié)點(diǎn)恢復(fù)正常服務(wù)。
JVM內(nèi)存溢出(OOM)
什么是內(nèi)存溢出:一個(gè)木桶只能裝40L水,多出來的水會(huì)從桶頂溢出。
在Java內(nèi)存空間中,會(huì)有多個(gè)區(qū)域會(huì)發(fā)生OOM問題,如堆空間、元空間、??臻g等。通常,線上環(huán)境產(chǎn)生內(nèi)存溢出的原因大致有三類:
JVM分配的內(nèi)存太小,不足以支撐程序的正常執(zhí)行時(shí)數(shù)據(jù)增長;
編寫的Java程序內(nèi)部存在問題、有Bug,導(dǎo)致GC回收速率跟不上分配速率;
堆空間、棧分配不足導(dǎo)致堆 / 棧溢出,一般不會(huì)出現(xiàn)這種情況;
自己的代碼或引入的第三方以來存在內(nèi)存溢出問題,導(dǎo)致可用內(nèi)存不足。
上述2、4問題皆是由編寫的Java程序代碼不嚴(yán)謹(jǐn)導(dǎo)致的OOM,由于Java內(nèi)存中產(chǎn)生大量垃圾對象,導(dǎo)致新對象沒有空閑內(nèi)存分配,從而產(chǎn)生的溢出。
在排查OOM問題時(shí),核心是:哪里OOM了?為什么OOM了?怎么避免出現(xiàn)OOM?
同時(shí),在排查過程中,應(yīng)到要建立數(shù)據(jù)的分析之上,也就是指Dump數(shù)據(jù)。
獲取Dump文件的方式有兩種:
啟動(dòng)時(shí)設(shè)置-XX:HeapDumpPath,事先指定OOM出現(xiàn)時(shí),自動(dòng)到處Dump文件。
重啟并在程序運(yùn)行一段時(shí)間后,通過工具導(dǎo)出。
java線上OOM排查
// JVM啟動(dòng)參數(shù):-Xms64M -Xmx64M -XX:+HeapDumpOnOutOfMemoryError // -XX:HeapDumpPath=/usr/local/java/java_code/java_log/Heap_OOM.hprofpublic class OOM { // 測試內(nèi)存溢出的對象類 public static class OomObject{} public static void main(String[] args){ List<OomObject> OOMlist = new ArrayList<>(); // 死循環(huán):反復(fù)往集合中添加對象實(shí)例 for(;;){ OOMlist.add(new OomObject()); } } } root@localhost ~]# java -Xms64M -Xmx64M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/java/jav [1] 9999
等待一段時(shí)間后,可以看到在/usr/local/java/java_code/java_log/目錄下,已經(jīng)自動(dòng)導(dǎo)出了堆Dump文件,接下來我們只需要把這個(gè)Dump文件直接往Eclipse MAT(Memory Analyzer Tool)工具里面一丟,然后它就能自動(dòng)幫你把 OOM 的原因分析出來,然后根據(jù)它分析的結(jié)果改善對應(yīng)的代碼即可。
線上OOM問題排查思路:
首先獲取Dump文件,最好是上線部署時(shí)配置了,這樣可以保留第一現(xiàn)場,但如若未配置對應(yīng)參數(shù),可以調(diào)小堆空間,然后重啟程序的時(shí)候重新配置參數(shù),爭取做到 “現(xiàn)場” 重現(xiàn)。
如果無法通過配置參數(shù)獲得程序 OOM 自然導(dǎo)出的Dump文件,那則可以等待程序在線上運(yùn)行一段時(shí)間,并協(xié)調(diào)測試人員對各接口進(jìn)行壓測,而后主動(dòng)式的通過jmap等工具導(dǎo)出堆的Dump文件(這種方式?jīng)]有程序自動(dòng)導(dǎo)出的Dump文件效果好)。
將Dump文件傳輸?shù)奖镜兀缓笸ㄟ^相關(guān)的Dump分析工具分析,如 JDK 自帶的jvisualvm,或第三方的MAT工具等。
根據(jù)分析結(jié)果嘗試定位問題,先定位問題發(fā)生的區(qū)域,如:確定是堆外內(nèi)存還是堆內(nèi)空間溢出,如果是堆內(nèi),是哪個(gè)數(shù)據(jù)區(qū)發(fā)生了溢出。確定了溢出的區(qū)域之后,再分析導(dǎo)致溢出的原因(后面會(huì)列出一下常見的 OOM 原因)。
根據(jù)定位到的區(qū)域以及原因,做出對應(yīng)的解決措施,如:優(yōu)化代碼、優(yōu)化 SQL 等。
線上內(nèi)存溢出問題小結(jié)
內(nèi)存溢出問題絕對是線上問題的??停ǔG闆r下,OOM大多數(shù)因?yàn)榇a問題導(dǎo)致的,在程序中容易引發(fā)OOM的情況:
一次性從外部將提及龐大的數(shù)據(jù)載入內(nèi)存,如DB讀表、讀本地文件等;
程序中使用容器(Map/List/Set等)未及時(shí)清理,內(nèi)存緊張而GC無法回收;
程序邏輯中存在死循環(huán)或大量循環(huán),或單個(gè)循環(huán)中產(chǎn)生大量重復(fù)的對象實(shí)例;
程序中引入的第三方依賴中存在Bug問題;
程序中存在內(nèi)存泄漏問題,一直在蠶食可用內(nèi)存,GC無法回收導(dǎo)致內(nèi)存溢出;
第三方以來加載大量類庫,元空間無法載入所有類元數(shù)據(jù)。
不過 Java 程序中,堆空間、元空間、??臻g等區(qū)域都可能出現(xiàn) OOM 問題,其中元空間的溢出大部分原因是由于分配空間不夠?qū)е碌?,?dāng)然,也不排除會(huì)存在 “例外的類庫” 導(dǎo)致 OOM。真正意義上的棧空間 OOM 在線上幾乎很難遇見,所以實(shí)際線上環(huán)境中,堆空間 OOM是最常見的,大部分需要排查 OOM 問題的時(shí)候,幾乎都是堆空間發(fā)生了溢出。
JVM內(nèi)存泄漏
舉例:一個(gè)木桶只能裝40L水,但此刻我往里面丟塊2KG的金磚,那該水桶在之后的過程中,最多只能裝38L的水。此時(shí)這種情況換到程序的內(nèi)存中,就被稱為內(nèi)存泄漏。
在發(fā)生內(nèi)存溢出時(shí),有可能是因?yàn)閮?nèi)存泄漏誘發(fā)的,但內(nèi)存泄漏絕對不可能因?yàn)?OOM 引發(fā)。
出現(xiàn)內(nèi)存泄漏主要情況:
堆內(nèi)泄漏:由于代碼不合理導(dǎo)致內(nèi)存中出現(xiàn)泄漏,如垃圾對象與靜態(tài)對象保持著引用、未正確關(guān)閉外部連接等。
堆外泄漏:申請bugger流后未釋放內(nèi)存、直接內(nèi)存中的數(shù)據(jù)未手動(dòng)清理等。
JVM內(nèi)存泄漏排查
// JVM啟動(dòng)參數(shù):-Xms64M -Xmx64M -XX:+HeapDumpOnOutOfMemoryError // -XX:HeapDumpPath=/usr/local/java/java_code/java_log/Heap_MemoryLeak.hprof// 如果不做限制,想要觀測到內(nèi)存泄漏導(dǎo)致OOM問題需要很長時(shí)間。public class MemoryLeak { // 長生命周期對象,靜態(tài)類型的root節(jié)點(diǎn) static List<Object> ROOT = new ArrayList<>(); public static void main(String[] args) { // 不斷創(chuàng)建新的對象,使用后不手動(dòng)將其從容器中移除 for (int i = 0;i <= 999999999;i++) { Object obj = new Object(); ROOT.add(obj); obj = i; } } } root@localhost ~]# java -Xms64M -Xmx64M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/java/jav[1] 78849
Exception in thread "main" OutOfMemoryError: Java heap space at Arrays.copyOf(Arrays.java:3210) at Arrays.copyOf(Arrays.java:3181) at ArrayList.grow(ArrayList.java:261) at ArrayList.ensureExplicitCapacity(ArrayList.java:235) at ArrayList.ensureCapacityInternal(ArrayList.java:227) at ArrayList.add(ArrayList.java:458) at MemoryLeak.main(MemoryLeak.java:14)
來源:http://zxse.cn/archives/1718025144794