執行緒剖析-助力定位程式碼層面高耗時問題
在當今的軟體開發領域,效能問題是一個永不過時的挑戰。為了解決這一挑戰,開發人員需要深入瞭解他們的應用程式執行時的效能,並快速定位高耗時問題。執行緒剖析是一種強大的工具,透過採集和計算執行時執行緒棧,可以幫助開發人員更好地理解和解決效能問題。本文將深入探討執行緒剖析的基本思想和實現思路,以及客戶端和服務端的設計。
一 基本思想
執行緒剖析的核心思想是在業務執行緒執行請求時建立一個特定閾值觸發的檢測任務,用於監測高耗時問題。如果任務未被取消,在達到高耗時閾值時,將有專門的執行緒去執行剖析任務,採集業務執行緒的堆疊,並非同步傳送給剖析服務端進行計算,以估算出棧上的各個方法耗時。這個工具不僅提供了詳細的效能資料,還能與開放遙測(OpenTelemetry)結合,從而實現鏈路特徵的關聯,主要流程如下:
二 實現思路
客戶端設計
客戶端的架構主要體現在任務的建立、排程、執行和匯出四個環節。
建立&排程任務
業務執行緒執行時,若滿足指定要監控的介面或執行緒名稱,將構造一個包含該執行緒物件的檢測任務放入佇列,時間輪的工作執行緒會週期性(預設100ms)在輪盤上移動一格,類似我們平時看到的鐘表上的指標那樣,每個週期會從任務佇列取出所有任務,將各個任務分配新增到時間輪中每個格子中。如下圖所示:
執行任務
分配完成後,由任務執行執行緒池的執行緒去執行當前週期所屬格子的所有任務。在執行前,業務執行緒可能優先結束而取消該任務的執行,例如在達到耗時閾值後,剖析任務已經或準備開始執行,但主執行緒取消了剖析任務這樣一個臨界點,此時可透過各語言的同步機制來及時取消剖析任務。
任務執行時,剖析執行緒將週期性採集執行緒的堆疊,而為了方便後續的分析工作,也會同時記錄當前堆疊產生的時間戳,直到業務執行緒發出中斷通知,或採集樣本數達到上限,或任務狀態發生改變,然後中斷剖析執行緒的執行。
執行完成後,將採集到的執行緒棧集 push 到診斷資料佇列,等待資料匯出執行緒消費此佇列,併傳送到服務端。這裡需要注意,執行緒棧資料文字量一般不會太小,比如我們一個專門用於測試的應用,500ms 觸發的閾值下的 HTTP 介面,每次請求讓執行緒隨機 Sleep 5s 以內,當介面耗時超過 3s,單次剖析產生的棧文字大小在 200KB 以上,因此這裡需要有個引數,來控制佇列預設長度,避免過多的堆疊快照擠兌記憶體。
整個任務執行流程如下圖所示:
資料預聚合&匯出
預聚合工作將由獨立的工作執行緒消費診斷資料佇列後來做,即將多個執行緒快照合併為一個,降低網路 IO 開銷。具體就是對於快照集中每個快照的棧幀,按照它的開始時間取快照集中相同棧幀的最小值,結束時間取快照集中相同棧幀的最大值這個規則進行聚合,流程如下圖所示:
而資料傳送層就比較簡單了,採用高效能無鎖佇列 Mpsc, 使用 gRPC 協議傳送到診斷服務端:
當然,為了降低業務系統的壓力,也可以將原始資料直接落盤,由外部獨立的採集器逐行採集然後傳送到訊息佇列。
服務端設計
服務端的架構主要考慮三個點:
資料接收
同 Otel 對 Trace 資料的處理思路類似,診斷資料傳送請求需要快速地被響應,來減少客戶端因請求延遲導致傳送佇列資料被丟棄的可能。因此,診斷服務端採用吞吐效能較好的 go 語言編寫,而請求涉及到跨語言呼叫,協議層上,綜合高效快速可靠因素,選用較成熟的 gRPC 協議進行通訊。
資料接收併成功解析後,需非同步將資料放入佇列,這裡我們選用採用了多副本機制的 Kafka 訊息中介軟體,來滿足診斷服務各模組之間的解耦,同時也保證診斷資料不丟失 。
資料解析&加工
診斷剖析資料消費組會去消費佇列中的資料,將資料進行進一步解析,並且持久化處理,其中包括:
a) 父子棧幀推導
客戶端的預聚合會將多個快照合併為一個,因此快照內的每個棧幀將擁有不同的起始和結束時間。由於 Java 的原始執行緒堆疊是無層級的結構,為了提高資料的可讀性,進一步降低高耗時問題定位發現的成本,因此需將已合併的堆疊進一步推導為包含父子棧幀的結構化資訊,即從棧頂的第二個棧幀開始遍歷呼叫棧,若當前棧幀的快照開始和結束的時間範圍位於上個棧幀的左開右閉或左閉右開區間,則將當前棧幀設定為上個棧幀的子棧:
注意:
1. 一個 Java 執行緒的大部分呼叫棧形式本身就是個從 "Thread.run" 開始的巢狀,而每次快照時也無從得知層級資訊,因此不考慮推導快照開始和結束時間完全一致的棧幀,將這些棧幀置為同級即可。
2. 使用執行緒的快照時間來推導還原父子棧和耗時仍然是個相對比較粗略的統計行為,其精度受到當前執行緒呼叫棧快照匯出的耗時,以及每次快照的間隔耗時的影響,因此父子層級結果僅供參考,並不絕對等於實際呼叫的關係結果。
b) 自身耗時計算
當已推匯出父子棧幀關係後,可對結果集進行遍歷,計算自身耗時,計算規則如下:
從第二個棧幀開始,如果與上一個棧幀的快照開始和結束時間一致,則上個棧幀的自身耗時設為 0,否則會將當前棧幀的父棧幀(若存在)的自身耗時減掉上個棧幀的自身耗時。
如果當前棧幀是最後一個,則將當前棧幀自身耗時設為快照開始與結束的時間差,並且將當前棧幀的父棧幀(若存在)的自身耗時減掉當前棧幀的自身耗時。
如果當前棧幀有子棧幀,處理方式同上。
以上圖呼叫時序為例,根據以上規則得出的自身耗時計算示意圖如下:
c) 資料持久化
當完成父子棧幀推導和自身耗時計算後,資料將持久化儲存,例如將資料儲存到 ClickHouse,供資料查詢端使用。
資料查詢
診斷剖析資料將以 HTTP API 形式對外提供查詢服務,例如可觀測性門戶系統,可根據執行緒名,鏈路 Trace ID, Span ID 等特徵進行剖析資料的查詢。
[{"data": "YXQgc3VuLm5pby5jaC5Vd...",//執行緒剖析棧"thread_name": "XNIO-1 I/O-1",//執行緒名稱"thread_state": "RUNNABLE",//執行緒狀態"trigger_millisecond": 500,//觸發閾值"self_millisecond": 38,//自身耗時"source_snapshot_count": 153//快照數},{"data": "YXQgaW8udW5kZXJ0b3cuc2Vy...","thread_name": "XNIO-1 task-1","thread_state": "RUNNABLE","trigger_millisecond": 500,"self_millisecond": 0,"source_snapshot_count": 140}]
呼叫鏈關聯
執行緒剖析能結合 OpenTelemetry ,藉助 OpenTelemetry Java Instrumentation 上下文的生命週期,從而關聯 Trace ID 、介面名等鏈路特徵。
自身監控指標
執行緒剖析功能需要擁有較完善的自身監控,以便觀測複雜剖析流程下對業務系統潛在的效能影響。這些監控包括:
任務檢測佇列大小
檢測佇列用於給時間輪提供任務,該指標的大小給執行緒剖析的取樣,介面名,執行緒名稱等條件提供了一定參考。
任務釋放平均耗時
剖析任務的釋放將會中斷正在執行的剖析任務,其中涉及到剖析、資料狀態機的改變,執行緒的中斷。多執行緒情況下,需保證操作的原子性,如果任務釋放的平均耗時變長,則能反映當前業務系統 CPU 執行緒上下文切換效率下降。
正在執行剖析任務的個數
執行緒剖析是以執行緒為單位來執行的,透過觀測正在進行執行緒剖析的任務數,可反映出剖析功能繁忙的程度,以及幫助我們決策是否需要對同時剖析的任務數進行限制。
執行緒堆疊匯出平均耗時
執行緒棧匯出方法的平均耗時,如果該操作耗時顯著升高,且呼叫棧未有明顯變化,則代表效能惡化。
資料佇列大小
指待傳送到服務端的資料佇列大小。
資料入隊速率
指待傳送到服務端的資料入隊的速率。
資料合併平均耗時
資料傳送前進行預聚合,將多個執行緒快照合併為一個,這個過程的平均耗時,該值可供剖析條件提供一定參考。
執行緒棧快照匯出傳送平均位元組
執行緒快照傳送的請求包平均大小。
資料匯出速率
執行緒快照傳送的速率。
對以上指標進行監控,也方便對相關引數進行調優,從而更好地在診斷剖析功能的完整性與服務效能之間做相關取捨。
三 結語
執行緒剖析為解決效能問題提供了有力支援。透過採集和分析執行緒棧資訊,它能夠幫助開發人員定位應用程式中的高耗時問題,為效能最佳化提供關鍵資訊。本文詳細介紹了執行緒剖析的基本思想和實現思路,以及客戶端和服務端的設計架構。其核心思想是透過建立特定閾值觸發的檢測任務,監測高耗時問題,並將採集到的資料非同步傳送到剖析服務端進行進一步計算和分析。
此外,執行緒剖析的自身監控指標,這些指標有助於更好地瞭解剖析功能的效能和繁忙程度,以便進行決策和調優。執行緒剖析不僅提供了效能資料,還可以與 OpenTelemetry 相結合,實現鏈路特徵的關聯,從而更全面地理解效能問題。
總的來說,執行緒剖析可以幫助開發人員提高應用程式的質量和效能,快速定位效能問題,以確保應用程式的順暢執行,同時,也可以更有效地應對效能挑戰,提高應用程式的可維護性和效能。
來自 “ 得物技術 ”, 原文作者:櫛楓忻垣;原文連結:https://server.it168.com/a2023/1116/6829/000006829837.shtml,如有侵權,請聯絡管理員刪除。
相關文章
- 執行緒剖析 - 助力定位程式碼層面高耗時問題|得物技術執行緒
- JVM調優jstack找出最耗cpu的執行緒&定位問題程式碼JVMJS執行緒
- 執行緒虛假喚醒問題剖析執行緒
- 如何利用執行緒堆疊定位問題執行緒
- Java多執行緒面試高配問題---多執行緒(3)🧵Java執行緒面試
- iOS主執行緒耗時檢測方案iOS執行緒
- iOS精準計算程式碼執行耗時iOS
- 多執行緒-執行緒安全問題的產生原因分析以及同步程式碼塊的方式解決執行緒安全問題執行緒
- 多執行緒-生產者消費者問題程式碼2並解決執行緒安全問題執行緒
- 執行緒問題執行緒
- Android高併發問題處理和執行緒池ThreadPool執行緒池原始碼分析Android執行緒thread原始碼
- 多執行緒程式設計,處理多執行緒的併發問題(執行緒池)執行緒程式設計
- java多執行緒執行問題Java執行緒
- 多執行緒問題執行緒
- 多執行緒下的程式同步(執行緒同步問題總結篇)執行緒
- 多執行緒掃描資料夾耗時方法分析執行緒
- ThreadLocal執行緒重用時帶來的問題thread執行緒
- 多執行緒-生產者消費者問題程式碼1執行緒
- SimpleDateFormat 執行緒安全問題ORM執行緒
- java執行緒安全問題Java執行緒
- 03 執行緒安全問題執行緒
- PHP執行耗時指令碼實時輸出內容PHP指令碼
- 執行緒(一)——執行緒,執行緒池,Task概念+程式碼實踐執行緒
- Java多執行緒9:ThreadLocal原始碼剖析Java執行緒thread原始碼
- iOS 深入剖析多執行緒iOS執行緒
- Java多執行緒中執行緒安全與鎖問題Java執行緒
- oracle_CPU佔用率高時的問題定位Oracle
- 不改一行程式碼定位線上效能問題行程
- JavaScript執行機制深層剖析JavaScript
- Js 執行機制深層剖析JS
- CUDA執行緒層次執行緒
- 程式碼注入之遠端呼叫執行緒的一些問題執行緒
- 【面經】多執行緒常見面試題執行緒面試題
- 如何計算一段js程式碼執行耗費的時間JS
- 關於離開頁面時執行的操作問題
- 併發程式設計之 原始碼剖析 執行緒池 實現原理程式設計原始碼執行緒
- 作業系統的程式/執行緒同步問題作業系統執行緒
- Linux每程式執行緒數問題處理Linux執行緒