Java線上故障解決方案

在這裏插入圖片描述

一、前言:

我們在⽣產環境中,程序代碼、硬件、⽹絡、協作軟件等任⼀因素,都會引發意想不到的問題,所以排查產線問題⽐較困難,所以問題的定位體現了⼀名⼯程師的基礎能⼒,問題的解決則體現了⼯程師的技能素養。


二、線上常見問題

在這裏插入圖片描述
如出現 (CPU佔⽤率過⾼、磁盤使⽤率100%、系統可⽤內存低、服務間調⽤時間過⻓、多線程併發異常、死鎖等)

三、定位問題

方案 :

  1. 業務⽇志分析排查

通常情況下,⼤部分錯誤信息都會在⽇志上有所體現

public static void main(String[] args) throws InterruptedException {
   
    
        ExecutorService executorService = Executors.newFixedThreadPool(100000);
        System.out.println("開始執行");
        for (int i = 0; i < 100000000; i++) {
   
    
            executorService.execute(() -> {
   
    
                String payload = IntStream.rangeClosed(1, 1000000)
                        .mapToObj(__ -> "a") .collect(Collectors.joining("")) + UUID.randomUUID().toString();
                System.out.println("等待一小時開始");
                try {
   
    
                    TimeUnit.HOURS.sleep(1);
                }catch (Exception e){
   
    
                    log.info(payload);
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(1,TimeUnit.HOURS);
    }

在這裏插入圖片描述

通過日誌可以發現出錯誤的位置是 第33行,報錯java.lang.OutOfMemoryError 錯誤, 因爲我們看下 newFixedThreadPool 方法的源碼,發現,線程池的工作隊列直接 new 了一個 LinkedBlockingQueue,他是一個無界隊列。如果任務較多並且執行較慢但話,隊列可能會快速積壓,撐爆內存導致OOM


我們 ⼀定要在關鍵代碼邏輯位置輸出相關⽇志,尤其是在代碼發⽣異常的時候,定要將⽇志輸出到⽂件中,只有這樣,才更利於我們的排查。

  1. APM分析排查

APM,全稱Application Performance Management,應⽤性能管理,⽬的是通過各種探針採集數據, 收集關鍵指標,同時搭配數據呈現以實現對應⽤程序性能管理和故障管理的系統化解決⽅案。通過分佈 式鏈路調⽤跟蹤系統,通過在系統請求中透傳 trace-id,將所有相關⽇志進⾏聚合,然後⽇志統⼀採集 和分析後,以圖形化的形式展示給⼯程師們,⽽他們在排查問題的時候,可以簡單粗暴且直觀的調度出 問題最根本的原因。

通常在分佈式架構中,僅通過分析單個服務的⽇志信息是不夠的,此時則需要APM進⾏全鏈路分析,通過請求鏈路監控,實時的發現鏈路中相關服務的異常情況。

在這裏插入圖片描述

⽬前市場上使⽤較多的鏈路跟蹤⼯具有如下⼏個:

  1. 物理環境排查
CPU分析
  • CPU使⽤率是衡量系統繁忙程度的重要指標。但是CPU使⽤率的安全閾值是相對的,取決於你的系統的IO密集型還是計算密集型。⼀般計算密集型應⽤CPU使⽤率偏⾼load偏低,IO密集型相反。
[root@ ~]# top

top命令是Linux下常⽤的 CPU 性能分析⼯具,能夠實時顯示系統中各個進程的資源佔⽤狀況,常⽤於服務端性能分析。
在這裏插入圖片描述

top 命令顯示了各個進程 CPU 使⽤情況,⼀般 CPU 使⽤率從⾼到低排序展示輸出。其中 LoadAverage 顯示最近1分鐘、5分鐘和15分鐘的系統平均負載,上圖各值爲3.4、3.31、3.46。


我們⼀般會關注 CPU 使⽤率最⾼的進程,正常情況下就是我們的應⽤主進程。第七⾏以下:各進
程的狀態監控,參數說明:



  • PID : 進程id
  • USER : 進程所有者的⽤戶名
  • PR : 進程優先級
  • NI : nice值。負值表示⾼優先級,正值表示低優先級
  • VIRT : 進程使⽤的虛擬內存總量,單位kb
  • SHR : 共享內存⼤⼩
  • %CPU : 上次更新到現在的CPU時間佔⽤百分⽐
  • %MEM : 進程使⽤的物理內存百分⽐
  • TIME+ : 進程使⽤的CPU時間總計,單位1/100秒
  • COMMAND : 命令名稱、命令⾏

內存
[root@ ~]# free -h

內存是排查線上問題的重要參考依據,free 是顯示的當前內存的使⽤,-h 表示⼈類可讀性。
在這裏插入圖片描述

參數說明:

  • total :內存總數
  • used:已經使⽤的內存數
  • free:空閒的內存數
  • shared:被共享使⽤的物理內存⼤⼩
  • buffers/buffer:被 buffer 和 cache 使⽤的物理內存⼤⼩
  • available: 還可以被應⽤程序使⽤的物理內存⼤⼩
磁盤
[root@ ~]# df -h

在這裏插入圖片描述

⽹絡
[root@ ~]# dstat

在這裏插入圖片描述

默認情況下,dstat每秒都會刷新數據


三、Arthas診斷命令

在這裏插入圖片描述
Arthas 是Alibaba開源的Java診斷工具,深受開發者喜愛。

  • 當你遇到以下類似問題而束手無策時,Arthas可以幫助你解決:
  • 這個類從哪個 jar 包加載的?爲什麼會報各種類相關的 Exception?
  • 我改的代碼爲什麼沒有執行到?難道是我沒 commit?分支搞錯了?
  • 遇到問題無法在線上 debug,難道只能通過加日誌再重新發布嗎?
  • 線上遇到某個用戶的數據處理有問題,但線上同樣無法 debug,線下無法重現!
  • 是否有一個全局視角來查看系統的運行狀況?
  • 有什麼辦法可以監控到JVM的實時運行狀態?
  • 怎麼快速定位應用的熱點,生成火焰圖?

官方文檔 :https://arthas.aliyun.com/doc/

安裝

 [root ~]# mkdir arthas
 [root ~]# cd arthas/
 [root ~]#  wget https://maven.aliyun.com/repository/public/com/taobao/arthas/arthas-packaging/3.1.4/arthas-packaging-3.1.4-bin.zip
 [root ~]# rm -rf /home/admin/.arthas/lib/*
 [root ~]# cd arthas
 [root ~]# ./install-local.sh
 [root ~]# java -jar arthas-boot.jar

在這裏插入圖片描述
arthas 會列出已存在的Java進程,並提醒輸⼊序號,鍵⼊回⻋,進⼊arthas 診斷界⾯。

arthas常⻅命令介紹

  • jvm 查看當前 JVM 的信息
  • thread 查看當前 JVM 的線程堆棧信息,-b選項可以⼀鍵檢測死鎖
  • trace ⽅法內部調⽤路徑,並輸出⽅法路徑上的每個節點上耗時,服務間調⽤時間過⻓時使⽤
  • stack 輸出當前⽅法被調⽤的調⽤路徑
  • Jad 反編譯指定已加載類的源碼,反編譯便於理解業務
  • logger 查看和修改logger,可以動態更新⽇志級別。

四、JVM問題定位命令

在 JDK 安裝⽬錄的 bin ⽬錄下默認提供了很多有價值的命令⾏⼯具。每個⼩⼯具體積基本都⽐較⼩,因
爲這些⼯具只是 jdk\lib\tools.jar 的簡單封裝。

其中,定位排查問題時最爲常⽤命令包括:jps(進程)、jmap(內存)、jstack(線程)、jinfo(參
數)等。

  • jps:查詢當前機器所有Java進程信息
  • jmap:輸出某個 Java 進程內存情況
  • jstack:打印某個 Java 線程的線程棧信息
  • jinfo:⽤於查看 jvm

1. jps

jps ⽤於輸出當前⽤戶啓動的所有進程 ID,當線上發現故障或者問題時,利⽤ jps 快速定位對應的 Java
進程 ID。

 [root ~]# jps -m

參數解釋:

  • m:輸出傳⼊ main ⽅法的參數
  • l:輸出完全的包名,應⽤主類名,jar的完全路徑名
    在這裏插入圖片描述

當然,我們也可以使⽤ Linux 提供的查詢進程狀態命令也能快速獲取 Tomcat 服務的進程 id。⽐如:

 [root ~]# ps -ef|grep tomcat

2. jmap

jmap(Java Memory Map)可以輸出所有內存中對象的⼯具,甚⾄可以將 VM 中的 heap,以⼆進制輸
出成⽂本,使⽤⽅式如下: jmap -heap:

 [root ~]# jmap -heap pid #輸出當前進程JVM堆內存新⽣代、⽼年代、持久代、GC算法等信

注意:pid 通過jps命令得知
在這裏插入圖片描述

3. jstack

jstack⽤於打印某個 Java 線程的線程棧信息

舉個慄⼦,某 Java 進程 CPU 佔⽤率⾼,我們想要定位到其中 CPU 佔⽤率最⾼的線程,如何定位?

3.1 利⽤ top 命令可以查出佔 CPU 最⾼的線程 pid

 [root ~]# top -Hp pid

在這裏插入圖片描述
3.2 佔⽤率最⾼的線程 ID 爲 22021,將其轉換爲16進制形式(因爲 java native 線程以16進制形式輸
出)

 [root ~]# printf '%x\n' 22021

3.3 利⽤ jstack 打印出 Java 線程調⽤棧信息

 [root ~]# jstack 21993 | grep '0x5605' -A 50 --color

在這裏插入圖片描述

4. jinfo

jinfo可以⽤來查看正在運⾏的 java 應⽤程序的擴展參數,包括Java System屬性和JVM命令⾏參數;也
可以動態的修改正在運⾏的 JVM ⼀些參數。

 [root ~]# jinfo pid

在這裏插入圖片描述

5. jstat

jstat命令可以查看堆內存各部分的使⽤量,以及加載類的數量。

 [root ~]# jstat -gc pid

在這裏插入圖片描述

五、GC分析

  1. Gc日誌分析

Java 虛擬機GC⽇志是⽤於定位問題重要的⽇志信息,頻繁的GC將導致應⽤吞吐量下降、響應時間增
加,甚⾄導致服務不可⽤。

JVM的GC日誌的主要參數包括如下幾個:

  • -XX:+PrintGC 輸出GC日誌
  • -XX:+PrintGCDetails 輸出GC的詳細日誌
  • -XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
  • -XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
  • -XX:+PrintHeapAtGC 在進行GC的前後打印出堆的信息
  • -Xloggc:…/logs/gc.log 日誌文件的輸出路徑

IDEA 配置

在這裏插入圖片描述

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/apps/logs/gc/gc.log -
XX:+UseConcMarkSweepGC

我們可以在 Java 應⽤的啓動參數中增加-XX:+PrintGCDetails 可以輸出 GC 的詳細⽇志,例外還可以增
加其他的輔助參數,如 -Xloggc 制定 GC ⽇志⽂件地址。如果你的應⽤還沒有開啓該參數,下次重啓時請
加⼊該參數。

以後打印出來的日誌爲:

0.756: [Full GC (System) 0.756: [CMS: 0K->1696K(204800K), 0.0347096 secs] 11488K->1696K(252608K), 
[CMS Perm : 10328K->10320K(131072K)], 0.0347949 secs] [Times: user=0.06 sys=0.00, real=0.05 secs]  

分析:

5.617(時間戳): [GC(Young GC) 5.617(時間戳): [ParNew(使用ParNew作爲年輕代的垃圾回收期):
 43296K(年輕代垃圾回收前的大小)->7006K(年輕代垃圾回收以後的大小)(47808K)(年輕代的總大小), 0.0136826 secs(回收時間)]
  1. CMS GC ⽇志分析

Concurrent Mark Sweep(CMS)是⽼年代垃圾收集器,從名字(Mark Sweep)可以看出,CMS 收集
器就是“標記-清除”算法實現的,分爲六個步驟:

  • 初始標記(STW initial mark)
  • 併發標記(Concurrent marking)
  • 併發預清理(Concurrent precleaning)
  • 重新標記(STW remark)
  • 併發清理(Concurrent sweeping)
  • 併發重置(Concurrent reset)

其中初始標記(STW initial mark) 和 重新標記(STW remark)需要“Stop the World”。

老年代的GC日誌(CMS)

//第一階段 初始標記,CMS的第一個STW階段,這個階段會所有的GC Roots進行標記。
 2020-10-20T17:04:45.424+0800: 10.756: [GC (CMS Initial Mark) [1 CMS-initial-mark: 68287K(68288K)] 69551K(99008K), 0.0019516 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
 解析:CMS Initial Mark 說明該階段爲初始標記階段,68287K(68288K)當前老年代空間的用量和總量,69551K(99008K)當前堆空間的用量和總量,0.0019516 secs初始化標記所耗費的時間。
 ​
 ​
 //第二階段併發標記
 2020-10-20T17:04:45.426+0800: 10.758: [CMS-concurrent-mark-start]
 2020-10-20T17:04:45.519+0800: 10.850: [CMS-concurrent-mark: 0.092/0.092 secs] [Times: user=0.56 sys=0.01, real=0.09 secs]
 解析:CMS-concurrent-mark: 0.092/0.092 secs] 併發標記所所耗費的時間
 ​
 ​
 //第三階段 併發預清理階段,併發執行的階段。在本階段,會查找前一階段執行過程中,從新生代晉升或新分配或被更新的對象。通過併發地重新掃描這些對象,預清理階段可以減少重新標記階段的工作量。
 2020-10-20T17:04:45.519+0800: 10.850: [CMS-concurrent-preclean-start]
 2020-10-20T17:04:45.598+0800: 10.930: [CMS-concu解析rrent-preclean: 0.080/0.080 secs] [Times: user=0.46 sys=0.00, real=0.08 secs]
 解析: [CMS-concurrent-preclean: 0.080/0.080 secs] 預清階段所使用功能的時間。
 ​
 ​
 //第四階段 併發可中止的預清理階段。這個階段工作和上一個階段差不多。增加這一階段是爲了讓我們可以控制這個階段的結束時機,比如掃描多長時間(默認5秒)或者Eden區使用佔比達到期望比例(默認50%)就結束本階段。
 2020-10-20T17:04:45.599+0800: 10.930: [CMS-concurrent-abortable-preclean-start]
 2020-10-20T17:04:45.599+0800: 10.930: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
 ​
 ​
 //第五階段 重新標記階段,需要STW,從GC Root開始重新掃描整堆,標記存活的對象。需要注意的是,雖然CMS只回收老年代的垃圾對象,但是這個階段依然需要掃描新生代,因爲很多GC Root都在新生代。
 2020-10-20T17:04:45.608+0800: 10.939: [GC (CMS Final Remark) [YG occupancy: 25310 K (30720 K)]2020-10-20T17:04:45.608+0800: 10.939: [Rescan (parallel) , 0.0117481 secs]2020-10-20T17:04:45.620+0800: 10.951: [weak refs processing, 0.0000354 secs]2020-10-20T17:04:45.620+0800: 10.951: [class unloading, 0.0268352 secs]2020-10-20T17:04:45.647+0800: 10.978: [scrub symbol table, 0.0053781 secs]2020-10-20T17:04:45.652+0800: 10.983: [scrub string table, 0.0006005 secs][1 CMS-remark: 68287K(68288K)] 93598K(99008K), 0.0447563 secs] [Times: user=0.18 sys=0.00, real=0.04 secs]
 解析:
  [YG occupancy: 25310 K (30720 K)] =》 新生代空間佔用大小,新生代總大小。
 [Rescan (parallel) , 0.0117481 secs] =》 暫停用戶線程的情況下完成對所有存活對象的標記,此階段所花費的時間。
 [weak refs processing, 0.0000354 secs] =》第一步 標記處理弱引用;
 [class unloading, 0.0033120 secs] =》 第二步,標記那些已卸載未使用的類;
  [scrub symbol table, 0.0053781 secs][scrub string table, 0.0004780 secs =》 最後標記未被引用的常量池對象。
  [1 CMS-remark: 68287K(68288K)] 93598K(99008K), 0.0447563 secs] =》 重新標記完成後 老年代使用量與總量,堆空間使用量與總量。
  [Times: user=0.18 sys=0.00, real=0.04 secs] =》 各個維度的時間消耗。
 ​
 ​
  //第六階段 併發清理階段, 對前面標記的所有可回收對象進行回收
 2020-10-20T17:04:45.653+0800: 10.984: [CMS-concurrent-sweep-start]
 2020-10-20T17:04:45.689+0800: 11.020: [CMS-concurrent-sweep: 0.036/0.036 secs] [Times: user=0.20 sys=0.01, real=0.04 secs]
 2020-10-20T17:04:45.689+0800: 11.020: [CMS-concurrent-reset-start]
 2020-10-20T17:04:45.689+0800: 11.021: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
 解析:
 [CMS-concurrent-sweep: 0.036/0.036 secs]  開併發清理所耗費的時間。
 [CMS-concurrent-reset: 0.000/0.000 secs]  重置數據和結構信息。

異常情況有:
伴隨 prommotion failed,然後 Full GC:
[prommotion failed:存活區內存不⾜,對象進⼊⽼年代,⽽此時⽼年代也仍然沒有內存容納對象,將
導致⼀次Full GC]
伴隨 concurrent mode failed,然後 Full GC:
[concurrent mode failed:CMS回收速度慢,CMS完成前,⽼年代已被佔滿,將導致⼀次Full GC]
頻繁 CMS GC: [內存喫緊,⽼年代⻓時間處於較滿的狀態]






個人博客地址:http://blog.yanxiaolong.cn/



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