高效線上問題排查——套路化和工具化

作者簡介

 

豐富,攜程高級技術專家,在性能優化、問題排查方面有較多的研究。



一、前言


線上問題排查相比於coding,是一個低頻的工作,很多人不會經常遇到。一旦需要進行問題排查的時候,往往是重要且緊急的,因此問題排查的效率,就顯得尤爲重要。有些線上問題,比較直觀,比如磁盤使用率高、網絡流量高這種,藉助合適的工具很快能定位到原因;但對於一些複雜的問題,如系統Load高、RSS佔用高、內存溢出等,需要結合多方面的數據才能定位到原因。這時候,需要有正確的解題思路,並輔以合適的工具,才能高效地解決問題。


目前業界排查問題的優秀工具還是挺多的,比如國內阿里開源的Arthas、PerfMa開源的爲終結性能問題而生的xPocket,Java官方的JMC(JDK Mission Control)、Eclipse的MAT(Memory Analyzer Tooling),以及我一直很推崇的神器Async-Profiler。上述只是列舉了一些比較流行的開源工具,商業工具如jProfiler、YourKit等也都建立了穩定的用戶羣體這些工具功能各有差異。當然這不是本文描述的重點,就不詳細展開了。


在對這些工具進行橫向對比時我們發現,他們的目標都是爲了解決一些特定的問題,如果我們有清晰的問題排查思路,結合這些工具,可以很快解決問題。而對於一些複雜場景,尤其是一些陌生的複雜問題,在沒有頭緒的情況下,縱然有各種神兵利器,也無計可施。線上問題排查猶如開車,老司機駕輕就熟,新手則手忙腳亂。當然如果新手有老司機加以指點,也可能很快地解決問題。


但問題是,這種老司機並不常見,也不可能時刻都能幫你。我們可以去網上查閱其他人總結的問題排查套路,再結合我們自己的場景,去嘗試解決問題,我也是經常這麼幹的。但這種方式效率依然不高,原因有三個:


1)信息檢索的成本:我們需要花時間去翻閱資料,去跟自己的場景匹配以判斷是否適合自己;

2)試錯的成本:有些資料不適合我們的場景,我們按照資料去嘗試,有可能被帶溝裏去,浪費時間;

3)問題排查需要藉助於一些第三方工具,而這些工具在生產環境需要安裝、配置和使用,也需要花較多的時間成本。


針對線上問題排查的特點和現狀,我們是否可以構建一個系統,這個系統會針對各種線上問題的排查形成一個知識(套路)庫,針對每一種問題,都有對應的套路和自動化工具幫助我們去定位問題。本文將結合一個比較有代表性的線上問題的排查過程,來探討這種方式的可行性。


二、問題排查的套路化


本章將以RSS佔用高爲例來對問題排查的套路化進行說明。RSS佔用高是很多人遇到過的問題,這個問題涉及的因素比較多,比較有代表性。當然在開啓了Swap的運行環境中,Swap高也是RSS高的一種表象,殊途同歸。


RSS是Resident Set Size(常駐內存大小)的縮寫,用於表示進程使用了多少內存(RAM中的物理內存)。如果我們遇到進程RSS接近服務器的物理內存,那就意味着你需要關注應用的健康程度了,這意味着應用後面很有可能出現OOM的問題,比如進程被OOM killer殺死,或者容器重啓,或者因使用Swap而速度變慢。


針對RSS高的問題,首先我們需要知道的是,Java進程消耗的內存絕不僅僅是你設置的Xmx或堆內存的用量這麼簡單,Java進程佔用的內存主要分爲2大部分:on-heap(堆內內存)和off-heap(堆外內存)。而堆外內存又包含JVM自身消耗的內存、JVM外的內存。所以,後續的排查思路我們也是按照堆內內存、JVM內存、JVM外內存3個方向來順序展開。


2.1  堆內存是否太大


