線上環境應用程序消耗CPU過高問題排查

一、CPU使用率過高的原因一般是:

 1、線程空耗,如大量線程獲取鎖的過程中自旋等待;

 2、系統進行密集運算(密集數學運算的AI程序等);

 3、內存不足造成JVM頻繁fullGC;

 4、系統正在進行大量線程上下文切換消耗資源;

 線程相關問題使用jstack指令(java虛擬機自帶的指令)獲取JVM中指定進程的當前所有線程的堆棧跟蹤信息。

二、jstack介紹

1、用法: jstack [ options ] pid

2、作用

 1)jstack命令用於打印指定Java進程、核心文件或遠程調試服務器的Java線程的Java堆棧跟蹤信息。

 2)jstack命令可以生成JVM當前時刻的線程快照。線程快照是當前JVM內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等。

 3)如果java程序崩潰生成core文件,jstack工具可以用來獲得core文件的java stack和native stack的信息,從而可以輕鬆地知道java程序是如何崩潰和在程序何處發生問題。

3、jstack生成的線程調用堆棧快照中的線程狀態

  • NEW   未啓動的,不會出現在Dump中;
  • RUNNABLE  在虛擬機內執行的;
  • BLOCKED  受阻塞並等待監視器鎖;
  • WATING  無限期等待另一個線程執行特定操作;
  • TIMED_WATING  有時限的等待另一個線程的特定操作;
  • TERMINATED  已退出的;

4、jstack日誌分析要點

 1)查看具有相同堆棧跟蹤的線程;

  當應用程序中存在性能瓶頸時,大多數線程將開始在該有問題的瓶頸區域上累積。這些線程將具有相同的堆棧跟蹤。因此,每當大量線程表現出相同/重複的堆棧跟蹤時,就應該研究這些堆棧跟蹤。這可能存在性能問題。

   以下是一些方案:

  1. 假設外部服務正在變慢,那麼大量線程將開始等待其響應。在這種情況下,這些線程將表現出相同的堆棧跟蹤。
  2. 假設一個線程獲得了一個鎖,它從未被釋放過,那麼處於相同執行路徑的其他幾個線程將進入阻塞狀態,表現出相同的堆棧跟蹤。
  3. 如果循環(for 循環, while 循環, do..while 循環) 條件不會終止,則執行該循環的多個線程將呈現相同的堆棧跟蹤。

  eg:

  此應用程序運行正常,但突然間變得無響應。獲取來自此應用程序的dump。顯示400個線程中的225個線程表現出相同的堆棧跟蹤:

  

   從堆棧跟蹤中可以推斷出線程被阻止並等待對象上的鎖定0x00000006afaa5a60。225 個這樣的線程正在等待獲得同一對象上的鎖定。這絕對是一個不好的跡象,說明線程匱乏。

  這個鎖是由“ajp-bio-192.168.100.128-9022-exec-84”持有的。下面是堆棧跟蹤此線程。此線程0x00000006afaa5a60獲取了對象上的鎖但在獲取鎖後,卡住了等待數據庫的響應。對於此應用程序數據庫,未設置超時。因此,此線程的數據庫調用再也沒有返回。由於這個原因,其他225個線程被卡住了。因此,應用程序變得無響應。

  設置正確的數據庫超時值後,此問題消失了。

  

 2)如果應用程序佔用大量 CPU,重點查看處於 run狀態的線程

 3)是否存在阻塞線程,阻止其他線程的線程顯示在此處。阻塞線程使應用程序無響應。

  線程A可能已獲取鎖 1,但永遠不會釋放它。線程B可能已獲取鎖 2 並等待此鎖 1。線程C可能正在等待獲取鎖 2。線程之間的這種傳遞塊可能會使整個應用程序無響應。

  補充:Monitor

  Monitor是 Java中用以實現線程之間的互斥與協作的主要手段,它可以看成是對象或者 Class的鎖。每一個對象都有,也僅有一個 monitor。下 面這個圖,描述了線程和 Monitor之間關係,以及線程的狀態轉換圖:

進入區(Entry Set):表示線程通過synchronized要求獲取對象的鎖。如果對象未被鎖住,則進入擁有者,否則則在進入區等待。一旦對象鎖被其他線程釋放,立即參與競爭。

擁有者(The Owner):表示某一線程成功競爭到對象鎖。

