
本文總結了一些常見的線上應急現象和對應排查步驟和工具。分享的主要目的是想讓對線上問題接觸少的同學有個預先認知,免得在遇到實際問題時手忙腳亂。畢竟作者自己也是從手忙腳亂時走過來的。
只不過這里先提示一下。在線上應急過程中要記住,只有一個總體目標:盡快恢復服務,消除影響。不管處于應急的哪個階段,我們首先必須想到的是恢復問題,恢復問題不一定能夠定位問題,也不一定有完美的解決方案,也許是通過經驗判斷,也許是預設開關等,但都可能讓我們達到快速恢復的目的,然后保留部分現場,再去定位問題、解決問題和復盤。
在大多數情況下,我們都是先優先恢復服務,保留下當時的異常信息(內存dump、線程dump、gc log等等,在緊急情況下甚至可以不用保留,等到事后去復現),等到服務正常,再去復盤問題。

常見現象:CPU 利用率高/飆升
場景預設:
監控系統突然告警,提示服務器負載異常。
預先說明:
CPU飆升只是一種現象,其中具體的問題可能有很多種,這里只是借這個現象切入。
注:CPU使用率是衡量系統繁忙程度的重要指標。但是CPU使用率的安全閾值是相對的,取決于你的系統的IO密集型還是計算密集型。一般計算密集型應用CPU使用率偏高load偏低,IO密集型相反。
常見原因:
- 頻繁 gc
- 死循環、線程阻塞、io wait...etc
模擬
這里為了演示,用一個最簡單的死循環來模擬CPU飆升的場景,下面是模擬代碼,
在一個最簡單的SpringBoot Web 項目中增加CpuReaper這個類,
@Componentpublic?class?CpuReaper?{????@PostConstruct????public?void?cpuReaper()?{????????int?num?=?0;????????long?start?=?System.currentTimeMillis()?/?1000;????????while?(true)?{????????????num?=?num?+?1;????????????if?(num?==?Integer.MAX_VALUE)?{????????????????System.out.println("reset");????????????????num?=?0;????????????}????????????if?((System.currentTimeMillis()?/?1000)?-?start?>?1000)?{????????????????return;????????????}????????}????}}
打包成jar之后,在服務器上運行。java -jar cpu-reaper.jar
(1)第一步:定位出問題的線程
方法 a: 傳統的方法
1、top 定位CPU 最高的進程執行top命令,查看所有進程占系統CPU的排序,定位是哪個進程搞的鬼。在本例中就是咱們的java進程。PID那一列就是進程號。

2、top -Hp pid 定位使用 CPU 最高的線程

3、printf '0x%x' tid 線程 id 轉化 16 進制
>?printf?'0x%x'?12817>?0x3211
4、jstack pid | grep tid 找到線程堆棧
>?jstack?12816?|?grep?0x3211?-A?30

方法 b: show-busy-java-threads
這個腳本來自于github上一個開源項目,項目提供了很多有用的腳本,show-busy-java-threads就是其中的一個。使用這個腳本,可以直接簡化方法A中的繁瑣步驟。如下,
>?wget?--no-check-certificate?https://raw.github.com/oldratlee/useful-scripts/release-2.x/bin/show-busy-java-threads>?chmod?+x?show-busy-java-threads>?./show-busy-java-threads
show-busy-java-threads#?從所有運行的Java進程中找出最消耗CPU的線程(缺省5個),打印出其線程棧#?缺省會自動從所有的Java進程中找出最消耗CPU的線程,這樣用更方便#?當然你可以手動指定要分析的Java進程Id,以保證只會顯示你關心的那個Java進程的信息show-busy-java-threads?-p?show-busy-java-threads?-c?
方法 c: arthas thread
阿里開源的arthas現在已經幾乎包攬了我們線上排查問題的工作,提供了一個很完整的工具集。在這個場景中,也只需要一個thread -n命令即可。
>?curl?-O?https://arthas.gitee.io/arthas-boot.jar?#?下載
后續
通過第一步,找出有問題的代碼之后,觀察到線程棧之后。我們就要根據具體問題來具體分析。這里舉幾個例子。
1、情況一:發現使用CPU最高的都是GC 線程。
GC?task?thread#0?(ParallelGC)"?os_prio=0?tid=0x00007fd99001f800?nid=0x779?runnableGC?task?thread#1?(ParallelGC)"?os_prio=0?tid=0x00007fd990021800?nid=0x77a?runnable?GC?task?thread#2?(ParallelGC)"?os_prio=0?tid=0x00007fd990023000?nid=0x77b?runnable?GC?task?thread#3?(ParallelGC)"?os_prio=0?tid=0x00007fd990025000?nid=0x77c?runnabl
2、情況二:發現使用CPU最高的是業務線程
- io wait
- 比如此例中,就是因為磁盤空間不夠導致的io阻塞
- 等待內核態鎖,如 synchronized
- jstack -l pid | grep BLOCKED 查看阻塞態線程堆棧
- dump 線程棧,分析線程持鎖情況。
- arthas提供了thread -b,可以找出當前阻塞其他線程的線程。針對 synchronized 情況
常見現象:頻繁 GC
1、 回顧GC流程
在了解下面內容之前,請先花點時間回顧一下GC的整個流程。