首先要確認一下Java應用的堆內存是否太大,因爲JVM自身也會消耗一些內存,所以你至少需要預留出部分內存存給JVM使用。如果應用涉及較多的網絡通信,那還需要預留一些內存給堆外使用,所以一般來說你的堆內存最多爲服務器物理內存的75%(經驗值,需依據應用自身特點調整),如4G內存服務器,那麼堆內存最大爲3G。


堆內存用量的查看手段非常多,相信各個公司的基礎架構團隊都提供了可視化的監控手段,當然也可以通過原生命令jcmd <pid> GC.heap_info查看,如圖1所示:


圖1 

    

如果Java進程的堆內存用量已接近或超過物理內存的75%,那麼基本可以確定堆內存用量過大。這時可以調小Xmx來控制堆內存用量。如果Xmx不能減小,可以通過dump堆內存+MAT或JFR(Java Flight Recorder)+ JMC(JDK Mission Control)來分析內存佔用/分配情況,通過程序調優來減少堆內存用量。

    

如果到此RSS佔用呈穩定趨勢,我們就可以告一段落了,否則要繼續後面的步驟。


2.2  是否存在大量ARENA區


如果堆內存不大,那麼繼續排查非堆內存。首先去看一下ARENA區,在高併發的應用中,往往ARENA區佔用的內存會比較多。爲什麼先看ARENA區的內存佔用呢?是因爲這個步驟是不需要重啓JVM進程就可以完成的。至於ARENA是什麼以及作用是什麼,可以讀一下這篇文章:https://blog.csdn.net/maokelong95/article/details/51989081


這裏我們直接進入排查問題環節。執行如下命令:sudo -u <user> pmap -x <pid>|sort -gr -k2 |less,如果存在大量大小爲65536或60000左右的內存區域,則很大可能是ARENA區域佔用了太多的內存,如圖2所示:


圖2 


這種情況下,最簡單粗暴的辦法是在JVM啓動參數中增加配置:export MALLOC_ARENA_MAX=1。需要注意的是,上述的數值只能是1,其他大於1的數值經實踐證明是無法控制ARENA數量的。


2.3  非堆內存是否開銷過大


如果前面2個步驟過後都沒有發現問題,還有很多內存你不知道消耗在哪裏了,那麼我們開始第3步:開啓Native Memory Tracking。前面說過,Java應用的執行,JVM自身也需要消耗一些內存的,通過開啓Native Memory Tracking,我們就能知道JVM自身消耗了多少內存。


書歸正傳,通過修改JVM參數並重啓Java進程開啓NativeMemory Tracking:

-XX:NativeMemoryTracking=detail。


進程重啓後,可以通過NMT的一些子命令(summary/detail/baseline/diff)查看Native Memory的佔用情況:sudo -u <user>jcmd <pid> VM.native_memory detail。


圖3是在使用baseline建立了基線的情況下用detail.diff看到的各內存區的變化情況:


圖3  


通過上圖,可以看到JVM各個區域所使用的內存大小,主要包含了Java Heap、Class、Thread、Code、GC、Compiler、Internal、Other、Symbol等,各部分作用如下:


1)Class:加載的類與方法信息,其實就是 metaspace,包含兩部分:一是 metadata,被-XX:MaxMetaspaceSize限制最大大小,另外是 class space,被-XX:CompressedClassSpaceSize限制最大大小;

2)Thread:線程與線程棧佔用內存,每個線程棧佔用大小受-Xss限制,但是總大小沒有限制。在x64的JVM中,Xss默認爲1024K,所以如果你的應用開啓了1000個線程,那麼這個Thread區佔用將是1024M,所以一般我們會把Xss設置爲256K即滿足要求;

3)Code:JIT 即時編譯後(C1C2 編譯器優化)的代碼佔用內存,受-XX:ReservedCodeCacheSize限制;

4)GC:垃圾回收佔用內存,例如垃圾回收需要的 CardTable,標記數,區域劃分記錄,還有標記 GC Root 等等,都需要內存。這個不受限制,一般不會很大,但也有例外,圖3是27G的堆內存,使用G1垃圾回收器,你能看到GC區居然佔用了3.8G的內存;

