Part0 遇到了故障怎么辦?
在生產上,我們會遇到各種各樣的故障,遇到了故障怎么辦?
不要慌,只有冷靜才是解決故障的利器。
下面以一個例子為例,在生產中碰到了CPU 100%的問題怎么辦?
在生產中真的碰到了CPU 100%的問題,再來看這篇文章已經遲了,還是先來模擬演練下吧。
怎么模擬演練?
(1)查找資料,選型排查CPU高負載問題的工具。
(2)安裝一個高負載程序或手寫個高負載應用部署。
(3)安裝、執行分析工具,實戰分析,找出故障原因。
(4)思考與總結。
Part1 工具選型
因為現在大部分的企業應用都是java編寫的,所以我們本次排查的高負載應用也是針對java的,但是思路其實是相同的,如果也有php、python、go等語言寫的程序,無非就是換個工具而已,排查的步驟都是類似的。
而top這個命令一定是Linux上不可動搖的資源監控工具。
以下三類工具從原生的top、jstack到功能強大的Arthas和一鍵式查找的show-busy-java-threads,它們都各有長處。在合適的環境選擇合適的工具才是考驗一個IT人員能力的時候。
運用之道,存乎一心。
1.1 原生方法
此方法無需額外安裝工具,在沒法連接互聯網的情況下使用此方法排查效果較好。
top、printf都是Linux原生命令,jstack、jstat是jdk自帶命令工具。
很多功能強大的Linux和java診斷工具也是以top、jstack、jstat為基礎命令做的封裝。
注意:jstack、jstat等命令需要jdk完整安裝,linux自帶的openJdk一般無此工具,可以在java的bin目錄下查看是否有這些命令。
oracle jdk 1.8下載地址:
https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html。
1.2 阿里開源:Arthas(阿爾薩斯)
Arthas(阿爾薩斯)是 阿里巴巴開源出來的一個針對 java 的線上診斷工具,功能非常強大。
Arthas的githup官網https://github.com/alibaba/arthas。
Arthas 支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同時提供豐富的 Tab 自動補全功能,進一步方便進行問題的定位和診斷。
1.3 淘寶開源:show-busy-java-threads
show-busy-java-threads.sh,其作者是淘寶同學【李鼎(哲良) oldratlee】,這個工具是useful-scripts工具集的其中一個工具。
useful-scripts的github網址:https://github.com/oldratlee/useful-scripts。
show-busy-java-threads用於快速排查Java的CPU性能問題(top us值過高),自動查出運行的Java進程中消耗CPU多的線程,並打印出其線程棧,從而確定導致性能問題的方法調用。
注意:此工具的核心還是使用jdk的jstack方法,只是在其上做了封裝展示。
Part2 高負載代碼創建
查看CPU負載的工具選好了,現在我們需要弄個程序來讓CPU達到高負載運行。
以java代碼為示例,寫一個死循環程序,基本就會導致CPU使用率百分百。
2.1 新建springboot項目
開始動手,新建springboot的maven項目,創建web服務,引入SpringBoot內置web容器,pom.xml關鍵引用jar包如下:
<!-- 引入容器類 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.2 創建service:TestWhile
創建service類TestWhile,編寫死循環代碼。
package com.yao.service;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author 姚毛毛
* @version V1.0
* @Package com.yao.yaojiaxiaoyuan
* @Description: 死循環demo
* @date 2019/11/19--16:55
*/
@Service
public class TestWhile {
/* 操作內存對象 */
ConcurrentHashMap map = new ConcurrentHashMap();
/**
* 死循環,生產中千萬不要這么寫,while(true)時一定要有退出條件
* @param threadName 指定線程名
*/
private void whileTrue(String threadName) {
// 不設置退出條件,死循環
while (true) {
// 在死循環中不斷的對map執行put操作,導致內存gc
for( int i = 0; i <= 100000; i ++) {
map.put(Thread.currentThread().getName() + i, i);
} // end for
}// end while
}
/**
* 循環size,新建線程,調用whileTrue
* @param size 線程數
*/
public void testWhile(int size) {
// 循環size,創建多線程,並發執行死循環
for (int i = 0; i < size; i++) {
int finalI = i;
// 新建並啟動線程,調用whileTrue方法
new Thread(() -> {
whileTrue("姚毛毛-" + finalI);
}).start();
}// end for
}// end testWhile
}
2.3 創建Controller:TestWhile
創建rest服務,編寫get方法testWhile,調用死循環服務testWhile。
package com.yao.controller;
import com.yao.service.TestWhile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
/* 注入服務TestWhile */
@Autowired
TestWhile testWhile;
/**
* testWhile循環size生產線程,調用whileTrue方法
* size有多少,則意味着調用了whileTrue多少次,產生了多少個死循環
* @param size 產生線程任務
* @return 調度成功,返回信息
*/
@RequestMapping("/testWhile")
public String testWhile(@RequestParam int size) {
testWhile.testWhile(size);
return "Hello I'm 姚毛毛!";
}
}
2.4 打包項目,上傳測試服務器
application.properties配置如下:
# 設置應用端口
server.port=9999
# 應用訪問根目錄
server.servlet.context-path=/api
打包我們可以選擇idea或maven原生工具。
(1)利用idea開發工具,打開右側的maven project,使用package打包項目,如圖所示:

(2)使用maven命令,打開項目根目錄,在windows的cmd命令窗口中中輸入命令如下:
maven clean package
打包項目為:spring-boot-hello-1.0.jar。
上傳服務器,路徑:/usr/local/games。
2.5 登錄服務器,運行項目
# 訪問上傳路徑
cd /usr/local/games
# 后台運行jar包
java -jar spring-boot-hello-1.0.jar &
注意:請在自用的測試服務器或虛擬機上使用,千萬不要在生產機器上運行此項目。
2.6 打開瀏覽器,訪問死循環方法
打開瀏覽器,地址欄輸入
http://【IP】:9999/api/testWhile?size=20
返回“Hello I'm 姚毛毛!”,說明調用成功。
Part 3 實戰分析:原生工具
實際上,很多排查工具的本質都是在原生工具上做的擴展和封裝。理解了原生工具的用法,對於更多強大的工具為什么能做到那樣的效果便也會心中有數了。
也有很多場景中,我們所運維的服務器是在內網環境,需要經過層層堡壘機、跳板機,此時安裝額外的排查工具較為困難與耗時,使用原生的工具與方法則是較為合適的選擇。
3.1 找到最耗CPU的進程
命令:top –c,顯示進程運行信息列表。
實例:top -c。
交互1:按1,數字1,顯示多核CPU信息。
交互2:鍵入P (大寫p),進程按照CPU使用率排序。
如下圖所示結果,已經在交互過程中按了數字1及大寫P。

可以看到紅框標處,測試機器的雙核CPU使用率都已經快達到100%。
而第一個進程PID是17376的就是我們要找的罪魁禍首了;可以看到進程最后一列,COMMAND注釋的進程名:“java -jar spring-boot-hello-1.0.jar”。
3.2 找到最耗CPU的線程
命令:top -H -p 【PID】,顯示一個進程的線程運行信息列表。
實例:top -Hp 17376 ,如下圖所示,可以看到多個高耗CPU使用率的線程。

3.3 轉換線程PID為16進制
命令:printf “%x\n” 【線程pid】,轉換多個線程數字為十六進制,第4步使用時前面加0x。
實例:printf '%x\n' 17378 17379 17412 17426,得到結果43e2、43e3、4404、4412;如下圖所示:

3.4 查看堆棧,定位線程
命令:jstack 【進程PID】| grep 【線程轉換后十六進制】-A10 , 使用jstack獲取進程PID堆棧,利用grep定位線程id,打印后續10行信息。
實例:jstack 17376 | grep '0x43e2' -A10 ,如下圖所示:

看上圖中的“GC task thread#0 (ParallelGC)”,代表垃圾回收線程,該線程會負責進行垃圾回收。
為什么會有兩個線程一直在進行垃圾回收,並且占用那么高的CPU使用率呢?
3.5 存儲堆棧,批量查看
第4步也可以換個方法查看,可以先將jstack堆棧信息存儲起來。
命令:jstack 【進程PID】> 【文件】
實例:jstack 17376 > yao.dump,存儲17376進程的堆棧信息。
再使用cat + grep查找看看后面幾個高CPU線程的堆棧信息。
實例:cat -n yao.dump | grep -A10 '0x4404',如下圖所示:

可以看到線程0x4404【線程17426】產生堆棧信息,直指方法whileTrue。
3.6 GC查看
在第3步時我們看到CPU占用率最高的並不是0x4404,而是0x43e2、0x43e3。但是並沒法看到其中是什么類與方法,只有一條GC信息。
是不是死循環導致了GC太頻繁,導致CPU使用率居高不下呢?
我們使用jstat看下jvm的GC信息看看。
命令:jstat -gcutil 【進程PID】【毫秒】【打印次數】
實例:jstat -gcutil 17376 2000 5,查看17376進程的GC信息,每2秒打印一次,共打印5次,如下圖所示:

可以看到Full GC的次數高達506次,Full GC的持續時間很長,平均每次Full GC耗時達到9秒(4766/506,即GCT/FGC)。
確實驗證了我們之前的想法,再返回第4或第5步查看其他幾個高CPU占用率線程,找到非GC信息的堆棧,查看具體的代碼。
Part4 實戰分析:Arthas(阿爾薩斯)
4.1 安裝
使用curl下載安裝
curl -L https://alibaba.github.io/arthas/install.sh | sh
如圖11.8所示:

#啟動
./as.sh
注意:如果報錯“Error: telnet is not installed. Try to use java -jar arthas-boot.jar”,說明telnet沒有安裝。
# 安裝telnet
yum install telnet -y
telnet安裝完成后重新啟動。
4.2 啟動
(1)啟動方法一
重新使用./as.sh啟動

如上圖,在啟動后,可以看到報錯信息:“Error: no available java process to attach”,意思是沒有活動的java進程。
啟動我們上面寫的java示例再重新看下。
輸入啟動命令:
Java -jar spring-boot-hello-1.0.jar &
Java進程啟動完成后,使用./as.sh重啟啟動Arthas。
如下圖所示,顯示了當前運行的java進程,按下1,則開始監控進程15458、jar包spring-boot-hello-1.0.jar。

關閉此java進程,我們再來一遍。
# 關閉15458 進程
Kill -9 15458
# 重新啟動java示例
Java -jar spring-boot-hello-1.0.jar &
# 啟動Arthas
./as.sh
# 按1進入java進程,此時java進程PID已經變成17376
1
進入阿爾薩斯完成,如下圖,可以看到登錄路徑已經變成了[arthas@17376]$,可以輸入dashboard,進入監控頁面了。

(2)啟動方法二
首先top -c查看哪個進程有問題,輸出結果如下圖:

再使用./as.sh 【PID】命令監控線程,實例命令如下:
# 打開Arthas,監控17376進程
./as.sh 17376
4.3 監控查看
已經進入Arthas操作界面,輸入dashboard,回車后將看到線程及堆棧信息,如圖所示,arthas已經將cpu高使用率的線程給安排上了。

當然,Arthas的dashboard顯示了非常豐富的資源監控信息,不只是線程運行信息,還有堆棧使用、GC等信息。
4.4 thread【ID】查看線程
ctrl + c 退出dashboard界面,輸入thread 32查看線程信息,如下圖所示:

可以看到是TestWhile類中的whileTrue方法中的put方法導致cpu使用率升高。
4.5 jad反編譯
使用Arthas自帶的反編譯方法jad,輸入命令:
jad com.yao.service.TestWhile
可以反編譯java的class查看問題函數的具體代碼,如下圖所示:

4.6 退出arthas
最后,既然問題已經找到,那就退出Arthas吧。輸入命令:quit,如下圖所示:

4.7 Arthas其他命令
Arthas還有些常用及好用的命令,命令如下:
help——查看命令幫助信息
cls——清空當前屏幕區域
session——查看當前會話的信息
reset——重置增強類,將被 Arthas 增強過的類全部還原,Arthas 服務端關閉時會重置所有增強過的類
version——輸出當前目標 Java 進程所加載的 Arthas 版本號
history——打印命令歷史
quit——退出當前 Arthas 客戶端,其他 Arthas 客戶端不受影響
stop——和shutdown命令一致
shutdown——關閉 Arthas 服務端,所有 Arthas 客戶端全部退出
keymap——Arthas快捷鍵列表及自定義快捷鍵
其他功能和具體使用教程,可以看這里:Arthas的進階命令(https://alibaba.github.io/arthas/advanced-use.html)。
Part5 實戰分析:show-busy-java-threads(java繁忙線程查找工具)
5.1 方法1——快速下載 & 安裝
# 快速安裝
source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/master/test-cases/self-installer.sh)
5.2 方法2——下載后賦權
# 下載到當前目錄下
wget --no-check-certificate https://raw.github.com/oldratlee/useful-scripts/release/show-busy-java-threads
# 賦權
chmod +x show-busy-java-threads
5.3 命令執行定位
以上兩種方法都可以下載安裝,安裝完成后,就可以直接執行了。
show-busy-java-threads
如下圖所示,找到了CPU使用率前5高的線程,查找非常迅速。

從前兩個線程可以看出,與使用原生工具(jstack)看到的一樣,都是頻繁gc導致的高cpu使用率。
而這gc線程出現的主要原因,則是后面幾個高CPU線程中的方法導致的。
與上面兩類工具一樣,既然已經定位到問題方法,那就修改下程序吧。
5.4 其他命令
與Arthas一樣,show-busy-java-threads也有一些其他很好用的增強命令:
show-busy-java-threads --help
查看show-busy-java-threads常用命令:
show-busy-java-threads
從所有的 Java進程中找出最消耗CPU的線程(缺省5個),打印出其線程棧。
show-busy-java-threads -c 3
-c 3:3為n,指定顯示最耗cpu使用率前3的線程。
show-busy-java-threads -c 3 -p 17376
展示進程17376耗費CPU組多的3個線程;
-p 17376 :17376為進程PID,-p參數指定進程PID。
show-busy-java-threads -s 【指定jstack命令的全路徑】
對於sudo方式的運行,JAVA_HOME環境變量不能傳遞給root,
而root用戶往往沒有配置JAVA_HOME且不方便配置,
顯式指定jstack命令的路徑就反而顯得更方便了
show-busy-java-threads -a yao.log
將輸出結果導入到指定文件yao.log中
show-busy-java-threads 3 5
每5秒執行一次,一共執行3次; 缺省執行一次,缺省間隔是3秒。
5.5 注意事項
如果報沒有權限(可能是無權限執行jstack),需要加sudo來執行:
# root權限執行 show-busy-java-threads
sudo show-busy-java-threads.sh
Part6 小結
學習完了三類工具的排查實戰后,我們現在來總結下,怎么去排查問題的?
(1)查看CPU負載過高進程。
(2)查看進程中負載高的線程。
(3)獲取進程中的堆棧信息。
(4)獲取堆棧中對應的線程信息,找到里面的問題方法。
在排查過程中我們不只使用了原生工具,還使用了增強工具Arthas與show-busy-java-threads,大大簡化了我們排查的步驟。
再仔細想一想,增強工具其實無非就是在原生工具的基礎上,將這些方法與步驟做了一些自動化處理是不是?
如果我們自己用shell腳本去寫一個自動監控程序,是不是也可以去借鑒借鑒呢?
祝大家在遇到相似問題時,可以做到手中有刀、心中有譜,穩如老狗、不忙不慌。
最后,若無法按照Part2的代碼變異出死循環jar包,可關注公眾號,留言hello,得到名為srping-boot-hello的jar包鏈接地址。
歡迎關注我的公眾號:姚毛毛的博客
這里有我的編程生涯感悟與總結,有Java、Linux、Oracle、mysql的相關技術,有工作中進行的架構設計實踐和讀書理論,有JVM、Linux、數據庫的性能調優,有……
有技術,有情懷,有溫度
歡迎關注我:姚毛毛& 妖生

