性能優化必備——火焰圖

来源:https://www.cnblogs.com/gugujifly/archive/2022/09/01/16647070.html
-Advertisement-
Play Games

本文主要介紹火焰圖及使用技巧,學習如何使用火焰圖快速定位軟體的性能卡點。 結合最佳實踐實戰案例,幫助讀者更加深刻地理解火焰圖構造及原理,理解 CPU 耗時,定位性能瓶頸。 ...


引言

本文主要介紹火焰圖及使用技巧,學習如何使用火焰圖快速定位軟體的性能卡點。
結合最佳實踐實戰案例,幫助讀者加深刻的理解火焰圖構造及原理,理解 CPU 耗時,定位性能瓶頸。

背景

當前現狀

假設沒有火焰圖,你是怎麼調優程式代碼的呢?讓我們來捋一下。

1. 功能開關法

想當年我剛工作,還是一個技術小白時,排查問題只能靠玄學,大致能猜出問題可能是由某個功能代碼導致的,此時的排查手段就是刪除多餘的功能代碼,然後再運行查看 CPU 消耗,確定問題。(至今我工作時還會發現一些老人使用如此方法調試性能。)

public void demo() {

    if (關閉1) {
        // 功能1
        handle1();
    }

    if (關閉2) {
        // 功能2
        handle2();
    }

    if (打開3) {
        // 功能3
        handle3();
    }

    // 功能4
    handle4();
}

此法全靠“經驗”和“運氣”,而且改動了代碼結構,假設這是一個已經通過測試的集成區代碼,此時需要修改代碼功能來調試程式是非常危險的一件事,當然有 Git 倉庫可以“一鍵還原”,但是,是人操作,總歸會有失手的時候,且定位效率太低

2. StopWatch 埋點法

當程式出現性能問題時,且不確定是哪一段代碼導致耗時,可以藉助方法耗時來判斷,此時我們只要在調用方法前後追加執行所需耗時日誌,即可判定到底是哪個方法最耗時。

public void demo() {
    Stopwatch stopwatch = Stopwatch.createStarted();
    handle1();
	log.info("method handle1 cost: {} ms", 
             stopwatch.elapsed(TimeUnit.MILLISECONDS));
    
    handle2();
    log.info("method handle2 cost: {} ms", 
             stopwatch.elapsed(TimeUnit.MILLISECONDS));
    
    handle3();
    log.info("method handle3 cost: {} ms", 
             stopwatch.elapsed(TimeUnit.MILLISECONDS));
    
    handle4();
    log.info("method handle4 cost: {} ms", 
             stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
}

此法較上一個方法的優勢是,不改變代碼的邏輯情況下,只是增強了一些觀測點位,由方法的耗時來定位性能瓶頸。但是,假設方法的處理調用棧很深,就不得不在子方法中再次埋點,此時判定流程即為:埋點 -> 發版 -> 定位 -> 埋點 -> 發版 -> 定位 -> .......且本質上也是改了代碼,就有出錯的可能。 心累,不高效!

3. TOP 命令定位熱線程

一般企業的軟體服務都是部署在 Linux 操作系統上,有經驗的老手排查性能最方便的辦法就是 top 定位。

top -p pid -H

image.png
明顯看到,pid 103 消耗了 40%的 CPU, 找到對應的 stack 線程信息如下(忽略查找辦法,我假設你已經會了:)):
image.png
此時可以得出結論,當前最耗 CPU 的線程是寫入磁碟文件,追查代碼最終會定位到是因為在高併發場景下打了大量的 INFO 日誌,導致磁碟寫入成為瓶頸。

總結:TOP 命令對於找 CPU 性能瓶頸時很有效的,但是存在如下幾個問題:

  • 排名最前的一定是當時最消耗 CPU 的,但不一定是程式性能的誘因。例如因某個 BUG 導致列印了大量 ERROR 日誌,最終 LOG 到磁碟是最消耗 CPU的,但罪魁禍首不是它。
  • TOP 註定使你只會關註最高的,等你修複最耗 CPU 的問題後,往往還會遇到別的程式問題導致 CPU 偏高,即一次只能看到一個問題,看不到全貌。
  • 文本的表現力非常有限:首先你得對 Linux 及 JVM 命令非常熟悉,其次文本對兩個及以上值做關聯性分析時,就捉襟見肘了,此時就迫切的需要另一種分析工具——圖。

什麼是火焰圖

火焰圖(Flame Graphs),因其形似火焰而得名。
image.png

如上就是一個典型的火焰圖,它由各種大小/顏色的方塊組成,每個方塊內部還標識了文字,整個圖片頂部凹凸不平,形似一簇簇“火苗”,因此得名火焰圖。
火焰圖是 SVG 生成,因此可以與用戶互動,滑鼠懸浮在某個方塊時,會詳細展示內部文字。點擊後,即會以當前被點擊方塊為底向上展開。