5)Compiler:C1 C2 編譯器本身的代碼和標記佔用的內存,這個不受限制,一般不會很大;

6)Internal:命令行解析,JVMTI 使用的內存,這個不受限制,一般不會很大;

7)Symbol: 常量池佔用的大小,字符串常量池受-XX:StringTableSize個數限制,總內存大小不受限制;


我們需要需要注意的是Class、Thread、GC幾個區域的大小。圖4是各種JVM垃圾回收器消耗內存的比例,注意這部分內存是堆內存之外的:


圖4 


實踐證明,G1的內存開銷甚至能佔到堆內存的20%,當然這不是本文要討論的內容,感興趣的讀者可以去閱讀《JVM G1源碼分析和調優》一書查看相關內容。


針對上面的各個區域大小做加法,看一下是否接近於RSS的大小,如果是,恭喜你可以到此結束了。後續你需要做的就是針對內存佔用比較大的JVM區去做優化,這裏就不詳細介紹了。


如果不是,很遺憾,你進入到了最難啃的環節,繼續往下看吧。


注:開啓NativeMemoryTracking會造成5%的性能下降,用完記得修改JVM參數並重啓永久關閉,或者可以通過以下命令臨時關閉:jcmd vm.native_memory stop <pid>。


2.4  堆外內存是否用量太多


堆外內存也是比較容易被忽略的一個區域,尤其是網絡通信非常頻繁的應用,這種應用往往大量使用Java NIO,而NIO爲了提高效率,往往會申請很多的堆外內存。確認這個區域用量是否過大,最直接的方法是先查看是否是DirectByteBuffer或者MappedByteBuffer使用了較多的堆外內存。


如果你的服務器已經開啓了遠程JMX,你可以通過ops提供的jmx查詢工具去查詢,也可以通過jdk自帶的工具(比如jconsole、jvisualvm)查詢,如圖5所示:


圖5  


