Java 線上問題排查思路與工具使用

一、前言

Java 語言是當前互聯網應用最爲廣泛的語言,作爲一名 Java 程序猿,當業務相對比較穩定之後平常工作除了 coding 之外,大部分時間(70%~80%)是會用來排查突發或者週期性的線上問題。

由於業務應用 bug(本身或引入第三方庫)、環境原因、硬件問題等原因,Java 線上服務出現故障 / 問題幾乎不可避免。例如,常見的現象包括部分請求超時、用戶明顯感受到系統發生卡頓等等。

儘快線上問題從系統表象來看非常明顯,但排查深究其發生的原因還是比較困難的,因此對開發測試或者是運維的同學產生了許多的困擾。

排查定位線上問題是具有一定技巧或者說是經驗規律的,排查者如果對業務系統瞭解得越深入,那麼相對來說定位也會容易一些。

不管怎麼說,掌握 Java 服務線上問題排查思路並能夠熟練排查問題常用工具 / 命令 / 平臺是每一個 Java 程序猿進階必須掌握的實戰技能。

筆者依據自己的 工作經驗總結出一套基本的線上問題排查流程,同學們可以根據自己的實際工作情況進行歸納總結。

二、Java 服務常見線上問題

所有 Java 服務的線上問題從系統表象來看歸結起來總共有四方面:CPU、內存、磁盤、網絡。例如 CPU 使用率峯值突然飈高、內存溢出 (泄露)、磁盤滿了、網絡流量異常、FullGC 等等問題。

基於這些現象我們可以將線上問題分成兩大類: 系統異常、業務服務異常。

1. 系統異常

常見的系統異常現象包括:  CPU 佔用率過高、CPU 上下文切換頻率次數較高、磁盤滿了、磁盤 I/O 過於頻繁、網絡流量異常 (連接數過多)、系統可用內存長期處於較低值 (導致 oom killer) 等等。

這些問題可以通過 top(cpu)、free(內存)、df(磁盤)、dstat(網絡流量)、pstack、vmstat、strace(底層系統調用) 等工具獲取系統異常現象數據。

此外,如果對系統以及應用進行排查後,均未發現異常現象的更笨原因,那麼也有可能是外部基礎設施如 IAAS 平臺本身引發的問題。

例如運營商網絡或者雲服務提供商偶爾可能也會發生一些故障問題,你的引用只有某個區域如廣東用戶訪問系統時發生服務不可用現象,那麼極有可能是這些原因導致的。

今天我司部署在阿里雲華東地域的業務系統中午時分突然不能爲廣東地區用戶提供正常服務,對系統進行各種排查均爲發現任何問題。

最後,通過查詢阿里雲公告得知原因是 “ 廣東地區電信線路訪問華東地區互聯網資源(包含阿里雲華東 1 地域)出現網絡丟包或者延遲增大的異常情況 “。

https://help.aliyun.com/noticelist/articleid/20724342.html?spm=5176.789004748.n2.6.LeTsMp

?wx_fmt=png

2. 業務服務異常

常見的業務服務異常現象包括: PV 量過高、服務調用耗時異常、線程死鎖、多線程併發問題、頻繁進行 Full GC、異常安全攻擊掃描等。

三、問題定位

我們一般會採用排除法,從外部排查到內部排查的方式來定位線上服務問題。

  • 首先我們要排除其他進程 (除主進程之外) 可能引起的故障問題;

  • 然後排除業務應用可能引起的故障問題;

  • 可以考慮是否爲運營商或者雲服務提供商所引起的故障。

1. 定位流程

1.1 系統異常排查流程

?wx_fmt=gif

1.2 業務應用排查流程

?wx_fmt=png

2. Linux 常用的性能分析工具

Linux 常用的性能分析工具使用包括 : top(cpu)、free(內存)、df(磁盤)、dstat(網絡流量)、pstack、vmstat、strace(底層系統調用) 等。

2.1 CPU

CPU 是系統重要的監控指標,能夠分析系統的整體運行狀況。監控指標一般包括運行隊列、CPU 使用率和上下文切換等。

top 命令是 Linux 下常用的 CPU 性能分析工具 , 能夠實時顯示系統中各個進程的資源佔用狀況 , 常用於服務端性能分析。