接前面的內容,這個情況下,我們自然而然想到去查看gc 的具體情況。
- 方法a : 查看gc 日志
- 方法b : jstat -gcutil 進程號 統計間隔毫秒 統計次數(缺省代表一致統計
- 方法c : 如果所在公司有對應用進行監控的組件當然更方便(比如Prometheus + Grafana)
這里對開啟 gc log 進行補充說明。一個常常被討論的問題(慣性思維)是在生產環境中GC日志是否應該開啟。因為它所產生的開銷通常都非常有限,因此我的答案是需要開啟。但并不一定在啟動JVM時就必須指定GC日志參數。
HotSpot JVM有一類特別的參數叫做可管理的參數。對于這些參數,可以在運行時修改他們的值。我們這里所討論的所有參數以及以“PrintGC”開頭的參數都是可管理的參數。這樣在任何時候我們都可以開啟或是關閉GC日志。比如我們可以使用JDK自帶的jinfo工具來設置這些參數,或者是通過JMX客戶端調用HotSpotDiagnostic MXBean的setVMOption方法來設置這些參數。
這里再次大贊arthas??,它提供的vmoption命令可以直接查看,更新VM診斷相關的參數。
獲取到gc日志之后,可以上傳到GC easy幫助分析,得到可視化的圖表分析結果。

2、GC 原因及定位
prommotion failed
從S區晉升的對象在老年代也放不下導致 FullGC(fgc 回收無效則拋 OOM)。
可能原因:
- survivor 區太小,對象過早進入老年代查看 SurvivorRatio 參數
- 大對象分配,沒有足夠的內存dump 堆,profiler/MAT 分析對象占用情況
- old 區存在大量對象dump 堆,profiler/MAT 分析對象占用情況
你也可以從full GC 的效果來推斷問題,正常情況下,一次full GC應該會回收大量內存,所以 正常的堆內存曲線應該是呈鋸齒形。如果你發現full gc 之后堆內存幾乎沒有下降,那么可以推斷:**堆中有大量不能回收的對象且在不停膨脹,使堆的使用占比超過full GC的觸發閾值,但又回收不掉,導致full GC一直執行。換句話來說,可能是內存泄露了。
一般來說,GC相關的異常推斷都需要涉及到內存分析,使用jmap之類的工具dump出內存快照(或者 Arthas的heapdump)命令,然后使用MAT、JProfiler、JVisualVM等可視化內存分析工具。
至于內存分析之后的步驟,就需要小伙伴們根據具體問題具體分析啦。
六、涉及工具
再說下一些工具。
- Arthas
- useful-scripts
- GC easy
- Smart Java thread dump analyzer - thread dump analysis in seconds
- PerfMa - Java虛擬機參數/線程dump/內存dump分析
- Linux 命令
- Java N 板斧
- MAT、JProfiler...等可視化內存分析工具