如果未開啓遠程JMX,可以通過jmxterm(https://docs.cyclopsgroup.org/jmxterm)工具在本地模式下查詢“java.nio:name=direct,type=BufferPool”和“java.nio:name=mapped,type=BufferPool”兩項內容確認用量。


如果確認上述堆外內存使用過多,那麼可以通過在jvm參數中設置-XX:MaxDirectMemorySize這個參數控制一下,因爲通過DirectByteBuffer分配的堆外內存,默認是不會控制這個區域的內存用量的。


如果上述內存用量不大,那我們就需要祭出終極殺器jemalloc來做進一步分析了。這裏涉及的內容比較多,受限於文章篇幅限制,就不展開描述了。


通過jemalloc的收集到的數據,我們基本能夠定位到堆外內存問題的原因。


2.5  總結


上述的4個步驟,基本能夠解決大多數的RSS佔用高的問題了。當然事無絕對,沒有一種藥是包治百病的。我們追求的基本目標是,通過問題排查的套路化,幫助工程師理清思路,少走彎路,以提高問題排查的效率。


三、問題排查的工具化


覆盤一下上述的問題排查過程,我們用到了很多的命令和第三方工具,整個過程還是工程師驅動命令行和工具。如果對命令參數不熟悉,或本地沒有安裝相應的工具,那這種套路化的教程也只能在一定程度上提高效率。在這個套路的基礎上,我們是否可以轉變思路,以工具爲主,工程師輔助,來提高排查問題的效率呢?讓我們來嘗試一下。


3.1 流程梳理


先來梳理工具的執行流程以及每個步驟需要做的事情,與第二節保持一致性,本節也劃分成4個子步驟。


3.1.1 確認堆內存是否太大


第一步要做的事情比較多,梳理如下:


  • Java進程pid:使用jps -v列出java進程列表,由用戶選擇具體的進程;
  • 獲取運行環境的物理內存和剩餘內存(free -m)
  • Java進程的堆內存用量(jcmd GC.heap_info)
  • Java進程GC情況(jstat-gcutil);


獲取到上述信息後,判斷堆內存是否太大。


3.1.2 是否存在大量ARENA區


通過pmap命令獲取內存分配列表,輔以awk命令提取內存信息,據此判斷是否存在大量ARENA區。


3.1.3 非堆內存是否開銷過大


此步驟需要在JVM啓動腳本中增加啓動參數並重新啓動進程,對於標準化的運行環境來說,在知道了啓動腳本位置和啓動命令的情況下,是可以通過工具來完成參數的修改和進程啓動。如果不能知道啓動腳本所在位置,我們可以複製當前進程的JVM參數來完成進程的啓動。


當JVM進程啓動完成,再次進入工具,我們就能借助於Native Memory Tracking的結果來判斷當前環節是否存在問題。


3.1.4 堆外內存是否用量太多


此步驟的自動執行,需要安裝第三方工具如jxmterm、jemalloc。在生產環境訪問外網受限的情況下,可以通過搭建內網資源服務器的方式來解決這個問題。通過一鍵安裝腳本,我們能快速完成所依賴工具的安裝和配置,剩下的就是讓工具來收集和分析數據定位問題了。


3.2  工具實現


目前公司內部很多運維工具,都是採用C+B/S的方式實現,這種方式工程師不需要申請目標服務器權限就能夠實現很多運維操作。但這種實現方式比較複雜,而我們的工具是帶有實驗性質,所以暫時使用shell+工具包的方式實現,即使用shell腳本將主流程串起來,各節點使用的工具如果有缺失,yum能安裝使用yum安裝,yum不能安裝的則提前下載內置到工具包中。


所有的準備工作完成後,編寫腳本的工作就相對簡單了,當然這需要用到很多的shell和linux、java命令,此處就不贅述了。腳本的最終運行效果如下:



四、總結


通過針對RSS佔用高問題的排查套路和排查工具的梳理,我們實現了一個簡單的問題快速排查腳本。當然在這個過程中可以發現,很多問題的排查,都可以使用類似的思路來工具化,日積月累,就形成了一個問題排查的工具包。


以內存問題排查舉例,我們積累了以下的快速工具,如圖:



這只是工具包的一部分,針對CPU、磁盤、網絡、GC等問題,藉助於Arthas、Async-Profiler等優秀的開源工具,我們都積累了很多快速工具,期望能幫工程師提高問題排查的效率。


如前面所講,這種完全基於shell的方式,由於需要登錄到目標服務器上操作,多數功能還需要有sudo權限,這有些許的不方便。另外,某些公司生產環境嚴格受限,那shell的方式就無法使用了。所以在此基礎上,可以擴展成Client+Server+Browser的模式,讓工程師在不登錄到服務器的情況下,就能完成問題的排查。


到此,本文的內容就結束了,但我們的工具還在不斷地積累中,在此也歡迎感興趣的同學幫我們提供場景,我們不斷豐富這個工具庫。同時,受限於作者水平,文中內容難免有不當之處,也歡迎提出意見和建議。


團隊招聘信息


我們是攜程機票研發團隊,負責攜程APP/PC端機票業務開發及創新。機票研發在搜索引擎、數據庫、深度學習、高併發等方向持續不斷地深入探索,持續優化用戶體驗,提高效率。


在機票研發,你可以和衆多技術頂尖大牛一起,真實的讓億萬用戶享受你的產品和代碼,提升全球旅行者的出行體驗和幸福指數。


如果你熱愛技術,並渴望不斷成長,攜程機票研發團隊期待與你一起騰飛。目前我們前端/後臺/數據/測試開發等領域均有開放職位。


簡歷投遞郵箱:[email protected],郵件標題:【姓名】-【攜程機票】-【投遞職位】。


【推薦閱讀】




 “攜程技術”公衆號

  分享,交流,成長



本文分享自微信公衆號 - 一線碼農聊技術(dotnetfly)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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