?wx_fmt=png

top 命令顯示了各個進程 CPU 使用情況 , 一般 CPU 使用率從高到低排序展示輸出。其中 Load Average 顯示最近 1 分鐘、5 分鐘和 15 分鐘的系統平均負載,上圖各值爲 2.46,1.96,1.99。

我們一般會關注 CPU 使用率最高的進程,正常情況下就是我們的應用主進程。第七行以下:各進程的狀態監控。

PID : 進程 id
USER : 進程所有者
PR : 進程優先級
NI : nice 值。負值表示高優先級,正值表示低優先級
VIRT : 進程使用的虛擬內存總量,單位 kb。VIRT=SWAP+RES
RES : 進程使用的、未被換出的物理內存大小,單位 kb。RES=CODE+DATA
SHR : 共享內存大小,單位 kb
S : 進程狀態。D= 不可中斷的睡眠狀態 R= 運行 S= 睡眠 T= 跟蹤 / 停止 Z= 殭屍進程
%CPU : 上次更新到現在的 CPU 時間佔用百分比
%MEM : 進程使用的物理內存百分比
TIME+ : 進程使用的 CPU 時間總計,單位 1/100 秒
COMMAND : 進程名稱

2.2 內存

內存是排查線上問題的重要參考依據,內存問題很多時候是引起 CPU 使用率較高的見解因素。

系統內存:free 是顯示的當前內存的使用 ,-m 的意思是 M 字節來顯示內容。

free -m

?wx_fmt=png

部分參數說明:

  total 內存總數: 3790M
  used 已經使用的內存數: 1880M
  free 空閒的內存數: 118M
  shared 當前已經廢棄不用 , 總是 0
  buffers Buffer 緩存內存數: 1792M

2.3 磁盤

df -h

?wx_fmt=png

du -m /path

?wx_fmt=png

2.4 網絡

dstat 命令可以集成了 vmstat、iostat、netstat 等等工具能完成的任務。

   dstat -c  cpu 情況    -d 磁盤讀寫        -n 網絡狀況        -l 顯示系統負載        -m 顯示形同內存狀況        -p 顯示系統進程信息        -r 顯示系統 IO 情況

?wx_fmt=png

2.5 其它

vmstat:

vmstat 2 10 -t

vmstat 是 Virtual Meomory Statistics(虛擬內存統計)的縮寫 , 是實時系統監控工具。該命令通過使用 knlist 子程序和 /dev/kmen 僞設備驅動器訪問這些數據,輸出信息直接打印在屏幕。

使用 vmstat 2 10  -t 命令,查看 io 的情況 (第一個參數是採樣的時間間隔數單位是秒,第二個參數是採樣的次數)。

?wx_fmt=png

