CPU飆升問題的解決實例


項目使用現象:
web項目,頁面打開特別慢,反正就是慢。平常打開頁面需要0.5s,現在需要3-5秒

進入服務器(4核8G內存)查看原因:
top發現:
但是cpu波動過大,使用平常在 10%左右,會突然飆升到100%、200%甚至300%
jmap -heap pid jvm內存已使用99.9%。jstat 發現發生了大量的fullgc.

所以即使存在OOM,也是響應很慢,不會停止提供服務

原因:
昨天領導讓放寬緩存條件,最大8M修改爲20M,所以
放寬緩存條件 --> 緩存增加 --> jvm內存耗用增加(99.9%) --> fullgc --> top時cpu飆升不穩定

 

解決:
收緊緩存條件,只能緩存8M以內的對象

插曲:收緊緩存條件,jvm內存消耗穩定在40%
再次top查看,發現cpu佔用已經穩定,但是內存佔用極不穩定,1%到40% 跳躍,奇怪,實在找不到原因,然後就問領導啥原因,領導看了一眼,罵了我一通:那是不同的線程。
因爲top的結果排序是按照cpu的佔比來排序的。

一、引子
對於互聯網公司,線上CPU飆升的問題很常見(例如某個活動開始,流量突然飆升時),按照本文的步驟排查,基本1分鐘即可搞定!特此整理排查方法一篇,供大家參考討論提高。

二、問題復現
線上系統突然運行緩慢,CPU飆升,甚至到100%,以及Full GC次數過多,接着就是各種報警:例如接口超時報警等。此時急需快速線上排查問題。

三、問題排查
不管什麼問題,既然是CPU飆升,肯定是查一下耗CPU的線程,然後看看GC。
3.1 核心排查步驟
1 獲取最耗cpu的pid
top 結果的第一行就是假設爲26906

2 pid爲26906的進程下的所有線程佔CPU的情況
top -Hp 26906  
得到 27010 //top -Hp 26906第一行的pid

3 十進制轉化爲16進制,這是因爲 線程堆棧信息展示的都是十六進制,而我們要查堆棧信息
printf %x\n 27010 // 27010從10進制轉化爲16進制,得到6982

4 查詢線程堆棧信息
jstack 26906  | grep 6982
結果:
"kafka-producer-network-thread | antispamProducer" #99 daemon prio=5 os_prio=0 tid=0x00007fa3b0862db0 nid=0x6982 runnable [0x00007fa356612000]
其中nid=0x6982就是線程號。 "kafka-producer-network-thread | antispamProducer"可以看到是kafaka線程
如果是“VM Thread”這就是虛擬機GC回收線程了

 5.執行“jstat -gcutil 進程號 統計間隔毫秒 統計次數(缺省代表一致統計)”,查看某進程GC持續變化情況,如果發現返回中FGC很大且一直增大-》確認Full GC!-》dump出內存,查找程序哪裏內存溢出了。-》可明確看到gc的原因!
如: jstat -gcutil 26906 3000 2
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00  22.69  92.78  78.57  97.16  93.47  24011  101.119    10    0.175  101.294
  0.00  22.69 100.00  78.57  97.16  93.47  24011  101.119    10    0.175  101.294


3.2 原因分析
1.內存消耗過大,導致Full GC次數過多
執行步驟1-5:

多個線程的CPU都超過了100%,通過jstack命令可以看到這些線程主要是垃圾回收線程-》上一節步驟2
通過jstat命令監控GC情況,可以看到Full GC次數非常多,並且次數在不斷增加。--》上一節步驟5
確定是Full GC,接下來找到具體原因:

生成大量的對象,導致內存溢出,此時可以通過eclipse的mat工具查看內存中有哪些對象比較多,飛機票:Eclipse Memory Analyzer(MAT),內存泄漏插件,安裝使用一條龍;
內存佔用不高,但是Full GC次數還是比較多,此時可能是代碼中手動調用 System.gc()導致GC次數過多,這可以通過添加 -XX:+DisableExplicitGC來禁用JVM對顯示GC的響應。

2.代碼中有大量消耗CPU的操作,導致CPU過高,系統運行緩慢;
執行步驟1-4:在步驟4jstack,可直接定位到代碼行。例如某些複雜算法,甚至算法BUG,無限循環遞歸等等。

3.由於鎖使用不當,導致死鎖。
執行步驟1-4: 如果有死鎖,會直接提示。關鍵字:deadlock.步驟四,會打印出業務死鎖的位置。
造成死鎖的原因:最典型的就是2個線程互相等待對方持有的鎖。