等待區(Wait Set):表示線程通過對象的wait方法,釋放對象的鎖,並在等待區等待被喚醒。

  從圖中可以看出,一個 Monitor在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set”和 “Wait Set”裏面等候。在“Entry Set”中等待的線程狀態是“Waiting for monitor entry”,而在 “Wait Set”中等待的線程狀態是 “in Object.wait()”。 先看 “Entry Set”裏面的線程。稱被 synchronized保護起來的代碼段爲臨界區。當一個線程申請進入臨界區時,它就進入了 “Entry Set”隊列。

 4)GC垃圾處理器

  根據所使用的 GC 算法類型(串行、並行、G1、CMS),將創建默認數量的垃圾回收線程。有時,會根據默認配置創建太多無關的GC線程。太多的 GC 線程也會影響應用程序的性能。因此,應仔細配置GC線程計數。

  4.1)並行垃圾處理器

  如果使用並行 GC 算法,則 GC 線程數由 -XX:並行 GC 線程屬性控制。 Linux/x86 計算機上的 -XX:ParallelGCThreads默認值是根據以下公式派生的:

  

   因此,如果JVM在具有32個處理器的服務器上運行,則ParallelGCThread值將爲:23(即8 + (32 – 8)*(5/8))。

  4.2)CMS垃圾收集器

  如果使用 CMS GC 算法,則 GC 線程數由 -XX:並行線程和 -XX:連接線程屬性控制。默認值 -XX:康格線程數是根據以下公式派生的:

  最大((並行線程+2)/4, 1)因此,如果JVM在具有32個處理器的服務器上運行,那麼

  • ParallelGCThread值將爲:23(即 8 + (32 – 8)*(5/8))
  • ConcGCThreads值將是:6。
  • 因此,總 GC 線程數爲:29(即ParallelGCThread 數 + ConcGCThreads ,即 23 + 6)

  4.3)G1垃圾收集器

  如果使用 G1 GC 算法,則 GC 線程數由 -XX:並行線程、-XX:連接線程、-XX:G1限制線程屬性控制。默認值 -XX:G1組件無限線程是根據以下公式派生的:

  並行線程數+1,因此,如果JVM在具有32個處理器的服務器上運行,那麼

  • ParallelGCThread值將爲:23(即 8 + (32 – 8)*(5/8))
  • ConcGCThreads值將爲:6
  • G1ConcRefinementThreads定義線程值將爲24(即23 + 1)
  • 因此,總 GC 線程數爲:53(即ParallelGCThread  數 + ConcGCThreads + G1ConcRefinementThreads,即 23 + 6 + 24)

  GC 的 53 個線程是一個相當大的數字。應適當調整。

 5)堆棧溢出

  

   堆棧溢出漏洞的解決方案是什麼?

  1. 修復代碼
  由於非終止遞歸調用等,線程堆棧大小可以增長到很大的大小。在這種情況下,必須修復導致遞歸循環的源代碼。當拋出“堆棧溢出錯誤”時,它將打印它以遞歸方式執行的代碼的堆棧跟蹤。此代碼是開始調試和修復問題的良好指針。

  2. 增加線程堆棧大小 (-Xs)
  可能存在需要增加線程堆棧大小的正當理由。也許線程必須執行大量方法或大量局部變量/在線程一直在執行的方法中創建。在這種情況下,您可以使用 JVM 參數“-Xss”來增加線程的堆棧大小。啓動應用程序時需要傳遞此參數。

  

   6)死鎖問題

  

  從上面的線程dump中,可以看到

  • “線程 4”正在等待由“線程 0”持有的0x00000007ac3b2718
  • “線程 0”正在等待由“線程 1”保存0x00000007ac3b2748
  • “線程 1”正在等待由“線程 2”保存的0x00000007ac3b2778
  • “線程 2”正在等待由“線程 3”持有的0x00000007ac3b27a8
  • “線程 3”正在等待由“線程 4”持有的0x00000007ac3b27d8

 7)、妖精陷阱

  在垃圾回收過程中,具有 finalizize()方法的對象與沒有它們的對象處理方式不同。在垃圾回收階段,具有 finalize() 的對象不會立即從內存中逐出。相反,作爲第一步,這些對象被添加到 java.lang.ref.終結器對象的內部隊列中。有一個名爲“Finalizer”的低優先級 JVM 線程,它執行隊列中每個對象的 finalize() 方法。只有在執行 finalizize() 方法之後,對象纔有資格進行垃圾回收。由於 finalizize()方法的實現不佳,如果Finalizer線程被阻止,將對 JVM 產生嚴重的級聯影響。 java.lang.ref.Finalize 的內部隊列將開始增長,導致JVM的內存消耗迅速增長,導致內存不足錯誤,危及整個JVM的可用性。因此,在分析線程dump時,強烈建議研究Finalizer線程的堆棧跟蹤。

  eg:在 finalizize() 方法中被阻止的Finalizer線程的示例堆棧跟蹤

  

