天天看點

0-一次線上問題排查所引發的思考

前言

之前或多或少分享過一些記憶體模型、對象建立之類的内容,其實大部分人看完都是懵懵懂懂,也不知道這些的實際意義。

如:

  • 線程執行一個任務遲遲沒有傳回,應用假死。
  • 接口響應緩慢,甚至請求逾時。
  • CPU 高負載運作。

這類問題并不像一個空指針、數組越界這樣明顯好查,這時就需要剛才提到的記憶體模型、對象建立、線程等相關知識結合在一起來排查問題了。

正好這次借助之前的一次生産問題來聊聊如何排查和解決問題。

生産現象

首先看看問題的背景吧:

我這其實是一個定時任務,在固定的時間會開啟 N 個線程并發的從 Redis 中擷取資料進行運算。

業務邏輯非常簡單,但應用一般涉及到多線程之後再簡單的事情都要小心對待。

果不其然這次就出問題了。

現象:原本隻需要執行幾分鐘的任務執行了幾個小時都沒退出。翻遍了所有的日志都沒找到異常。

于是便開始定位問題之路。

定位問題

既然沒辦法直接從日志中發現異常,那就隻能看看應用到底在幹嘛了。

這次我使用了 jstack 來檢視線程的執行情況,它的作用其實就是 dump 目前的線程堆棧。

當然在 dump 之前是需要知道我應用的 pid 的,可以使用 jps -v 這樣的方式列出所有的 Java 程序。

當然如果知道關鍵字的話直接使用 ps aux|grep java 也是可以的。

拿到 pid=1523 了之後就可以利用 jstack 1523 > 1523.log 這樣的方式将 dump 檔案輸出到日志檔案中。

如果應用簡單不複雜,線程這些也比較少其實可以直接打開檢視。

但複雜的應用導出來的日志檔案也比較大還是建議用專業的分析工具。

我這裡的日志比較少直接打開就可以了。

因為我清楚知道應用中開啟的線程名稱,是以直接根據線程名就可以在日志中找到相關的堆棧:

是以通常建議大家線程名字給的有意義,在排查問題時很有必要。

其實其他幾個線程都和這裡的堆棧類似,很明顯的看出都是在做 Redis 連接配接。

于是我登入 Redis 檢視了目前的連接配接數,發現已經非常高了。

這樣 Redis 的響應自然也就變慢了。

接着利用 jps -v 列出了目前是以在跑的 Java 程序,果不其然有好幾個應用都在查詢 Redis,而且都是并發連接配接,問題自然就找到了。

解決辦法

是以問題的主要原因是:大量的應用并發查詢 Redis,導緻 Redis 的性能降低。

既然找到了問題,那如何解決呢?

  • 減少同時查詢 Redis 的應用,分開時段降低 Redis 的壓力。
  • 将 Redis 複制幾個叢集,各個應用分開查詢。但是這樣會涉及到資料的同步等運維操作,或者由程式了進行同步也會增加複雜度。

本地模拟

上文介紹的是線程相關問題,現在來分析下記憶體的問題。

以這個類為例:

https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/oom/heap/HeapOOM.java

1public class HeapOOM {

2

3    public static void main(String[] args) {

4        List<String> list = new ArrayList<>(10) ;

5        while (true){

6            list.add("1") ;

7        }

8    }

9}

啟動參數如下:

1-Xms20m

2-Xmx20m

3-XX:+HeapDumpOnOutOfMemoryError

4-XX:HeapDumpPath=/Users/xx/Documents

為了更快的突出記憶體問題将堆的最大記憶體固定在 20M,同時在 JVM 出現 OOM 的時候自動 dump 記憶體到 /Users/xx/Documents(不配路徑則會生成在目前目錄)。

執行之後果不其然出現了異常:

同時對應的記憶體 dump 檔案也生成了。

記憶體分析

這時就需要相應的工具進行分析了,最常用的自然就是 MAT 了。

我試了一個線上工具也不錯(檔案大了就不适合了):

http://heaphero.io/index.jsp

上傳剛才生成的記憶體檔案之後:

因為是記憶體溢出,是以主要觀察下大對象:

也有相應提示,這個很有可能就是記憶體溢出的對象,點進去之後:

看到這個堆棧其實就很明顯了:

在向 ArrayList 中不停的寫入資料時,會導緻頻繁的擴容也就是數組複制這些過程,最終達到 20M 的上限導緻記憶體溢出了。

更多建議

上文說過,一旦使用了多線程,那就要格外小心。

以下是一些日常建議:

  • 盡量不要線上程中做大量耗時的網絡操作,如查詢資料庫(可以的話在一開始就将資料從從 DB 中查出準備好)。
  • 盡可能的減少多線程競争鎖。可以将資料分段,各個線程分别讀取。
  • 多利用 CAS+自旋 的方式更新資料,減少鎖的使用。
  • 應用中加上 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp 參數,在記憶體溢出時至少可以拿到記憶體日志。
  • 線程池監控。如線程池大小、隊列大小、最大線程數等資料,可提前做好預估。
  • JVM 監控,可以看到堆記憶體的漲幅趨勢,GC 曲線等資料,也可以提前做好準備。

總結

線上問題定位需要綜合技能,是以是需要一些基礎技能。如線程、記憶體模型、Linux 等。

當然這些問題沒有實操過都是紙上談兵;如果第一次碰到線上問題,不要慌張,反而應該慶幸解決之後你又會習得一項技能。