r 表示運行隊列 (就是說多少個進程真的分配到 CPU),b 表示阻塞的進程。    
swpd 虛擬內存已使用的大小,如果大於 0,表示你的機器物理內存不足了,如果不是程序內存泄露的原因,那麼你該升級內存了或者把耗內存的任務遷移到其他機器。
free   空閒的物理內存的大小,我的機器內存總共 8G,剩餘 3415M。
buff   Linux/Unix 系統是用來存儲,目錄裏面有什麼內容,權限等的緩存,我本機大概佔用 300 多 M
cache 文件緩存
si 列表示由磁盤調入內存,也就是內存進入內存交換區的數量;
so 列表示由內存調入磁盤,也就是內存交換區進入內存的數量
一般情況下,si、so 的值都爲 0,如果 si、so 的值長期不爲 0,則表示系統內存不足,需要考慮是否增加系統內存。    
bi 從塊設備讀入數據的總量(讀磁盤)(每秒 kb)
bo 塊設備寫入數據的總量(寫磁盤)(每秒 kb)
隨機磁盤讀寫的時候,這兩個值越大 ((超出 1024k),能看到 cpu 在 IO 等待的值也會越大 這裏設置的 bi+bo 參考值爲 1000,如果超過 1000,而且 wa 值比較大,則表示系統磁盤 IO 性能瓶頸。
in 每秒 CPU 的中斷次數,包括時間中斷
cs(上下文切換 Context Switch)

strace:strace 常用來跟蹤進程執行時的系統調用和所接收的信號。

strace -cp tid strace -T -p tid    -T 顯示每一調用所耗的時間 .    -p pid  跟蹤指定的進程 pid.    -v 輸出所有的系統調用 . 一些調用關於環境變量 , 狀態 , 輸入輸出等調用由於使用頻繁 , 默認不輸出 .    -V 輸出 strace 的版本信息 .

?wx_fmt=png

3. JVM 定位問題工具

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

?wx_fmt=png

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

  • jps: 查詢當前機器所有 JAVA 進程信息;

  • jmap: 輸出某個 java 進程內存情況 (如:產生那些對象及數量等);

  • jstack: 打印某個 Java 線程的線程棧信息;

  • jinfo: 用於查看 jvm 的配置參數。

3.1 jps 命令

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

jps -l -m -m -l -l 參數用於輸出主啓動類的完整路徑

?wx_fmt=png

當然,我們也可以使用 Linux 提供的查詢進程狀態命令,例如:

ps -ef | grep tomcat

我們也能快速獲取 tomcat 服務的進程 id。

3.2 jmap 命令

jmap -heap pid   輸出當前進程 JVM 堆新生代、老年代、持久代等請情況,GC 使用的算法等信息 jmap -histo:live {pid} | head -n 10  輸出當前進程內存中所有對象包含的大小 jmap -dump:format=b,file=/usr/local/logs/gc/dump.hprof {pid} 以二進制輸出檔當前內存的堆情況,然後可以導入 MAT 等工具進行

jmap(Java Memory Map) 可以輸出所有內存中對象的工具 , 甚至可以將 VM 中的 heap, 以二進制輸出成文本。

jmap -heap pid:

jmap -heap pid   輸出當前進程 JVM 堆新生代、老年代、持久代等請情況,GC 使用的算法等信息

jmap 可以查看 JVM 進程的內存分配與使用情況,使用 的 GC 算法等信息。

?wx_fmt=jpeg

jmap -histo:live {pid} | head -n 10:

jmap -histo:live {pid} | head -n 10  輸出當前進程內存中所有對象包含的大小

輸出當前進程內存中所有對象實例數 (instances) 和大小 (bytes), 如果某個業務對象實例數和大小存在異常情況,可能存在內存泄露或者業務設計方面存在不合理之處。

jmap -dump:

jmap -dump:format=b,file=/usr/local/logs/gc/dump.hprof {pid}

-dump:formate=b,file= 以二進制輸出當前內存的堆情況至相應的文件,然後可以結合 MAT 等內存分析工具深入分析當前內存情況。

一般我們要求給 JVM 添加參數 -XX:+Heap Dump On Out Of Memory Error OOM 確保應用發生 OOM 時 JVM 能夠保存並 dump 出當前的內存鏡像。

當然,如果你決定手動 dump 內存時,dump 操作佔據一定 CPU 時間片、內存資源、磁盤資源等,因此會帶來一定的負面影響。

此外,dump 的文件可能比較大 , 一般我們可以考慮使用 zip 命令對文件進行壓縮處理,這樣在下載文件時能減少帶寬的開銷。

下載 dump 文件完成之後,由於 dump 文件較大可將 dump 文件備份至制定位置或者直接刪除,以釋放磁盤在這塊的空間佔用。

3.3 jstack 命令

printf '%x\n' tid   -->  10 進制至 16 進制線程 ID(navtive 線程) %d 10 進制 jstack pid | grep tid -C 30 --color ps -mp 8278 -o THREAD,tid,time | head -n 40

某 Java 進程 CPU 佔用率高,我們想要定位到其中 CPU 佔用率最高的線程。

(1) 利用 top 命令可以查出佔 CPU 最高的線程 pid

top -Hp {pid}

?wx_fmt=png

(2) 佔用率最高的線程 ID 爲 6900,將其轉換爲 16 進制形式 (因爲 java native 線程以 16 進制形式輸出)

printf '%x\n' 6900

?wx_fmt=png

(3) 利用 jstack 打印出 java 線程調用棧信息

jstack 6418 | grep '0x1af4' -A 50 --color

?wx_fmt=png

3.4 jinfo 命令

查看某個 JVM 參數值 jinfo -flag ReservedCodeCacheSize 28461 jinfo -flag MaxPermSize 28461

3.5 jstat 命令

jstat -gc pid jstat -gcutil `pgrep -u admin java`

4. 內存分析工具 MAT

4.1 什麼是 MAT?

MAT(Memory Analyzer Tool),一個基於 Eclipse 的內存分析工具,是一個快速、功能豐富的 JAVA heap 分析工具,它可以幫助我們查找內存泄漏和減少內存消耗。

使用內存分析工具從衆多的對象中進行分析,快速的計算出在內存中對象的佔用大小,看看是誰阻止了垃圾收集器的回收工作,並可以通過報表直觀的查看到可能造成這種結果的對象。

?wx_fmt=png

右側的餅圖顯示當前快照中最大的對象。單擊工具欄上的柱狀圖,可以查看當前堆的類信息,包括類的對象數量、淺堆 (Shallow heap)、深堆 (Retained Heap).

淺堆表示一個對象結構所佔用內存的大小。深堆表示一個對象被回收後,可以真實釋放的內存大小。

1)支配樹 (The Dominator Tree)

