CPU百分百?別慌,教你迅速排查的三種姿勢!

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
    }
}

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 姚毛毛!”,說明調用成功。

Part3 實戰分析:原生工具

實際上,很多排查工具的本質都是在原生工具上做的擴展和封裝。理解了原生工具的用法,對於更多強大的工具爲什麼能做到那樣的效果便也會心中有數了。

也有很多場景中,我們所運維的服務器是在內網環境,需要經過層層堡壘機、跳板機,此時安裝額外的排查工具較爲困難與耗時,使用原生的工具與方法則是較爲合適的選擇。

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”。

2、找到最耗CPU的線程

 命令:top -H -p 【PID】,顯示一個進程的線程運行信息列表。實例:top -Hp 17376 ,如下圖所示,可以看到多個高耗CPU使用率的線程。

3、轉換線程PID爲16進制

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

4、查看堆棧,定位線程

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

看上圖中的“GC task thread#0 (ParallelGC)”,代表垃圾回收線程,該線程會負責進行垃圾回收。爲什麼會有兩個線程一直在進行垃圾回收,並且佔用那麼高的CPU使用率呢?

5、存儲堆棧,批量查看

第4步也可以換個方法查看,可以先將jstack堆棧信息存儲起來。 命令:jstack 【進程PID】> 【文件】 實例:jstack 17376 > yao.dump,存儲17376進程的堆棧信息。再使用cat + grep查找看看後面幾個高CPU線程的堆棧信息。實例:cat -n yao.dump | grep -A10 '0x4404',如下圖所示:

可以看到線程0x4404【線程17426】產生堆棧信息,直指方法whileTrue。

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(阿爾薩斯)

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安裝完成後重新啓動。

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

3、監控查看

已經進入Arthas操作界面,輸入dashboard,回車後將看到線程及堆棧信息,如圖11.13所示,arthas已經將cpu高使用率的線程給安排上了。

當然,Arthas的dashboard顯示了非常豐富的資源監控信息,不只是線程運行信息,還有堆棧使用、GC等信息。

4、thread【ID】查看線程

ctrl + c 退出dashboard界面,輸入thread 32查看線程信息,如下圖所示:

可以看到是TestWhile類中的whileTrue方法中的put方法導致cpu使用率升高。

5、jad反編譯

使用Arthas自帶的反編譯方法jad,輸入命令:jad com.yao.service.TestWhile 可以反編譯java的class查看問題函數的具體代碼,如下圖所示:

6、退出arthas

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

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繁忙線程查找工具)

1、方法1——快速下載 & 安裝

# 快速安裝
    source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/master/test-cases/self-installer.sh)

2、方法2——下載後賦權

    # 下載到當前目錄下
    wget --no-check-certificate https://raw.github.com/oldratlee/useful-scripts/release/show-busy-java-threads
# 賦權
    chmod +x show-busy-java-threads

3、命令執行定位 以上兩種方法都可以下載安裝,安裝完成後,就可以直接執行了。show-busy-java-threads 如下圖所示,找到了CPU使用率前5高的線程,查找非常迅速。

從前兩個線程可以看出,與使用原生工具(jstack)看到的一樣,都是頻繁gc導致的高cpu使用率。而這gc線程出現的主要原因,則是後面幾個高CPU線程中的方法導致的。與上面兩類工具一樣,既然已經定位到問題方法,那就修改下程序吧。

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、注意事項

如果報沒有權限(可能是無權限執行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包鏈接地址。


推薦閱讀

公衆號@陳樹義,用最簡單的語言,分享我的技術見解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章