1. Java 應(yīng)用線上問(wèn)題排查思路、常用工具小結(jié)

        共 5779字,需瀏覽 12分鐘

         ·

        2020-08-03 04:40

        來(lái)源:https://ricstudio.top/archives/java-online-question-probe

        前言

        本文總結(jié)了一些常見(jiàn)的線上應(yīng)急現(xiàn)象和對(duì)應(yīng)排查步驟和工具。分享的主要目的是想讓對(duì)線上問(wèn)題接觸少的同學(xué)有個(gè)預(yù)先認(rèn)知,免得在遇到實(shí)際問(wèn)題時(shí)手忙腳亂。畢竟作者自己也是從手忙腳亂時(shí)走過(guò)來(lái)的。

        只不過(guò)這里先提示一下。在線上應(yīng)急過(guò)程中要記住,只有一個(gè)總體目標(biāo):盡快恢復(fù)服務(wù),消除影響。不管處于應(yīng)急的哪個(gè)階段,我們首先必須想到的是恢復(fù)問(wèn)題,恢復(fù)問(wèn)題不一定能夠定位問(wèn)題,也不一定有完美的解決方案,也許是通過(guò)經(jīng)驗(yàn)判斷,也許是預(yù)設(shè)開(kāi)關(guān)等,但都可能讓我們達(dá)到快速恢復(fù)的目的,然后保留部分現(xiàn)場(chǎng),再去定位問(wèn)題、解決問(wèn)題和復(fù)盤(pán)。

        在大多數(shù)情況下,我們都是先優(yōu)先恢復(fù)服務(wù),保留下當(dāng)時(shí)的異常信息(內(nèi)存dump、線程dump、gc log等等,在緊急情況下甚至可以不用保留,等到事后去復(fù)現(xiàn)),等到服務(wù)正常,再去復(fù)盤(pán)問(wèn)題。

        好,現(xiàn)在讓我們進(jìn)入正題吧。

        常見(jiàn)現(xiàn)象:CPU 利用率高/飆升

        場(chǎng)景預(yù)設(shè):

        監(jiān)控系統(tǒng)突然告警,提示服務(wù)器負(fù)載異常。

        預(yù)先說(shuō)明:

        CPU飆升只是一種現(xiàn)象,其中具體的問(wèn)題可能有很多種,這里只是借這個(gè)現(xiàn)象切入。

        注:CPU使用率是衡量系統(tǒng)繁忙程度的重要指標(biāo)。但是CPU使用率的安全閾值是相對(duì)的,取決于你的系統(tǒng)的IO密集型還是計(jì)算密集型。一般計(jì)算密集型應(yīng)用CPU使用率偏高load偏低,IO密集型相反。

        常見(jiàn)原因:

        • 頻繁 gc
        • 死循環(huán)、線程阻塞、io wait...etc

        模擬

        這里為了演示,用一個(gè)最簡(jiǎn)單的死循環(huán)來(lái)模擬CPU飆升的場(chǎng)景,下面是模擬代碼,

        在一個(gè)最簡(jiǎn)單的SpringBoot Web 項(xiàng)目中增加CpuReaper這個(gè)類,

        /**
        ?*?模擬?cpu?飆升場(chǎng)景
        ?*?@author?Richard_yyf
        ?*/

        @Component
        public?class?CpuReaper?{

        ????@PostConstruct
        ????public?void?cpuReaper()?{
        ????????int?num?=?0;
        ????????long?start?=?System.currentTimeMillis()?/?1000;
        ????????while?(true)?{
        ????????????num?=?num?+?1;
        ????????????if?(num?==?Integer.MAX_VALUE)?{
        ????????????????System.out.println("reset");
        ????????????????num?=?0;
        ????????????}
        ????????????if?((System.currentTimeMillis()?/?1000)?-?start?>?1000)?{
        ????????????????return;
        ????????????}
        ????????}
        ????}
        }

        打包成jar之后,在服務(wù)器上運(yùn)行。java -jar cpu-reaper.jar &

        第一步:定位出問(wèn)題的線程

        方法 a: 傳統(tǒng)的方法

        1. top 定位CPU 最高的進(jìn)程

          執(zhí)行top命令,查看所有進(jìn)程占系統(tǒng)CPU的排序,定位是哪個(gè)進(jìn)程搞的鬼。在本例中就是咱們的java進(jìn)程。PID那一列就是進(jìn)程號(hào)。(對(duì)指示符含義不清楚的見(jiàn)【附錄】)

        2. top -Hp pid 定位使用 CPU 最高的線程

        3. printf '0x%x' tid 線程 id 轉(zhuǎn)化 16 進(jìn)制

        >?printf?'0x%x'?12817>?0x3211
        1. jstack pid | grep tid 找到線程堆棧
        >?jstack?12816?|?grep?0x3211?-A?30

        方法 b: show-busy-java-threads

        這個(gè)腳本來(lái)自于github上一個(gè)開(kāi)源項(xiàng)目,項(xiàng)目提供了很多有用的腳本,show-busy-java-threads就是其中的一個(gè)。使用這個(gè)腳本,可以直接簡(jiǎn)化方法A中的繁瑣步驟。如下,

        >?wget?--no-check-certificate?https://raw.github.com/oldratlee/useful-scripts/release-2.x/bin/show-busy-java-threads
        >?chmod?+x?show-busy-java-threads
        >?./show-busy-java-threads
        show-busy-java-threads
        #?從所有運(yùn)行的Java進(jìn)程中找出最消耗CPU的線程(缺省5個(gè)),打印出其線程棧
        #?缺省會(huì)自動(dòng)從所有的Java進(jìn)程中找出最消耗CPU的線程,這樣用更方便
        #?當(dāng)然你可以手動(dòng)指定要分析的Java進(jìn)程Id,以保證只會(huì)顯示你關(guān)心的那個(gè)Java進(jìn)程的信息
        show-busy-java-threads?-p?<指定的java進(jìn)程id>

        show-busy-java-threads?-c?<要顯示的線程棧數(shù)>

        方法 c: arthas thread

        阿里開(kāi)源的arthas現(xiàn)在已經(jīng)幾乎包攬了我們線上排查問(wèn)題的工作,提供了一個(gè)很完整的工具集。在這個(gè)場(chǎng)景中,也只需要一個(gè)thread -n命令即可。

        >?curl?-O?https://arthas.gitee.io/arthas-boot.jar?#?下載

        要注意的是,arthas的cpu占比,和前面兩種cpu占比統(tǒng)計(jì)方式不同。前面兩種針對(duì)的是Java進(jìn)程啟動(dòng)開(kāi)始到現(xiàn)在的cpu占比情況,arthas這種是一段采樣間隔內(nèi),當(dāng)前JVM里各個(gè)線程所占用的cpu時(shí)間占總cpu時(shí)間的百分比。

        具體見(jiàn)官網(wǎng):https://alibaba.github.io/arthas/thread.html

        后續(xù)

        通過(guò)第一步,找出有問(wèn)題的代碼之后,觀察到線程棧之后。我們就要根據(jù)具體問(wèn)題來(lái)具體分析。這里舉幾個(gè)例子。

        情況一:發(fā)現(xiàn)使用CPU最高的都是GC 線程。

        GC?task?thread#0?(ParallelGC)"?os_prio=0?tid=0x00007fd99001f800?nid=0x779?runnableGC?task?thread#1?(ParallelGC)"?os_prio=0?tid=0x00007fd990021800?nid=0x77a?runnable?GC?task?thread#2?(ParallelGC)"?os_prio=0?tid=0x00007fd990023000?nid=0x77b?runnable?GC?task?thread#3?(ParallelGC)"?os_prio=0?tid=0x00007fd990025000?nid=0x77c?runnabl

        gc 排查的內(nèi)容較多,所以我決定在后面單獨(dú)列一節(jié)講述。

        情況二:發(fā)現(xiàn)使用CPU最高的是業(yè)務(wù)線程

        • io wait
          • 比如此例中,就是因?yàn)榇疟P(pán)空間不夠?qū)е碌膇o阻塞
        • 等待內(nèi)核態(tài)鎖,如 synchronized
          • jstack -l pid | grep BLOCKED 查看阻塞態(tài)線程堆棧
          • dump 線程棧,分析線程持鎖情況。
          • arthas提供了thread -b,可以找出當(dāng)前阻塞其他線程的線程。針對(duì) synchronized 情況

        常見(jiàn)現(xiàn)象:頻繁 GC

        1. 回顧GC流程

        在了解下面內(nèi)容之前,請(qǐng)先花點(diǎn)時(shí)間回顧一下GC的整個(gè)流程。


        接前面的內(nèi)容,這個(gè)情況下,我們自然而然想到去查看gc 的具體情況。

        • 方法a : 查看gc 日志
        • 方法b : jstat -gcutil 進(jìn)程號(hào) 統(tǒng)計(jì)間隔毫秒 統(tǒng)計(jì)次數(shù)(缺省代表一致統(tǒng)計(jì)
        • 方法c : 如果所在公司有對(duì)應(yīng)用進(jìn)行監(jiān)控的組件當(dāng)然更方便(比如Prometheus + Grafana)

        這里對(duì)開(kāi)啟 gc log 進(jìn)行補(bǔ)充說(shuō)明。一個(gè)常常被討論的問(wèn)題(慣性思維)是在生產(chǎn)環(huán)境中GC日志是否應(yīng)該開(kāi)啟。因?yàn)樗a(chǎn)生的開(kāi)銷通常都非常有限,因此我的答案是需要開(kāi)啟。但并不一定在啟動(dòng)JVM時(shí)就必須指定GC日志參數(shù)。

        HotSpot JVM有一類特別的參數(shù)叫做可管理的參數(shù)。對(duì)于這些參數(shù),可以在運(yùn)行時(shí)修改他們的值。我們這里所討論的所有參數(shù)以及以“PrintGC”開(kāi)頭的參數(shù)都是可管理的參數(shù)。這樣在任何時(shí)候我們都可以開(kāi)啟或是關(guān)閉GC日志。比如我們可以使用JDK自帶的jinfo工具來(lái)設(shè)置這些參數(shù),或者是通過(guò)JMX客戶端調(diào)用HotSpotDiagnostic MXBean的setVMOption方法來(lái)設(shè)置這些參數(shù)。

        這里再次大贊arthas??,它提供的vmoption命令可以直接查看,更新VM診斷相關(guān)的參數(shù)。

        獲取到gc日志之后,可以上傳到GC easy幫助分析,得到可視化的圖表分析結(jié)果。

        2. GC 原因及定位

        prommotion failed

        從S區(qū)晉升的對(duì)象在老年代也放不下導(dǎo)致 FullGC(fgc 回收無(wú)效則拋 OOM)。

        可能原因:

        • survivor 區(qū)太小,對(duì)象過(guò)早進(jìn)入老年代

          查看 SurvivorRatio 參數(shù)

        • 大對(duì)象分配,沒(méi)有足夠的內(nèi)存

          dump 堆,profiler/MAT 分析對(duì)象占用情況

        • old 區(qū)存在大量對(duì)象

          dump 堆,profiler/MAT 分析對(duì)象占用情況

        你也可以從full GC 的效果來(lái)推斷問(wèn)題,正常情況下,一次full GC應(yīng)該會(huì)回收大量?jī)?nèi)存,所以 正常的堆內(nèi)存曲線應(yīng)該是呈鋸齒形。如果你發(fā)現(xiàn)full gc 之后堆內(nèi)存幾乎沒(méi)有下降,那么可以推斷:**堆中有大量不能回收的對(duì)象且在不停膨脹,使堆的使用占比超過(guò)full GC的觸發(fā)閾值,但又回收不掉,導(dǎo)致full GC一直執(zhí)行。換句話來(lái)說(shuō),可能是內(nèi)存泄露了。

        一般來(lái)說(shuō),GC相關(guān)的異常推斷都需要涉及到內(nèi)存分析,使用jmap之類的工具dump出內(nèi)存快照(或者 Arthas的heapdump)命令,然后使用MAT、JProfiler、JVisualVM等可視化內(nèi)存分析工具。

        至于內(nèi)存分析之后的步驟,就需要小伙伴們根據(jù)具體問(wèn)題具體分析啦。

        常見(jiàn)現(xiàn)象:線程池異常

        場(chǎng)景預(yù)設(shè):

        業(yè)務(wù)監(jiān)控突然告警,或者外部反饋提示大量請(qǐng)求執(zhí)行失敗。

        異常說(shuō)明:

        Java 線程池以有界隊(duì)列的線程池為例,當(dāng)新任務(wù)提交時(shí),如果運(yùn)行的線程少于 corePoolSize,則創(chuàng)建新線程來(lái)處理請(qǐng)求。如果正在運(yùn)行的線程數(shù)等于 corePoolSize 時(shí),則新任務(wù)被添加到隊(duì)列中,直到隊(duì)列滿。當(dāng)隊(duì)列滿了后,會(huì)繼續(xù)開(kāi)辟新線程來(lái)處理任務(wù),但不超過(guò) maximumPoolSize。當(dāng)任務(wù)隊(duì)列滿了并且已開(kāi)辟了最大線程數(shù),此時(shí)又來(lái)了新任務(wù),ThreadPoolExecutor 會(huì)拒絕服務(wù)。

        常見(jiàn)問(wèn)題和原因

        這種線程池異常,一般可以通過(guò)開(kāi)發(fā)查看日志查出原因,有以下幾種原因:

        1. 下游服務(wù) 響應(yīng)時(shí)間(RT)過(guò)長(zhǎng)

          這種情況有可能是因?yàn)橄掠畏?wù)異常導(dǎo)致的,作為消費(fèi)者我們要設(shè)置合適的超時(shí)時(shí)間和熔斷降級(jí)機(jī)制。

          另外針對(duì)這種情況,一般都要有對(duì)應(yīng)的監(jiān)控機(jī)制:比如日志監(jiān)控、metrics監(jiān)控告警等,不要等到目標(biāo)用戶感覺(jué)到異常,從外部反映進(jìn)來(lái)問(wèn)題才去看日志查。

        2. 數(shù)據(jù)庫(kù)慢 sql 或者數(shù)據(jù)庫(kù)死鎖

        查看日志中相關(guān)的關(guān)鍵詞。

        1. Java 代碼死鎖

          jstack –l pid | grep -i –E 'BLOCKED | deadlock'

        四、常見(jiàn)問(wèn)題恢復(fù)

        這一部分內(nèi)容參考自此篇文章

        對(duì)于上文提到的一些問(wèn)題,這里總結(jié)了一些恢復(fù)的方法。

        五、Arthas

        這里還是想單獨(dú)用一節(jié)安利一下Arthas這個(gè)工具。

        Arthas 是阿里巴巴開(kāi)源的Java 診斷工具,基于 Java Agent 方式,使用 Instrumentation 方式修改字節(jié)碼方式進(jìn)行 Java 應(yīng)用診斷。

        • dashboard :系統(tǒng)實(shí)時(shí)數(shù)據(jù)面板, 可查看線程,內(nèi)存,gc 等信息
        • thread :查看當(dāng)前線程信息,查看線程的堆棧,如查看最繁忙的前 n 線程
        • getstatic:獲取靜態(tài)屬性值,如 getstatic className attrName 可用于查看線上開(kāi)關(guān)真實(shí)值
        • sc:查看 jvm 已加載類信息,可用于排查 jar 包沖突
        • sm:查看 jvm 已加載類的方法信息
        • jad:反編譯 jvm 加載類信息,排查代碼邏輯沒(méi)執(zhí)行原因
        • logger:查看logger信息,更新logger level
        • watch:觀測(cè)方法執(zhí)行數(shù)據(jù),包含出參、入?yún)?、異常?/section>
        • trace:方法內(nèi)部調(diào)用時(shí)長(zhǎng),并輸出每個(gè)節(jié)點(diǎn)的耗時(shí),用于性能分析
        • tt:用于記錄方法,并做回放

        以上內(nèi)容節(jié)選自Arthas官方文檔。

        另外,Arthas里的 還集成了 ognl 這個(gè)輕量級(jí)的表達(dá)式引擎,通過(guò)ognl,你可以用arthas 實(shí)現(xiàn)很多的“騷”操作。

        其他的這里就不多說(shuō)了,感興趣的可以去看看arthas的官方文檔、github issue。

        六、涉及工具

        再說(shuō)下一些工具。

        • Arthas
        • useful-scripts
        • GC easy
        • Smart Java thread dump analyzer - thread dump analysis in seconds
        • PerfMa - Java虛擬機(jī)參數(shù)/線程dump/內(nèi)存dump分析
        • Linux 命令
        • Java N 板斧
        • MAT、JProfiler...等可視化內(nèi)存分析工具

        參考

        • https://developer.aliyun.com/article/757655
        • Arthas 3.2.0 文檔
        • 《分布式服務(wù)架構(gòu):原理、設(shè)計(jì)與實(shí)戰(zhàn)》

        附錄

        top 命令顯示的指示符的含義

        指示符含義
        PID進(jìn)程id
        USER進(jìn)程所有者
        PR進(jìn)程優(yōu)先級(jí)
        NInice值。負(fù)值表示高優(yōu)先級(jí),正值表示低優(yōu)先級(jí)
        VIRT進(jìn)程使用的虛擬內(nèi)存總量,單位kb。VIRT=SWAP+RES
        RES進(jìn)程使用的、未被換出的物理內(nèi)存大小,單位kb。RES=CODE+DATA
        SHR共享內(nèi)存大小,單位kb
        S進(jìn)程狀態(tài)。D=不可中斷的睡眠狀態(tài) R=運(yùn)行 S=睡眠 T=跟蹤/停止 Z=僵尸進(jìn)程
        %CPU上次更新到現(xiàn)在的CPU時(shí)間占用百分比
        %MEM進(jìn)程使用的物理內(nèi)存百分比
        TIME+進(jìn)程使用的CPU時(shí)間總計(jì),單位1/100秒
        COMMAND進(jìn)程名稱(命令名/命令行)
        瀏覽 29
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 久热久草 | 91人人摸人人操 | 国产黄a三级三级三级看三级黑人 | 差差漫画线上入口页面弹窗 | 亚洲欧美制服 |