5、jstack日誌日誌分析

  刷新某個頁面造成CPU使用率耗盡的jstack日誌如下:

 日誌分析:

5.1、基礎知識點梳理

5.1.1、通過日誌可以看到大量線程處於waiting on condition狀態,該狀態出現在線程等待某個條件的發生;

  1)最常見的情況是線程在等待網絡的讀寫數據(即進行網絡IO),比如當網絡數據沒有準備好讀時,線程處於這種等待狀態。而一旦有數據準備好讀之後,線程會重新激活,讀取並處理數據;

  2)正等待網絡讀寫,這可能是一個網絡瓶頸的徵兆,因爲網絡阻塞導致線程無法執行,原因有兩種:

  一種情況是網絡非常忙,幾乎消耗了所有的帶寬,仍然有大量數據等待網絡讀寫;

  另一種情況也可能是網絡空閒,但由於路由等問題,導致包無法正常的到達;

  3)該線程在 sleep,等待 sleep的時間到了時候,將被喚醒。

      推薦閱讀https://zhuanlan.zhihu.com/p/475571849 

5.1.2、僞運行狀態

  處於“可運行”狀態的線程會消耗 CPU。因此,當分析線程dump以消除高 CPU 消耗時,應徹底檢查處於“可運行”狀態的線程。通常在線程dump中,多個線程被分類爲“RUNNABLE”狀態。但實際上,一部分實際上並沒有RUN狀態,而是在等待。但是,JVM仍然將它們歸類爲“可運行”狀態。

  eg:

  在這些堆棧跟蹤中,線程實際上並不處於“可運行”狀態。即沒有主動執行任何代碼。只是在等待套接字讀取或寫入。因爲JVM並不真正知道線程的native方法中正在做什麼,JVM將它們分類爲“RUNNABLE”狀態。實際運行的線程將消耗 CPU,而這些線程處於 I/O 等待狀態,不消耗任何 CPU。

       

5.2、通過可視化工具分析(推薦在線分析網站https://fastthread.io/ft-index.jsp)

1)使用top指令查看內存正常;

2)分析GC線程狀況

GC線程數正常;

3)查看是否存在阻塞線程

檢測結果未發現異常。

4)常見情況下的異常檢測(死鎖、終結器線程等)

如圖可知,並未發現異常情況;

 5)排除了以上情況,說明沒有明顯的代碼護或者環境配置異常,對於CPU消耗過高的情況,分析RUN狀態的線程

 查看cpu佔用最高的線程的pid

top -Hp pid

根據進程號查看堆棧信息

jstack 1 | grep -A 10 "0x88"(線程號轉爲16進制)

對比兩個堆棧日誌發現,佔用CPU高的線程是數據庫進行網絡IO線程,線程正在等待系統調用epoll實例事件。

補充:

  epoll_wait()系統調用等待文件描述符epfd引用的epoll實例上的事件。事件所指向的存儲區域將包含可供調用者使用的事件。 epoll_wait()最多返回最大事件。 maxevents參數必須大於零。 timeout參數指定epoll_wait()將阻止的最小毫秒數。 指定超時值爲-1會導致epoll_wait()無限期阻塞,而指定的超時時間等於零導致epoll_wait()立即返回,即使沒有可用事件。

火焰圖

 確定問題原因,排查刷新頁面時的日誌中的sql,從數據庫執行發現有髒數據導致的一個sql執行時一直不返回查詢結果,數據進行清洗,測試,系統正常。

 

推薦相關文章:

https://blog.csdn.net/lsz137105/article/details/104644396

https://blog.csdn.net/zhoumuyu_yu/article/details/112476477

 

感謝閱讀,借鑑了不少大佬資料,如需轉載,請註明出處,謝謝!https://www.cnblogs.com/huyangshu-fs/p/16718553.html

 

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