線上服務(wù) CPU 100% ?一鍵定位 so easy!
背景
經(jīng)常做后端服務(wù)開(kāi)發(fā)的同學(xué),或多或少都遇到過(guò) CPU 負(fù)載特別高的問(wèn)題。
尤其是在周末或大半夜,突然群里有人反饋線上機(jī)器負(fù)載特別高,不熟悉定位流程和思路的同學(xué)可能登上服務(wù)器一通手忙腳亂,定位過(guò)程百轉(zhuǎn)千回。

對(duì)此,也有不少同學(xué)曾經(jīng)整理過(guò)相關(guān)流程或方法論,類似把大象放進(jìn)冰箱要幾步
傳統(tǒng)的方案一般是4步:
1. top oder by with P:1040 // 首先按進(jìn)程負(fù)載排序找到 axLoad(pid)
2. top -Hp 進(jìn)程PID:1073 // 找到相關(guān)負(fù)載 線程PID
3. printf “0x%x\\n”線程PID: 0x431 // 將線程PID轉(zhuǎn)換為 16進(jìn)制,為后面查找 jstack 日志做準(zhǔn)備
4. jstack 進(jìn)程PID | vim +/十六進(jìn)制線程PID - // 例如:jstack 1040|vim +/0x431 -
但是對(duì)于線上問(wèn)題定位來(lái)說(shuō),分秒必爭(zhēng),上面的 4 步還是太繁瑣耗時(shí)了,有沒(méi)有可能封裝成為一個(gè)工具,在有問(wèn)題的時(shí)候一鍵定位,秒級(jí)找到有問(wèn)題的代碼行呢?
當(dāng)然可以!
工具鏈的成熟與否不僅體現(xiàn)了一個(gè)開(kāi)發(fā)者的運(yùn)維能力,也體現(xiàn)了開(kāi)發(fā)者的效率意識(shí)。
淘寶的oldratlee 同學(xué)就將上面的流程封裝為了一個(gè)工具:
show-busy-java-threads.sh
https://github.com/oldratlee/useful-scripts
可以很方便的定位線上的這類問(wèn)題,下面我會(huì)舉兩個(gè)例子來(lái)看實(shí)際的效果。
快速安裝使用:
source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/master/test-cases/self-installer.sh)
1、java 正則表達(dá)式回溯造成 CPU 100%
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexLoad {
public static void main(String\[\] args) {
String\[\] patternMatch = {"(\[\\\w\\\s\]+)+(\[+\\\-/*\])+(\[\\\w\\\s\]+)",
"(\[\\\w\\\s\]+)+(\[+\\\-/*\])+(\[\\\w\\\s\]+)+(\[+\\\-/*\])+(\[\\\w\\\s\]+)"};
List patternList = new ArrayList();
patternList.add("Avg Volume Units product A + Volume Units product A");
patternList.add("Avg Volume Units / Volume Units product A");
patternList.add("Avg retailer On Hand / Volume Units Plan / Store Count");
patternList.add("Avg Hand Volume Units Plan Store Count");
patternList.add("1 - Avg merchant Volume Units");
patternList.add("Total retailer shipment Count");
for (String s :patternList ){
for(int i=0;i<patternmatch.length;i++){
Pattern pattern = Pattern.compile(patternMatch\[i\]);
Matcher matcher = pattern.matcher(s);
System.out.println(s);
if (matcher.matches()) {
System.out.println("Passed");
}else
System.out.println("Failed;");
}
}
}
}
編譯、運(yùn)行上述代碼之后,咱們就能觀察到服務(wù)器多了一個(gè) 100% CPU 的 java 進(jìn)程:

怎么使用呢?
show-busy-java-threads.sh
# 從 所有的 Java進(jìn)程中找出最消耗CPU的線程(缺省5個(gè)),打印出其線程棧。
show-busy-java-threads.sh -c <要顯示的線程棧數(shù)>
show-busy-java-threads.sh -c <要顯示的線程棧數(shù)> -p <指定的Java Process>
# -F選項(xiàng):執(zhí)行jstack命令時(shí)加上-F選項(xiàng)(強(qiáng)制jstack),一般情況不需要使用
show-busy-java-threads.sh -p <指定的Java Process> -F
show-busy-java-threads.sh -s <指定jstack命令的全路徑>
# 對(duì)于sudo方式的運(yùn)行,JAVA_HOME環(huán)境變量不能傳遞給root,
# 而root用戶往往沒(méi)有配置JAVA_HOME且不方便配置,
# 顯式指定jstack命令的路徑就反而顯得更方便了
show-busy-java-threads.sh -a <輸出記錄到的文件>
show-busy-java-threads.sh -t <重復(fù)執(zhí)行的次數(shù)> -i <重復(fù)執(zhí)行的間隔秒數(shù)>
# 缺省執(zhí)行一次;執(zhí)行間隔缺省是3秒
##############################
# 注意:
##############################
# 如果Java進(jìn)程的用戶 與 執(zhí)行腳本的當(dāng)前用戶 不同,則jstack不了這個(gè)Java進(jìn)程。
# 為了能切換到Java進(jìn)程的用戶,需要加sudo來(lái)執(zhí)行,即可以解決:
sudo show-busy-java-threads.sh
示例:
work@dev\_zz\_Master 10.48.186.32 23:45:50 ~/demo >
bash show-busy-java-threads.sh
\[1\] Busy(96.2%) thread(8577/0x2181) stack of java process(8576) under user(work):
"main" prio=10 tid=0x00007f0c64006800 nid=0x2181 runnable \[0x00007f0c6a64a000\]
java.lang.Thread.State: RUNNABLE
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4168)
at java.util.regex.Pattern$Loop.match(Pattern.java:4295)
...
at java.util.regex.Matcher.match(Matcher.java:1127)
at java.util.regex.Matcher.matches(Matcher.java:502)
at RegexLoad.main(RegexLoad.java:27)
\[2\] Busy(1.5%) thread(8591/0x218f) stack of java process(8576) under user(work):
"C2 CompilerThread1" daemon prio=10 tid=0x00007f0c64095800 nid=0x218f waiting on condition \[0x0000000000000000\]
java.lang.Thread.State: RUNNABLE
\[3\] Busy(0.8%) thread(8590/0x218e) stack of java process(8576) under user(work):
"C2 CompilerThread0" daemon prio=10 tid=0x00007f0c64093000 nid=0x218e waiting on condition \[0x0000000000000000\]
java.lang.Thread.State: RUNNABLE
\[4\] Busy(0.2%) thread(8593/0x2191) stack of java process(8576) under user(work):
"VM Periodic Task Thread" prio=10 tid=0x00007f0c640a2800 nid=0x2191 waiting on condition
\[5\] Busy(0.1%) thread(25159/0x6247) stack of java process(25137) under user(work):
"VM Periodic Task Thread" prio=10 tid=0x00007f13340b4000 nid=0x6247 waiting on condition
work@dev\_zz\_Master 10.48.186.32 23:46:04 ~/demo >
可以看到,一鍵直接定位異常代碼行,是不是很方便?
2、線程死鎖,程序 hang 住
import java.util.*;
public class SimpleDeadLock extends Thread {
public static Object l1 = new Object();
public static Object l2 = new Object();
private int index;
public static void main(String\[\] a) {
Thread t1 = new Thread1();
Thread t2 = new Thread2();
t1.start();
t2.start();
}
private static class Thread1 extends Thread {
public void run() {
synchronized (l1) {
System.out.println("Thread 1: Holding lock 1...");
try { Thread.sleep(10); }
catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (l2) {
System.out.println("Thread 2: Holding lock 1 & 2...");
}
}
}
}
private static class Thread2 extends Thread {
public void run() {
synchronized (l2) {
System.out.println("Thread 2: Holding lock 2...");
try { Thread.sleep(10); }
catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (l1) {
System.out.println("Thread 2: Holding lock 2 & 1...");
}
}
}
}
}
執(zhí)行之后的效果:

如何用工具定位:

一鍵定位:可以清晰的看到線程互相鎖住了對(duì)方等待的資源,導(dǎo)致死鎖,直接定位到代碼行和具體原因。
通過(guò)上面兩個(gè)例子,我想各位同學(xué)應(yīng)該對(duì)這個(gè)工具和工具能解決什么問(wèn)題有了比較深刻的了解了,遇到 CPU 100% 問(wèn)題可以從此不再慌亂。
不過(guò)更多的還是依賴大家自己去實(shí)踐,畢竟實(shí)踐出真知嘛~
3、免費(fèi)實(shí)用的腳本工具大禮包
除了正文提到的 show-busy-java-threads.sh,oldratlee 同學(xué)還整合和不少常見(jiàn)的開(kāi)發(fā)、運(yùn)維過(guò)程中涉及到的腳本工具,覺(jué)得特別有用的我簡(jiǎn)單列下:
(1)show-duplicate-java-classes
偶爾會(huì)遇到本地開(kāi)發(fā)、測(cè)試都正常,上線后卻莫名其妙的 class 異常,歷經(jīng)千辛萬(wàn)苦找到的原因竟然是 Jar 沖突!
這個(gè)工具就可以找出Java Lib(Java庫(kù),即Jar文件)或Class目錄(類目錄)中的重復(fù)類。
Java開(kāi)發(fā)的一個(gè)麻煩的問(wèn)題是Jar沖突(即多個(gè)版本的Jar),或者說(shuō)重復(fù)類。會(huì)出NoSuchMethod等的問(wèn)題,還不見(jiàn)得當(dāng)時(shí)出問(wèn)題。找出有重復(fù)類的Jar,可以防患未然。
# 查找當(dāng)前目錄下所有Jar中的重復(fù)類
show-duplicate-java-classes
# 查找多個(gè)指定目錄下所有Jar中的重復(fù)類
show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2
# 查找多個(gè)指定Class目錄下的重復(fù)類。Class目錄 通過(guò) -c 選項(xiàng)指定
show-duplicate-java-classes -c path/to/class_dir1 -c /path/to/class_dir2
# 查找指定Class目錄和指定目錄下所有Jar中的重復(fù)類的Jar
show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 -c path/to/class_dir1 -c path/to/class_dir2
例如:
# 在war模塊目錄下執(zhí)行,生成war文件
$ mvn install
...
# 解壓war文件,war文件中包含了應(yīng)用的依賴的Jar文件
$ unzip target/*.war -d target/war
...
# 檢查重復(fù)類
$ show-duplicate-java-classes -c target/war/WEB-INF/classes target/war/WEB-INF/lib
...
(2)find-in-jars
在當(dāng)前目錄下所有jar文件里,查找類或資源文件。
用法:注意,后面Pattern是grep的 擴(kuò)展正則表達(dá)式。
find-in-jars 'log4j\\.properties'
find-in-jars 'log4j\\.xml$' -d /path/to/find/directory
find-in-jars log4j\\\.xml
find-in-jars 'log4j\\.properties|log4j\\.xml'
示例:
$ ./find-in-jars 'Service.class$'
./WEB-INF/libs/spring-2.5.6.SEC03.jar!org/springframework/stereotype/Service.class
./rpc-benchmark-0.0.1-SNAPSHOT.jar!com/taobao/rpc/benchmark/service/HelloService.class
(3)housemd pid [java_home]
很早的時(shí)候,我們使用BTrace排查問(wèn)題,在感嘆BTrace的強(qiáng)大之余,也曾好幾次將線上系統(tǒng)折騰掛掉。
2012年淘寶的聚石寫了HouseMD,將常用的幾個(gè)Btrace腳本整合在一起形成一個(gè)獨(dú)立風(fēng)格的應(yīng)用,其核心代碼用的是Scala
HouseMD是基于字節(jié)碼技術(shù)的診斷工具, 因此除了Java以外, 任何最終以字節(jié)碼形式運(yùn)行于JVM之上的語(yǔ)言, HouseMD都支持對(duì)它們進(jìn)行診斷, 如Clojure(感謝@Killme2008提供了它的使用入門), scala, Groovy, JRuby, Jython, kotlin等.
使用housemd對(duì)java程序進(jìn)行運(yùn)行時(shí)跟蹤,支持的操作有:
查看加載類 跟蹤方法 查看環(huán)境變量 查看對(duì)象屬性值
詳細(xì)信息請(qǐng)參考:
https://github.com/CSUG/HouseMD/wiki/UserGuideCN
(4)jvm pid
執(zhí)行jvm debug工具,包含對(duì)java棧、堆、線程、gc等狀態(tài)的查看,支持的功能有:
========線程相關(guān)=======
1 : 查看占用cpu最高的線程情況
2 : 打印所有線程
3 : 打印線程數(shù)
4 : 按線程狀態(tài)統(tǒng)計(jì)線程數(shù)
========GC相關(guān)=======
5 : 垃圾收集統(tǒng)計(jì)(包含原因)可以指定間隔時(shí)間及執(zhí)行次數(shù),默認(rèn)1秒, 10次
6 : 顯示堆中各代的空間可以指定間隔時(shí)間及執(zhí)行次數(shù),默認(rèn)1秒,5次
7 : 垃圾收集統(tǒng)計(jì)。可以指定間隔時(shí)間及執(zhí)行次數(shù),默認(rèn)1秒, 10次
8 : 打印perm區(qū)內(nèi)存情況*會(huì)使程序暫停響應(yīng)*
9 : 查看directbuffer情況
========堆對(duì)象相關(guān)=======
10 : dump heap到文件*會(huì)使程序暫停響應(yīng)*默認(rèn)保存到\`pwd\`/dump.bin,可指定其它路徑
11 : 觸發(fā)full gc。*會(huì)使程序暫停響應(yīng)*
12 : 打印jvm heap統(tǒng)計(jì)*會(huì)使程序暫停響應(yīng)*
13 : 打印jvm heap中top20的對(duì)象。*會(huì)使程序暫停響應(yīng)*參數(shù):1:按實(shí)例數(shù)量排序,2:按內(nèi)存占用排序,默認(rèn)為1
14 : 觸發(fā)full gc后打印jvm heap中top20的對(duì)象。*會(huì)使程序暫停響應(yīng)*參數(shù):1:按實(shí)例數(shù)量排序,2:按內(nèi)存占用排序,默認(rèn)為1
15 : 輸出所有類裝載器在perm里產(chǎn)生的對(duì)象。可以指定間隔時(shí)間及執(zhí)行次數(shù)
========其它=======
16 : 打印finalzer隊(duì)列情況
17 : 顯示classloader統(tǒng)計(jì)
18 : 顯示jit編譯統(tǒng)計(jì)
19 : 死鎖檢測(cè)
20 : 等待X秒,默認(rèn)為1
q : exit
進(jìn)入jvm工具后可以輸入序號(hào)執(zhí)行對(duì)應(yīng)命令
可以一次執(zhí)行多個(gè)命令,用分號(hào)";"分隔,如:1;3;4;5;6
每個(gè)命令可以帶參數(shù),用冒號(hào)":"分隔,同一命令的參數(shù)之間用逗號(hào)分隔,如:
Enter command queue:1;5:1000,100;10:/data1/output.bin
(5)greys[@IP:PORT]
PS:目前Greys僅支持Linux/Unix/Mac上的Java6+,Windows暫時(shí)無(wú)法支持
Greys是一個(gè)JVM進(jìn)程執(zhí)行過(guò)程中的異常診斷工具,可以在不中斷程序執(zhí)行的情況下輕松完成問(wèn)題排查工作。
和HouseMD一樣,Greys-Anatomy取名同名美劇“實(shí)習(xí)醫(yī)生格蕾”,目的是向前輩致敬。代碼編寫的時(shí)候參考了BTrace和HouseMD兩個(gè)前輩的思路。
使用greys對(duì)java程序進(jìn)行運(yùn)行時(shí)跟蹤(不傳參數(shù),需要先greys -C pid,再greys)。支持的操作有:
查看加載類,方法信息 查看JVM當(dāng)前基礎(chǔ)信息 方法執(zhí)行監(jiān)控(調(diào)用量,失敗率,響應(yīng)時(shí)間等) 方法執(zhí)行數(shù)據(jù)觀測(cè)、記錄與回放(參數(shù),返回結(jié)果,異常信息等) 方法調(diào)用追蹤渲染
詳細(xì)信息請(qǐng)參考:
https://github.com/oldmanpushcart/greys-anatomy/wiki
(6)sjksjk --commands sjk --help
使用sjk對(duì)Java診斷、性能排查、優(yōu)化工具
ttop:監(jiān)控指定jvm進(jìn)程的各個(gè)線程的cpu使用情況 jps: 強(qiáng)化版 hh: jmap -histo強(qiáng)化版 gc: 實(shí)時(shí)報(bào)告垃圾回收信息
更多信息請(qǐng)參考:
https://github.com/aragozin/jvm-tools