特征
使用火焰圖分析之前,我們得首先瞭解火焰圖的基本構造

  • 每一列代表一個調用棧,每一格代表一個被調用函數
  • 方塊上的字元標識調用方法,數字表示當前採樣出現次數
  • Y 軸表示調用棧深度,X 軸將多個調用棧歸併,並首字母排序展示
  • X 軸寬度表示採樣數據中出現頻次,即寬度越大,導致性能瓶頸的原因可能就越大(註意:是可能,不是確定
  • 顏色沒什麼意義,隨機分配(可能創始人想讓你看起來更像一個火焰。。)

火焰圖可以做什麼

那此時你已經知道了火焰圖,如何定位軟體問題呢?我們需要一套尋找性能瓶頸的方法論。
可以明確的是 CPU 消耗高的口徑

CPU 消耗高的口徑 = 調用棧出現頻率最高的一定是吃 CPU 的

如上我們已經知道了火焰圖的構造,及“物料”含義,此時我們的關註點應該在方形的寬度上,方形的寬度大小代表了該調用棧在整個抽樣歷史中出現的次數。次數意味著頻率,即出現次數越多的即可能最消耗 CPU。
image.png
但只關註最長的是沒用的,如底部的 root 和中部的方塊都很寬,只能說明這些方法是“入口方法”,即每次發起調用都會經過的方法。
我們更應該關註火焰山頂部的"平頂山"(plateaus)出現的次數多,即沒有子調用,抽樣出現的頻率高,說明執行方法的時間較長,或者執行頻率太高(如長輪詢),即CPU 大部分執行都分配給了“平頂山”,它才是性能瓶頸的根因。

總結方法論:火焰圖看“平頂山”,山頂的函數可能存在性能問題!

最佳實踐

實踐是檢驗真理的唯一標準!下麵我將以一個小的 Demo 來展示如何定位程式性能問題,加深對火焰圖使用的理解。

Demo 程式如下:

public class Demo {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(20);

        while (true) {
            executorService.submit(Demo::handle1);
            executorService.submit(Demo::handle2);
            executorService.submit(Demo::handle3);
            executorService.submit(Demo::handle4);
        }
    }

    @SneakyThrows
    private static void handle4() {
        Thread.sleep(1000);
    }

    @SneakyThrows
    private static void handle2() {
        Thread.sleep(50);
    }

    @SneakyThrows
    private static void handle3() {
        Thread.sleep(100);
    }

    @SneakyThrows
    private static void handle1() {
        Thread.sleep(50);
    }
}

代碼很簡單,當然現實中也不會這麼寫,主要是配合演出。。
主要是開了一個線程池,且分別執行四個 task,不同的 task 耗時不一致,此時我們的性能瓶頸在 handle4 這個任務上,在知道結論的前提下,我們比較看火焰圖得出答案的是否符合預期!

1. JVM堆棧信息拉取

當前我是在自己的 Mac 上運行的程式,idea 執行這一段程式非常便捷,那如何獲取當前運行 main 函數的 PID?
此時需要用到 TOP 命令,上面是個 while 死迴圈,很明顯吃 CPU最厲害,只要找到歸屬 Java 線程的最高一個 PID 即為所求。
image.png
很明顯得到 COMMAND = java 最高的 PID = 20552
此時執行如下命令獲取堆棧信息,並寫入 tmp.txt 文件

jstack -l 20552 > tmp.txt

2. 生成火焰圖

生成火焰圖的工具有很多,我一般會藉助 FastThread,線上分析堆棧,非常方便,同時支持生成火焰圖,方便我們定位問題
image.png
打開官網首頁,選擇剛剛 dump 的堆棧文件,點擊 Analyze,此時只需要等待網站分析好後(正常 3~5 s),即可查看火焰圖

fastThread 網站分析報告非常豐富,一般的問題我們直接通過它給出的結論基本能定位到問題了,本文暫且無需關註,感興趣的話,後續我會分享,直接拉到 Flame Graph 子標題處
image.png
此時明顯能看出4個“平頂山”,且 com.Demo.handle4 寬度最大,com.Demo.handle3 次之,符合預期!

原理剖析

基於上述小 Demo ,我們深入理解下火焰圖的生成原理。

舉個例子,便於你理解,假設我們要觀測一個人在忙些什麼,哪些事最占用他的時間,會怎麼做?
從時間維度的話,且不考慮成本的話,我肯定安排一個監控攝像頭,全天候 24h,360度監控他,然後再安排人員,逐幀排查,並彙總他所做的事,得出:睡覺 8h,工作8h,玩手機 4h,吃飯2h,其它2h。從而得出結論:睡覺占用他時間最多。

