常見 CPU 性能問題
你所負責的服務(下稱:服務)是否遇到過以下現象:
- 休息的時候,手機突然收到大量告警短信,提示服務的 99.9 line 從 20ms 飆升至 10s;
- 正在敲代碼實現業務功能時,收到業務/客服同事電話,反饋系統打不開;
- 下班后,收到運維同學電話,服務器監控告警提示“某個機器的負載(Load Average)從 0.1、0.5、0.8 突然間飆升至 9.73、10.67、10.49”。
結果:引發雪崩的場景如下圖所示:
通常造成這幾種現象的根本原因主要有以下 3 點。
- 服務在某個時間點運行了某一個定時器,但因為最近業務數據量增加,導致提取了大量數據到服務進行計算。
- 外部系統因為系統問題/數據量增加/邏輯修改,向消息中間件推送比平常多 10 倍的消息,此時服務消費了 MQ 消息,將消息內容放入一個新線程進行異步處理。而給消息中間件返回消息已處理完成,導致消息中間件一直給服務推送消息。
- 上線一個可以從 C 端觸發需要大量 CPU 計算的功能,上線后使用的用戶比預估量高 5 倍。
我們再來回顧一下之前的過程,如下圖所示:
從現象到結果(CPU 問題),再到追溯原因這一過程,通常是通過對時間點、查日志、找上下游問題、Code Review、推測出問題代碼,從而得出最終結論。但這種方式的缺點是耗時較長,且不能保證得到的結果一定是真實的。
利用工具定位 CPU 性能問題
借助Flame Graph(下稱 CPU 火焰圖),來定位 CPU 性能問題。
在實際工作中,使用 CPU 火焰圖用於量化框架中的性能,包括代碼編譯消耗的時間、代碼緩存、其他系統內庫及內核代碼執行的時間,通常用於定位 CPU 使用率問題。生成火焰圖的方式有基於 Perf 或者Arthas。
利用 Perf 生成火焰圖
從 Linux 系統原生提供的性能分析工具 perf 命令(performance 的縮寫)說起,該命令執行后,會返回 CPU 正在執行的函數名以及調用棧(stack)。 通常,它的執行頻率是 99Hz(每秒 99 次)。如果 99 次都返回同一個函數名,那就證明 CPU 同一秒鍾都執行同一個函數,可能存在性能問題。具體可以通過執行以下 perf 命令獲取對應的性能日志:
1 perf record -F 99 -p PID -g
該命令返回一個用於記錄 CPU 調用棧的文本文件,該文件長達幾十萬甚至上百萬行,輸出的日志文件並不方便閱讀,所以需要有一個將日志文件轉成火焰圖的工具——perf report。
1 perf report -n --stdio
perf report 命令可以很好地將數百個堆棧跟蹤樣本匯總為文本。類似的代碼路徑合並在一起,文本輸出的信息為樹形圖,而每個葉子上都有 CPU 占用的百分比。從左上角到右下角讀取路徑,
- perf 可以通過命令生成 CPU 在執行什么,但並不能直接顯示 Java 進程的調用棧。那用什么方式可以與 Java 進程的調用棧結合起來生成火焰圖呢?
- perf_events:標准 Linux 分析器,用於生成系統堆棧。
- perf-map-agent:提供轉換 perf_events 帶有 Java 標示的 JVMTI 代理。
- Misc:生成全部 Java 進程的堆棧信息。
- Flame Graph:通過已經生成的 Java 堆棧信息,生成火焰圖。
火焰圖種類
火焰圖(Flame Graph):最普通/朴素的火焰圖,標示了每一個調用棧的深度與執行時間。
紅藍微分火焰圖(Differential Flame Graph):有兩個值列,用於顯示計數之前和之后。使用第二列(“之后”或“現在”)調整火焰圖的大小,然后使用 2-1 的增量進行着色:正極為紅色,負極為藍色。pl 工具獲取兩個折疊樣式的概要文件(使用 stackcollapse 腳本生成)並發出這兩列輸出。
CPI 火焰圖(CPI Flame Graph):測量每條指令的平均周期(CPI)比率,越高的值意味着完成指令需要更多的周期(通常指等待內存 I/O 的“停滯周期”),往往作為一個全系統的指標來研究。
CPI(平均每條指令的平均時鍾周期數)=(CPU 時間/IC 指令數)*頻率
耗時分析方法
請看以上實際的火焰圖例子,例子中包含各種疊起來並且大小不一的圖形,那如何得知哪個方法耗時最多?下面我們來分析一下如何閱讀上述火焰圖。
- Y 軸:棧深度
- X 軸:CPU 耗時
- 長方形:一個棧(方法)
- 長度:出現在監視器中的時長(占用 CPU 的時間)
- 其他:從左到右的順序只是按照字母排序,並無意義
通過上述分析,我們知道了圖中各種數字的含義。因為下面調用棧的耗時是依賴於上面調用棧,所以越往上的調用棧長方形越長,CPU 在該調用棧的耗時越多。因此,上圖中耗時最多的方法應該是 f()。
總結一下,閱讀火焰圖的方式主要有以下兩種。
- 從下到上:從父到子方法追查。
- 從左到右:先找出占用最多時間的棧,關注最寬的方法。
如何排查線上 CPU 問題
定位到 CPU 問題后,接下來開始排查線上問題。
- 壓測環境:在壓測環境,我們可以通過壓測與監控工具,發現性能問題。利用 CPU 火焰圖,在壓測時,通過對 CPU 進行定時采樣,定位具體占用 CPU 調用棧。
- 模擬環境:建立一個基於線上,但與其隔離的模擬環境。通過流量復制工具,將線上流量復制到模擬環境。再通過對 CPU 進行定時采樣,將采樣的日志生成火焰圖,從而定位具體占用 CPU 調用棧。
下圖是一個線上發生 CPU 性能問題的例子:
如果線在頂部代表 CPU 空閑,線在底部代表 CPU 繁忙。該問題是在用戶量不多的服務中發生的,此時可以先回顧一下問題的過程。
時間點 1 發布了一個包含監控功能與業務功能的版本。慢慢地 CPU 開始繁忙起來。到時間點 2,CPU 使用率超過 50%,持續了 3 小時。在時間點 3 實施過重啟操作,CPU 使用率回歸正常,但慢慢地也在往上增加。在時間點 4 ,回滾業務功能,但CPU 沒有降下來。在時間點 5 實施回滾監控功能,服務恢復正常。
在上述處理線上 CPU 問題的過程中,采取了回滾版本讓服務恢復正常。但如何才能定位 CPU 在哪里被占用最多呢?這時候,CPU 火焰圖就需要登場了。
在壓測環境,使用壓測工具(ab/wrk/jmeter 等)對服務的 URL 進行隨機參數的請求。發現 CPU 在壓測開始 10 分鍾之后,CPU 慢慢上升,服務響應越來越慢。此時使用 perf 工具對 CPU 進行采樣,通過火焰圖工具將采樣的日志生成包含調用棧的火焰圖。
從上圖中可以看出,大部分時間在執行服務中的邏輯,指向新加的服務監控組件。但此時問題在於:監控組件創建 span 時應該是根據路徑作為 key,指向已緩存的數據可以加快處理時間。
而實際情況則是將參數也放到了緩存 key 中,所以緩存沒命中,導致key一直在創建,長期地占用 CPU 在計算 key 是否有命中,從而產生 CPU 性能問題。