列出了堆中最大的對象,第二層級的節點表示當被第一層級的節點所引用到的對象,當第一層級對象被回收時,這些對象也將被回收。

這個工具可以幫助我們定位對象間的引用情況,垃圾回收時候的引用依賴關係

2)Path to GC Roots

被 JVM 持有的對象,如當前運行的線程對象,被 systemclass loader 加載的對象被稱爲 GC Roots, 從一個對象到 GC Roots 的引用鏈被稱爲 Path to GC Roots。

通過分析 Path to GC Roots 可以找出 JAVA 的內存泄露問題,當程序不在訪問該對象時仍存在到該對象的引用路徑。

四、日誌分析

1. GC 日誌分析

1.1 GC 日誌詳細分析

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

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/usr/local/gc/gc.log -XX:+UseConcMarkSweepGC

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

?wx_fmt=png

上圖爲線上某應用在平穩運行狀態下的 GC 日誌截圖。

2017-12-29T18:25:22.753+0800: 73143.256: [GC2017-12-29T18:25:22.753+0800: 73143.257: [ParNew: 559782K->1000K(629120K), 0.0135760 secs] 825452K->266673K(2027264K), 0.0140300 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[2017-12-29T18:25:22.753+0800: 73143.256] : 自JVM啓動73143.256秒時發生本次GC. [ParNew: 559782K->1000K(629120K), 0.0135760 secs] : 對新生代進行的GC,使用ParNew收集器,559782K是新生代回收前的大小,1000K是新生代回收後大小,629120K是當前新生代分配的內存總大小, 0.0135760 secs表示本次新生代回收耗時 0.0135760秒 [825452K->266673K(2027264K), 0.0140300 secs]:825452K是回收堆內存大小,266673K是回收堆之後內存大小,2027264K是當前堆內存總大小,0.0140300 secs表示本次回收共耗時0.0140300秒 [Times: user=0.02 sys=0.00, real=0.02 secs] : 用戶態耗時0.02秒,系統態耗時0.00,實際耗時0.02

無論是 minor GC 或者是 Full GC, 我們主要關注 GC 回收實時耗時 , 如 real=0.02secs, 即 stop the world 時間,如果該時間過長,則嚴重影響應用性能。

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

初始標記 :在這個階段,需要虛擬機停頓正在執行的任務,官方的叫法 STW(Stop The Word)。這個過程從垃圾回收的 “ 根對象 “ 開始,只掃描到能夠和 “ 根對象 “ 直接關聯的對象,並作標記。

所以這個過程雖然暫停了整個 JVM,但是很快就完成了。

併發標記 :這個階段緊隨初始標記階段,在初始標記的基礎上繼續向下追溯標記。併發標記階段,應用程序的線程和併發標記的線程併發執行,所以用戶不會感受到停頓。

併發預清理 :併發預清理階段仍然是併發的。在這個階段,虛擬機查找在執行併發標記階段新進入老年代的對象 (可能會有一些對象從新生代晉升到老年代, 或者有一些對象被分配到老年代)。

通過重新掃描,減少下一個階段 “ 重新標記 “ 的工作,因爲下一個階段會 Stop The World。

重新標記 :這個階段會暫停虛擬機,收集器線程掃描在 CMS 堆中剩餘的對象。掃描從 “ 跟對象 “ 開始向下追溯,並處理對象關聯。

併發清理 :清理垃圾對象,這個階段收集器線程和應用程序線程併發執行。

併發重置 :這個階段,重置 CMS 收集器的數據結構,等待下一次垃圾回收。

cms 使得在整個收集的過程中只是很短的暫停應用的執行 , 可通過在 JVM 參數中設置 -XX:UseConcMarkSweepGC 來使用此收集器 , 不過此收集器僅用於 old 和 Perm(永生) 的對象收集。

CMS 減少了 stop the world 的次數,不可避免地讓整體 GC 的時間拉長了。

Full GC 的次數說的是 stop the world 的次數,所以一次 CMS 至少會讓 Full GC 的次數 +2,因爲 CMS Initial mark 和 remark 都會 stop the world,記做 2 次。而 CMS 可能失敗再引發一次 Full GC。

?wx_fmt=png

上圖爲線上某應用在進行 CMS GC 狀態下的 GC 日誌截圖。

?wx_fmt=png

如果你已掌握 CMS 的垃圾收集過程,那麼上面的 GC 日誌你應該很容易就能看的懂,這裏我就不詳細展開解釋說明了。

此外 CMS 進行垃圾回收時也有可能會發生失敗的情況。

異常情況有:

1)伴隨 prommotion failed, 然後 Full GC:

[prommotion failed:存活區內存不足,對象進入老年代,而此時老年代也仍然沒有內存容納對象,將導致一次 Full GC]

2)伴隨 concurrent mode failed,然後 Full GC:

[concurrent mode failed:CMS 回收速度慢,CMS 完成前,老年代已被佔滿,將導致一次 Full GC]

3)頻繁 CMS GC:

[內存吃緊,老年代長時間處於較滿的狀態]

2. 業務日誌

業務日誌除了關注系統異常與業務異常之外,還要關注服務執行耗時情況,耗時過長的服務調用如果沒有熔斷等機制,很容易導致應用性能下降或服務不可用,服務不可用很容易導致雪崩。

?wx_fmt=png

上面是某一接口的調用情況,雖然大部分調用沒有發生異常,但是執行耗時相對比較長。

grep ‘[0-9]{3,}ms’ *.log

找出調用耗時大於 3 位數的 dao 方法,把 3 改成 4 就是大於 4 位數

互聯網應用目前幾乎採用分佈式架構,但不限於服務框架、消息中間件、分佈式緩存、分佈式存儲等等。

那麼這些應用日誌如何聚合起來進行分析呢 ?

首先,你需要一套分佈式鏈路調用跟蹤系統,通過在系統線程上線文間透傳 traceId 和 rpcId,將所有日誌進行聚合,例如淘寶的鷹眼,spring cloud zipkin 等等。

五、案列分析

CPU 使用率高問題定位

?wx_fmt=png

按照定位流程首先排除了系統層面的問題。

利用 top -Hp 6814 輸出進程 ID 爲 6814 的所有線程 CPU 使用率情況,發現某個線程使用率比較高,有些異常。

printf '%x\n' 2304     #輸出線程 ID 的 16 進制 jstack pid | grep '0x900' -C 30 --color

輸出的日誌表明該線程一直處於與 mysql I/O 狀態:

?wx_fmt=jpeg

利用 jmap -dump:format=b,file=/usr/local/logs/gc/dump.hprof {pid} 以二進制輸出檔當前內存的堆情況,然後可以導入 MAT 等工具進行分析。

如下圖所示,點擊 MAT 的支配樹可以發現存在某個超大對象數組,實例對象數目多大 30 多萬個。

?wx_fmt=png

經過分析發現數組中每一個對象都是核心業務對象,我們的業務系統有一個定時任務線程會訪問數據庫某張業務表的所有記錄。

然後加載至內存然後進行處理因此內存吃緊,導致 CPU 突然飆升。發現該問題後,已對該方案進行重新設計。

發佈了58 篇原創文章 · 獲贊 12 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章