由上可以總結一套分析流程:

記錄(監控)-> 分析&歸併(逐幀排查) ->  Top N -> 得出結論

帶著流程去看我們應該如何排查 CPU 在執行中,哪些事(進程/線程)最占用它的時間呢?
簡單粗暴的方法是每時每刻都記錄執行的方法堆棧,再彙總歸併,得出最耗時的方法棧在哪。此法的問題在於

  • 數據量大
  • 時間長

其實只要採樣去觀測 CPU 在乾什麼就好了,這是一個概率學問題,如果 CPU 因為執行某個方法耗時,大概率採樣下來,得到的歸併結果也是最多的,雖然有誤差,但是多次統計下,差不了多少的。
同理,dump 下的堆棧,查看大多數線程在乾什麼,依據堆棧內每個方法出現的頻率聚合,出現的頻次最多的就是當前 CPU 分配執行最多的方法。

"pool-1-thread-18" #28 prio=5 os_prio=31 tid=0x00007f9a8d4c0000 nid=0x8d03 sleeping[0x000000030be59000]
    java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at com.Demo.handle2(Demo.java:31)
    at com.Demo$$Lambda$2/1277181601.run(Unknown Source)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

    Locked ownable synchronizers:
- <0x00000006c6921ac0> (a java.util.concurrent.ThreadPoolExecutor$Worker)

至於我們的 jstack 信息如何被處理成火焰圖的格式,社區已經為常見的 dump 格式都提供了工具,stackcollapse-jstack.pl 處理 jstack 輸出。

Example input:

"MyProg" #273 daemon prio=9 os_prio=0 tid=0x00007f273c038800 nid=0xe3c runnable [0x00007f28a30f2000]
    java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.read(SocketInputStream.java:121)
        ...
        at java.lang.Thread.run(Thread.java:744)

Example output:

MyProg;java.lang.Thread.run;java.net.SocketInputStream.read;java.net.SocketInputStream.socketRead0 1

總結&展望

火焰圖的介紹到此結束,相信你又多了一種排查問題的手段!
存在即合理,工具之開發重要性而言不必多說,我始終持包容態度面對新事物,它確確實實解決了某些痛點而脫穎而出的。
後續我會介紹更多排查問題的手段,如果你喜歡本文風格,請關註或留言,歡迎討論!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Vector底層實現 vector的三個私有成員 :_start 記錄初始位置 , _finish 記錄有效字元 , _endofstoage 記錄容量大小 vector會存儲的類型不同,所以要用模版來定類型 typedef T* iterator; iterator _start; iterato ...
  • Java註解 1.註解的理解 註解(Annotation)也被稱為元數據(Metadata),用於修飾解釋 包、類、方法、屬性、構造器、局部變數等數據信息 和註釋一樣,註解不影響程式邏輯,但註解可以被編譯或者運行,相當於嵌入在代碼中的補充信息 在JavaSE中,註解的使用目的比較簡單,例如標記過時的 ...
  • 前言 嗨嘍~大家好呀,這裡是魔王吶 ! 又是學習的一天,讓我們開始叭~ 環境使用: Python 3.8 Pycharm 模塊使用: requests >>> pip install requests re csv 如果安裝python第三方模塊: win + R 輸入 cmd 點擊確定, 輸入安裝 ...
  • 數組(array):是一種用於存儲多個相同數據類型的存儲模型(可以理解為容器) 數組定義和靜態初始化 數組的兩種定義格式: 格式1: 數據類型[] 變數名; 範例: int[] arr; // 定義了一個int類型的數組,數組名是arr 格式2: 數據類型 變數名[]; 範例: int arr[]; ...
  • 聲明 本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關! 本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權, ...
  • “finally塊一定會執行嗎?” 這是最近一個工作3年的小伙伴去面試的時候遇到的問題。 你遇到這個問題會怎麼回答呢? 大家好,我是Mic,一個工作了14年的Java程式員 對於這個問題,面試官想考察什麼呢? 問題解析 這個問題,很明顯是考察Java基礎。 finally語句塊在實際開發中使用得非常 ...
  • 1 Docker 安裝 Nexus3 1.1 創建目錄 在硬碟上創建 Nexus3 的主目錄: mkdir -p /Users/yygnb/dockerMe/nexus3 為該目錄添加許可權: chmod 777 -R /Users/yygnb/dockerMe/nexus3 1.2 拉取鏡像 搜索鏡 ...
  • 1.type() 函數 使用 type() 函數可以查看數據類型。 示例: >>> type(123) <class 'int'> >>> type(12.3) <class 'float'> >>> type("abc") <class 'str'> >>> type([1,2,3]) <clas ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...