4.隨機出現大量線程訪問接口緩慢。
代碼某個位置有阻塞性的操作,導致該功能調用整體比較耗時,但出現是比較隨機的;平時消耗的CPU不多,而且佔用的內存也不高。
思路:
首先找到該接口,通過壓測工具不斷加大訪問力度,大量線程將阻塞於該阻塞點。
執行步驟1-4:
 daemon prio= os_prio= tid=0x00007fd08d0fa000 nid=0x6403 waiting on condition [0x00007000033db000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)-》期限等待
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:)
    at com.*.user.controller.UserController.detail(UserController.java:)-》業務代碼阻塞點
如上圖,找到業務代碼阻塞點,這裏線程進入了TIMED_WAITING(期限等待)狀態。關於線程狀態,不理解的飛機票:Thread類源碼剖析

5.某個線程由於某種原因而進入WAITING狀態,此時該功能整體不可用,但是無法復現;
執行步驟1-4:jstack多查詢幾次,每次間隔30秒,對比一直停留在WAITING狀態的線程。
 prio= os_prio= tid=0x00007f9de08c7000 nid=0x5603 waiting on condition [0x0000700001f89000]
java.lang.Thread.State: WAITING (parking) ->無期限等待
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:)
at com.*.SyncTask.lambda$main$(SyncTask.java:)-》業務代碼阻塞點
at com.*.SyncTask$$Lambda$/.run(Unknown Source)
at java.lang.Thread.run(Thread.java:)

參考:https://www.bbsmax.com/A/1O5E3vDnz7/

cpu頻率很高的原因

一直在fgc
程序中有大量計算的操作,且開啓了多線程
病毒與攻擊
機器散熱有問題

代碼中因邏輯漏洞導致的死循環

 

----------------------------------------------------------------------------------

添加與2020年5月18日

這兩天做一個數據彙總處理的功能,即7張表(數據量在萬-500萬之間)彙總到一張表中。採取了分頁存儲的方式,但是遇到了一些問題。

現象:

1 jmap發現堆使用率達到將近100%
2 日誌寫的異常快速,需要不定時刪除對應日誌信息
3 記錄處理剛開始快,後來越來越慢

最後結合日誌中的異常定位問題代碼:

   private void addStatsStuAnswer(String tableName, String startTime, String endTime) {
        int startNum = 0;
        List<Exercise> exerciseList = null;
        List<StatsStuAnswer> stuAnswerList = null;
        long start = 0L, end = 0L;
        int size = 0;
        // 按頁處理提交數據, 直到查詢到的結果小於PAGE_SIZE
        do {
            try {
                start = System.currentTimeMillis();
                log.info(tableName + "從第" + startNum + "條記錄開始處理");
                // 1 查詢exercise表中新增的記錄
                exerciseList = stuAnswerService.listExeByPage(tableName, startNum, PAGE_SIZE, startTime, endTime);
                if (CollectionUtils.isEmpty(exerciseList)) {
                    return;
                }
                // 2 將答題實例轉化爲統計實例,增加習題、學生、班級等具體信息
                // 假設這裏拋出了異常。。。。那麼該do while就是一個死循環
                stuAnswerList = exerciseToStuAnswer(exerciseList, tableName);
                // 3 執行冪等性的插入
                saveStuAnswer(stuAnswerList);
                end = System.currentTimeMillis();
                log.info(tableName + "從" + startNum + "條記錄開始,處理了" + exerciseList.size() + "條記錄,耗時:{}秒", (end - start) / 1000);
                startNum += PAGE_SIZE;;
            } catch (Exception e) {
                log.error(tableName + "從" + startNum + "條記錄開始時,習題數據彙總出錯,", e);
            } 
        }while(exerciseList != null  && PAGE_SIZE == exerciseList.size() );
    }

本質問題是:stuAnswerList = exerciseToStuAnswer(exerciseList, tableName);這行代碼拋出異常會導致do while的死循環

另外:現在的分頁方式爲 

"select * from ${tableName} " +
"where modify_time <![CDATA[ >= ]]> #{startTime}  " +
"and modify_time <![CDATA[ <= ]]>#{endTime} " +
"limit #{startNum}, #{pageSize}" 

雖然modify_time上面有索引,但是到了實際場景中,增加modify_time字段時,會有百萬條的記錄modify_time值相同,此時越是向後面查詢,數據的查詢會越慢。

百萬級別數據分頁的正確方法 應該是每次查出最後一條的id 然後記錄,下一次查詢大於該id的記錄1000條。

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