再棘手的線上故障,都逃不過這些高效排查套路
一、前言
線上問題排查相比于coding,是一個(gè)低頻的工作,很多人不會(huì)經(jīng)常遇到。一旦需要進(jìn)行問題排查的時(shí)候,往往是重要且緊急的,因此問題排查的效率,就顯得尤為重要。有些線上問題,比較直觀,比如磁盤使用率高、網(wǎng)絡(luò)流量高這種,借助合適的工具很快能定位到原因;但對(duì)于一些復(fù)雜的問題,如系統(tǒng)Load高、RSS占用高、內(nèi)存溢出等,需要結(jié)合多方面的數(shù)據(jù)才能定位到原因。這時(shí)候,需要有正確的解題思路,并輔以合適的工具,才能高效地解決問題。
目前業(yè)界排查問題的優(yōu)秀工具還是挺多的,比如國內(nèi)阿里開源的Arthas、PerfMa開源的為終結(jié)性能問題而生的xPocket,Java官方的JMC(JDK Mission Control)、Eclipse的MAT(Memory Analyzer Tooling),以及我一直很推崇的神器Async-Profiler。上述只是列舉了一些比較流行的開源工具,商業(yè)工具如jProfiler、YourKit等也都建立了穩(wěn)定的用戶群體,這些工具功能各有差異。當(dāng)然這不是本文描述的重點(diǎn),就不詳細(xì)展開了。
在對(duì)這些工具進(jìn)行橫向?qū)Ρ葧r(shí)我們發(fā)現(xiàn),他們的目標(biāo)都是為了解決一些特定的問題,如果我們有清晰的問題排查思路,結(jié)合這些工具,可以很快解決問題。
而對(duì)于一些復(fù)雜場景,尤其是一些陌生的復(fù)雜問題,在沒有頭緒的情況下,縱然有各種神兵利器,也無計(jì)可施。線上問題排查猶如開車,老司機(jī)駕輕就熟,新手則手忙腳亂。當(dāng)然如果新手有老司機(jī)加以指點(diǎn),也可能很快地解決問題。
但問題是,這種老司機(jī)并不常見,也不可能時(shí)刻都能幫你。我們可以去網(wǎng)上查閱其他人總結(jié)的問題排查套路,再結(jié)合我們自己的場景,去嘗試解決問題,我也是經(jīng)常這么干的。但這種方式效率依然不高,原因有三個(gè):
- 信息檢索的成本:我們需要花時(shí)間去翻閱資料,去跟自己的場景匹配以判斷是否適合自己;
- 試錯(cuò)的成本:有些資料不適合我們的場景,我們按照資料去嘗試,有可能被帶溝里去,浪費(fèi)時(shí)間;
- 問題排查需要借助于一些第三方工具,而這些工具在生產(chǎn)環(huán)境需要安裝、配置和使用,也需要花較多的時(shí)間成本。
針對(duì)線上問題排查的特點(diǎn)和現(xiàn)狀,我們是否可以構(gòu)建一個(gè)系統(tǒng),這個(gè)系統(tǒng)會(huì)針對(duì)各種線上問題的排查形成一個(gè)知識(shí)(套路)庫,針對(duì)每一種問題,都有對(duì)應(yīng)的套路和自動(dòng)化工具幫助我們?nèi)ザㄎ粏栴}。本文將結(jié)合一個(gè)比較有代表性的線上問題的排查過程,來探討這種方式的可行性。
二、問題排查的套路化
本章將以RSS占用高為例來對(duì)問題排查的套路化進(jìn)行說明。RSS占用高是很多人遇到過的問題,這個(gè)問題涉及的因素比較多,比較有代表性。當(dāng)然在開啟了Swap的運(yùn)行環(huán)境中,Swap高也是RSS高的一種表象,殊途同歸。
RSS是Resident Set Size(常駐內(nèi)存大小)的縮寫,用于表示進(jìn)程使用了多少內(nèi)存(RAM中的物理內(nèi)存)。如果我們遇到進(jìn)程RSS接近服務(wù)器的物理內(nèi)存,那就意味著你需要關(guān)注應(yīng)用的健康程度了,這意味著應(yīng)用后面很有可能出現(xiàn)OOM的問題,比如進(jìn)程被OOM killer殺死,或者容器重啟,或者因使用Swap而速度變慢。
針對(duì)RSS高的問題,首先我們需要知道的是,Java進(jìn)程消耗的內(nèi)存絕不僅僅是你設(shè)置的Xmx或堆內(nèi)存的用量這么簡單,Java進(jìn)程占用的內(nèi)存主要分為2大部分:on-heap(堆內(nèi)內(nèi)存)和off-heap(堆外內(nèi)存)。而堆外內(nèi)存又包含JVM自身消耗的內(nèi)存、JVM外的內(nèi)存。所以,后續(xù)的排查思路我們也是按照堆內(nèi)內(nèi)存、JVM內(nèi)存、JVM外內(nèi)存3個(gè)方向來順序展開。
1. 堆內(nèi)存是否太大
首先要確認(rèn)一下Java應(yīng)用的堆內(nèi)存是否太大,因?yàn)镴VM自身也會(huì)消耗一些內(nèi)存,所以你至少需要預(yù)留出部分內(nèi)存存給JVM使用。如果應(yīng)用涉及較多的網(wǎng)絡(luò)通信,那還需要預(yù)留一些內(nèi)存給堆外使用,所以一般來說你的堆內(nèi)存最多為服務(wù)器物理內(nèi)存的75%(經(jīng)驗(yàn)值,需依據(jù)應(yīng)用自身特點(diǎn)調(diào)整),如4G內(nèi)存服務(wù)器,那么堆內(nèi)存最大為3G。
堆內(nèi)存用量的查看手段非常多,相信各個(gè)公司的基礎(chǔ)架構(gòu)團(tuán)隊(duì)都提供了可視化的監(jiān)控手段,當(dāng)然也可以通過原生命令jcmd GC.heap_info查看,如圖1所示:
如果Java進(jìn)程的堆內(nèi)存用量已接近或超過物理內(nèi)存的75%,那么基本可以確定堆內(nèi)存用量過大。這時(shí)可以調(diào)小Xmx來控制堆內(nèi)存用量。如果Xmx不能減小,可以通過dump堆內(nèi)存+MAT或JFR(Java Flight Recorder)+ JMC(JDK Mission Control)來分析內(nèi)存占用/分配情況,通過程序調(diào)優(yōu)來減少堆內(nèi)存用量。
如果到此RSS占用呈穩(wěn)定趨勢,我們就可以告一段落了,否則要繼續(xù)后面的步驟。
2. 是否存在大量ARENA區(qū)
如果堆內(nèi)存不大,那么繼續(xù)排查非堆內(nèi)存。首先去看一下ARENA區(qū),在高并發(fā)的應(yīng)用中,往往ARENA區(qū)占用的內(nèi)存會(huì)比較多。為什么先看ARENA區(qū)的內(nèi)存占用呢?是因?yàn)檫@個(gè)步驟是不需要重啟JVM進(jìn)程就可以完成的。
接下來我們直接進(jìn)入排查問題環(huán)節(jié)。執(zhí)行如下命令:
- sudo -u pmap -x |sort -gr -k2 |less
如果存在大量大小為65536或60000左右的內(nèi)存區(qū)域,則很大可能是ARENA區(qū)域占用了太多的內(nèi)存,如圖2所示:
這種情況下,最簡單粗暴的辦法是在JVM啟動(dòng)參數(shù)中增加配置:
- export MALLOC_ARENA_MAX=1
需要注意的是,上述的數(shù)值只能是1,其他大于1的數(shù)值經(jīng)實(shí)踐證明是無法控制ARENA數(shù)量的。
3. 非堆內(nèi)存是否開銷過大
如果前面2個(gè)步驟過后都沒有發(fā)現(xiàn)問題,還有很多內(nèi)存你不知道消耗在哪里了,那么我們開始第3步:開啟Native Memory Tracking。前面說過,Java應(yīng)用的執(zhí)行,JVM自身也需要消耗一些內(nèi)存的,通過開啟Native Memory Tracking,我們就能知道JVM自身消耗了多少內(nèi)存。
書歸正傳,通過修改JVM參數(shù)并重啟Java進(jìn)程開啟NativeMemory Tracking:
- -XX:NativeMemoryTracking=detail
進(jìn)程重啟后,可以通過NMT的一些子命令(summary/detail/baseline/diff)查看Native Memory的占用情況:
- sudo -u jcmd VM.native_memory detail
圖3是在使用baseline建立了基線的情況下用detail.diff看到的各內(nèi)存區(qū)的變化情況:
通過上圖,可以看到JVM各個(gè)區(qū)域所使用的內(nèi)存大小,主要包含了Java Heap、Class、Thread、Code、GC、Compiler、Internal、Other、Symbol等,各部分作用如下:
- Class:加載的類與方法信息,其實(shí)就是 metaspace,包含兩部分:一是 metadata,被-XX:MaxMetaspaceSize限制最大大小,另外是 class space,被-XX:CompressedClassSpaceSize限制最大大小;
- Thread:線程與線程棧占用內(nèi)存,每個(gè)線程棧占用大小受-Xss限制,但是總大小沒有限制。在x64的JVM中,Xss默認(rèn)為1024K,所以如果你的應(yīng)用開啟了1000個(gè)線程,那么這個(gè)Thread區(qū)占用將是1024M,所以一般我們會(huì)把Xss設(shè)置為256K即滿足要求;
- Code:JIT 即時(shí)編譯后(C1C2 編譯器優(yōu)化)的代碼占用內(nèi)存,受-XX:ReservedCodeCacheSize限制;
- GC:垃圾回收占用內(nèi)存,例如垃圾回收需要的 CardTable,標(biāo)記數(shù),區(qū)域劃分記錄,還有標(biāo)記 GC Root 等等,都需要內(nèi)存。這個(gè)不受限制,一般不會(huì)很大,但也有例外,圖3是27G的堆內(nèi)存,使用G1垃圾回收器,你能看到GC區(qū)居然占用了3.8G的內(nèi)存;
- Compiler:C1 C2 編譯器本身的代碼和標(biāo)記占用的內(nèi)存,這個(gè)不受限制,一般不會(huì)很大;
- Internal:命令行解析,JVMTI 使用的內(nèi)存,這個(gè)不受限制,一般不會(huì)很大;
- Symbol: 常量池占用的大小,字符串常量池受-XX:StringTableSize個(gè)數(shù)限制,總內(nèi)存大小不受限制;
我們需要需要注意的是Class、Thread、GC幾個(gè)區(qū)域的大小。圖4是各種JVM垃圾回收器消耗內(nèi)存的比例,注意這部分內(nèi)存是堆內(nèi)存之外的:
實(shí)踐證明,G1的內(nèi)存開銷甚至能占到堆內(nèi)存的20%,當(dāng)然這不是本文要討論的內(nèi)容,感興趣的讀者可以去閱讀《JVM G1源碼分析和調(diào)優(yōu)》一書查看相關(guān)內(nèi)容。
針對(duì)上面的各個(gè)區(qū)域大小做加法,看一下是否接近于RSS的大小,如果是,恭喜你可以到此結(jié)束了。后續(xù)你需要做的就是針對(duì)內(nèi)存占用比較大的JVM區(qū)去做優(yōu)化,這里就不詳細(xì)介紹了。
如果不是,很遺憾,你進(jìn)入到了最難啃的環(huán)節(jié),繼續(xù)往下看吧。
注:開啟NativeMemoryTracking會(huì)造成5%的性能下降,用完記得修改JVM參數(shù)并重啟永久關(guān)閉,或者可以通過以下命令臨時(shí)關(guān)閉:jcmd vm.native_memory stop 。
4. 堆外內(nèi)存是否用量太多
堆外內(nèi)存也是比較容易被忽略的一個(gè)區(qū)域,尤其是網(wǎng)絡(luò)通信非常頻繁的應(yīng)用,這種應(yīng)用往往大量使用Java NIO,而NIO為了提高效率,往往會(huì)申請(qǐng)很多的堆外內(nèi)存。確認(rèn)這個(gè)區(qū)域用量是否過大,最直接的方法是先查看是否是DirectByteBuffer或者M(jìn)appedByteBuffer使用了較多的堆外內(nèi)存。
如果你的服務(wù)器已經(jīng)開啟了遠(yuǎn)程JMX,你可以通過ops提供的jmx查詢工具去查詢,也可以通過jdk自帶的工具(比如jconsole、jvisualvm)查詢,如圖5所示:
如果未開啟遠(yuǎn)程JMX,可以通過jmxterm(https://docs.cyclopsgroup.org/jmxterm)工具在本地模式下查詢以下兩項(xiàng)內(nèi)容確認(rèn)用量:
- java.nio:name=direct,type=BufferPool
- java.nio:name=mapped,type=BufferPool
如果確認(rèn)上述堆外內(nèi)存使用過多,那么可以通過在jvm參數(shù)中設(shè)置-XX:MaxDirectMemorySize這個(gè)參數(shù)控制一下,因?yàn)橥ㄟ^DirectByteBuffer分配的堆外內(nèi)存,默認(rèn)是不會(huì)控制這個(gè)區(qū)域的內(nèi)存用量的。
如果上述內(nèi)存用量不大,那我們就需要祭出終極殺器jemalloc來做進(jìn)一步分析了。這里涉及的內(nèi)容比較多,受限于文章篇幅限制,就不展開描述了。
通過jemalloc的收集到的數(shù)據(jù),我們基本能夠定位到堆外內(nèi)存問題的原因。
5. 總結(jié)
上述的4個(gè)步驟,基本能夠解決大多數(shù)的RSS占用高的問題了。當(dāng)然事無絕對(duì),沒有一種藥是包治百病的。我們追求的基本目標(biāo)是,通過問題排查的套路化,幫助工程師理清思路,少走彎路,以提高問題排查的效率。
三、問題排查的工具化
復(fù)盤一下上述的問題排查過程,我們用到了很多的命令和第三方工具,整個(gè)過程還是工程師驅(qū)動(dòng)命令行和工具。如果對(duì)命令參數(shù)不熟悉,或本地沒有安裝相應(yīng)的工具,那這種套路化的教程也只能在一定程度上提高效率。在這個(gè)套路的基礎(chǔ)上,我們是否可以轉(zhuǎn)變思路,以工具為主,工程師輔助,來提高排查問題的效率呢?讓我們來嘗試一下。
1. 流程梳理
先來梳理工具的執(zhí)行流程以及每個(gè)步驟需要做的事情,與第二節(jié)保持一致性,本節(jié)也劃分成4個(gè)子步驟。
1)確認(rèn)堆內(nèi)存是否太大
第一步要做的事情比較多,梳理如下:
- Java進(jìn)程pid:使用jps -v列出java進(jìn)程列表,由用戶選擇具體的進(jìn)程;
- 獲取運(yùn)行環(huán)境的物理內(nèi)存和剩余內(nèi)存(free -m)
- Java進(jìn)程的堆內(nèi)存用量(jcmd GC.heap_info)
- Java進(jìn)程GC情況(jstat-gcutil);
獲取到上述信息后,判斷堆內(nèi)存是否太大。
2)是否存在大量ARENA區(qū)
通過pmap命令獲取內(nèi)存分配列表,輔以awk命令提取內(nèi)存信息,據(jù)此判斷是否存在大量ARENA區(qū)。
3)非堆內(nèi)存是否開銷過大
此步驟需要在JVM啟動(dòng)腳本中增加啟動(dòng)參數(shù)并重新啟動(dòng)進(jìn)程,對(duì)于標(biāo)準(zhǔn)化的運(yùn)行環(huán)境來說,在知道了啟動(dòng)腳本位置和啟動(dòng)命令的情況下,是可以通過工具來完成參數(shù)的修改和進(jìn)程啟動(dòng)。如果不能知道啟動(dòng)腳本所在位置,我們可以復(fù)制當(dāng)前進(jìn)程的JVM參數(shù)來完成進(jìn)程的啟動(dòng)。
當(dāng)JVM進(jìn)程啟動(dòng)完成,再次進(jìn)入工具,我們就能借助于Native Memory Tracking的結(jié)果來判斷當(dāng)前環(huán)節(jié)是否存在問題。
4)堆外內(nèi)存是否用量太多
此步驟的自動(dòng)執(zhí)行,需要安裝第三方工具如jxmterm、jemalloc。在生產(chǎn)環(huán)境訪問外網(wǎng)受限的情況下,可以通過搭建內(nèi)網(wǎng)資源服務(wù)器的方式來解決這個(gè)問題。通過一鍵安裝腳本,我們能快速完成所依賴工具的安裝和配置,剩下的就是讓工具來收集和分析數(shù)據(jù)定位問題了。
2. 工具實(shí)現(xiàn)
目前公司內(nèi)部很多運(yùn)維工具,都是采用C+B/S的方式實(shí)現(xiàn),這種方式工程師不需要申請(qǐng)目標(biāo)服務(wù)器權(quán)限就能夠?qū)崿F(xiàn)很多運(yùn)維操作。但這種實(shí)現(xiàn)方式比較復(fù)雜,而我們的工具是帶有實(shí)驗(yàn)性質(zhì),所以暫時(shí)使用shell+工具包的方式實(shí)現(xiàn),即使用shell腳本將主流程串起來,各節(jié)點(diǎn)使用的工具如果有缺失,yum能安裝使用yum安裝,yum不能安裝的則提前下載內(nèi)置到工具包中。
所有的準(zhǔn)備工作完成后,編寫腳本的工作就相對(duì)簡單了,當(dāng)然這需要用到很多的shell和linux、java命令,此處就不贅述了。腳本的最終運(yùn)行效果如下:
四、總結(jié)
通過針對(duì)RSS占用高問題的排查套路和排查工具的梳理,我們實(shí)現(xiàn)了一個(gè)簡單的問題快速排查腳本。當(dāng)然在這個(gè)過程中可以發(fā)現(xiàn),很多問題的排查,都可以使用類似的思路來工具化,日積月累,就形成了一個(gè)問題排查的工具包。
以內(nèi)存問題排查舉例,我們積累了以下的快速工具,如圖:
這只是工具包的一部分,針對(duì)CPU、磁盤、網(wǎng)絡(luò)、GC等問題,借助于Arthas、Async-Profiler等優(yōu)秀的開源工具,我們都積累了很多快速工具,期望能幫工程師提高問題排查的效率。
如前面所講,這種完全基于shell的方式,由于需要登錄到目標(biāo)服務(wù)器上操作,多數(shù)功能還需要有sudo權(quán)限,這有些許的不方便。另外,某些公司生產(chǎn)環(huán)境嚴(yán)格受限,那shell的方式就無法使用了。所以在此基礎(chǔ)上,可以擴(kuò)展成Client+Server+Browser的模式,讓工程師在不登錄到服務(wù)器的情況下,就能完成問題的排查。
到此,本文的內(nèi)容就結(jié)束了,但我們的工具還在不斷地積累中,在此也歡迎感興趣的同學(xué)幫我們提供場景,我們不斷豐富這個(gè)工具庫。同時(shí),受限于作者水平,文中內(nèi)容難免有不當(dāng)之處,也歡迎提出意見和建議。