線程剖析 - 助力定位代碼層面高耗時問題
在當(dāng)今的軟件開發(fā)領(lǐng)域,性能問題是一個永不過時的挑戰(zhàn)。為了解決這一挑戰(zhàn),開發(fā)人員需要深入了解他們的應(yīng)用程序運行時的性能,并快速定位高耗時問題。線程剖析是一種強大的工具,通過采集和計算運行時線程棧,可以幫助開發(fā)人員更好地理解和解決性能問題。本文將深入探討線程剖析的基本思想和實現(xiàn)思路,以及客戶端和服務(wù)端的設(shè)計。
一、基本思想
線程剖析的核心思想是在業(yè)務(wù)線程執(zhí)行請求時創(chuàng)建一個特定閾值觸發(fā)的檢測任務(wù),用于監(jiān)測高耗時問題。如果任務(wù)未被取消,在達(dá)到高耗時閾值時,將有專門的線程去執(zhí)行剖析任務(wù),采集業(yè)務(wù)線程的堆棧,并異步發(fā)送給剖析服務(wù)端進(jìn)行計算,以估算出棧上的各個方法耗時。這個工具不僅提供了詳細(xì)的性能數(shù)據(jù),還能與開放遙測(OpenTelemetry)結(jié)合,從而實現(xiàn)鏈路特征的關(guān)聯(lián),主要流程如下:
圖片
二、實現(xiàn)思路
客戶端設(shè)計
客戶端的架構(gòu)主要體現(xiàn)在任務(wù)的創(chuàng)建、調(diào)度、執(zhí)行和導(dǎo)出四個環(huán)節(jié)。
創(chuàng)建&調(diào)度任務(wù)
業(yè)務(wù)線程執(zhí)行時,若滿足指定要監(jiān)控的接口或線程名稱,將構(gòu)造一個包含該線程對象的檢測任務(wù)放入隊列,時間輪的工作線程會周期性(默認(rèn)100ms)在輪盤上移動一格,類似我們平時看到的鐘表上的指針那樣,每個周期會從任務(wù)隊列取出所有任務(wù),將各個任務(wù)分配添加到時間輪中每個格子中。如下圖所示:
圖片
執(zhí)行任務(wù)
分配完成后,由任務(wù)執(zhí)行線程池的線程去執(zhí)行當(dāng)前周期所屬格子的所有任務(wù)。在執(zhí)行前,業(yè)務(wù)線程可能優(yōu)先結(jié)束而取消該任務(wù)的執(zhí)行,例如在達(dá)到耗時閾值后,剖析任務(wù)已經(jīng)或準(zhǔn)備開始執(zhí)行,但主線程取消了剖析任務(wù)這樣一個臨界點,此時可通過各語言的同步機制來及時取消剖析任務(wù)。
任務(wù)執(zhí)行時,剖析線程將周期性采集線程的堆棧,而為了方便后續(xù)的分析工作,也會同時記錄當(dāng)前堆棧產(chǎn)生的時間戳,直到業(yè)務(wù)線程發(fā)出中斷通知,或采集樣本數(shù)達(dá)到上限,或任務(wù)狀態(tài)發(fā)生改變,然后中斷剖析線程的執(zhí)行。
執(zhí)行完成后,將采集到的線程棧集 push 到診斷數(shù)據(jù)隊列,等待數(shù)據(jù)導(dǎo)出線程消費此隊列,并發(fā)送到服務(wù)端。這里需要注意,線程棧數(shù)據(jù)文本量一般不會太小,比如我們一個專門用于測試的應(yīng)用,500ms 觸發(fā)的閾值下的 HTTP 接口,每次請求讓線程隨機 Sleep 5s 以內(nèi),當(dāng)接口耗時超過 3s,單次剖析產(chǎn)生的棧文本大小在 200KB 以上,因此這里需要有個參數(shù),來控制隊列默認(rèn)長度,避免過多的堆棧快照擠兌內(nèi)存。
整個任務(wù)執(zhí)行流程如下圖所示:
圖片
數(shù)據(jù)預(yù)聚合&導(dǎo)出
預(yù)聚合工作將由獨立的工作線程消費診斷數(shù)據(jù)隊列后來做,即將多個線程快照合并為一個,降低網(wǎng)絡(luò) IO 開銷。具體就是對于快照集中每個快照的棧幀,按照它的開始時間取快照集中相同棧幀的最小值,結(jié)束時間取快照集中相同棧幀的最大值這個規(guī)則進(jìn)行聚合,流程如下圖所示:
圖片
而數(shù)據(jù)發(fā)送層就比較簡單了,采用高性能無鎖隊列 Mpsc, 使用 gRPC 協(xié)議發(fā)送到診斷服務(wù)端:
圖片
當(dāng)然,為了降低業(yè)務(wù)系統(tǒng)的壓力,也可以將原始數(shù)據(jù)直接落盤,由外部獨立的采集器逐行采集然后發(fā)送到消息隊列。
服務(wù)端設(shè)計
圖片
服務(wù)端的架構(gòu)主要考慮三個點:
數(shù)據(jù)接收
同 Otel 對 Trace 數(shù)據(jù)的處理思路類似,診斷數(shù)據(jù)發(fā)送請求需要快速地被響應(yīng),來減少客戶端因請求延遲導(dǎo)致發(fā)送隊列數(shù)據(jù)被丟棄的可能。因此,診斷服務(wù)端采用吞吐性能較好的 go 語言編寫,而請求涉及到跨語言調(diào)用,協(xié)議層上,綜合高效快速可靠因素,選用較成熟的 gRPC 協(xié)議進(jìn)行通信。
數(shù)據(jù)接收并成功解析后,需異步將數(shù)據(jù)放入隊列,這里我們選用采用了多副本機制的 Kafka 消息中間件,來滿足診斷服務(wù)各模塊之間的解耦,同時也保證診斷數(shù)據(jù)不丟失 。
數(shù)據(jù)解析&加工
診斷剖析數(shù)據(jù)消費組會去消費隊列中的數(shù)據(jù),將數(shù)據(jù)進(jìn)行進(jìn)一步解析,并且持久化處理,其中包括:
- a) 父子棧幀推導(dǎo)
客戶端的預(yù)聚合會將多個快照合并為一個,因此快照內(nèi)的每個棧幀將擁有不同的起始和結(jié)束時間。由于 Java 的原始線程堆棧是無層級的結(jié)構(gòu),為了提高數(shù)據(jù)的可讀性,進(jìn)一步降低高耗時問題定位發(fā)現(xiàn)的成本,因此需將已合并的堆棧進(jìn)一步推導(dǎo)為包含父子棧幀的結(jié)構(gòu)化信息,即從棧頂?shù)牡诙€棧幀開始遍歷調(diào)用棧,若當(dāng)前棧幀的快照開始和結(jié)束的時間范圍位于上個棧幀的左開右閉或左閉右開區(qū)間,則將當(dāng)前棧幀設(shè)置為上個棧幀的子棧:
圖片
注意:
1. 一個 Java 線程的大部分調(diào)用棧形式本身就是個從 "Thread.run" 開始的嵌套,而每次快照時也無從得知層級信息,因此不考慮推導(dǎo)快照開始和結(jié)束時間完全一致的棧幀,將這些棧幀置為同級即可。
2. 使用線程的快照時間來推導(dǎo)還原父子棧和耗時仍然是個相對比較粗略的統(tǒng)計行為,其精度受到當(dāng)前線程調(diào)用??煺諏?dǎo)出的耗時,以及每次快照的間隔耗時的影響,因此父子層級結(jié)果僅供參考,并不絕對等于實際調(diào)用的關(guān)系結(jié)果。
- b) 自身耗時計算
當(dāng)已推導(dǎo)出父子棧幀關(guān)系后,可對結(jié)果集進(jìn)行遍歷,計算自身耗時,計算規(guī)則如下:
- 從第二個棧幀開始,如果與上一個棧幀的快照開始和結(jié)束時間一致,則上個棧幀的自身耗時設(shè)為 0,否則會將當(dāng)前棧幀的父棧幀(若存在)的自身耗時減掉上個棧幀的自身耗時。
- 如果當(dāng)前棧幀是最后一個,則將當(dāng)前棧幀自身耗時設(shè)為快照開始與結(jié)束的時間差,并且將當(dāng)前棧幀的父棧幀(若存在)的自身耗時減掉當(dāng)前棧幀的自身耗時。
- 如果當(dāng)前棧幀有子棧幀,處理方式同上。
以上圖調(diào)用時序為例,根據(jù)以上規(guī)則得出的自身耗時計算示意圖如下:
圖片
- c) 數(shù)據(jù)持久化
當(dāng)完成父子棧幀推導(dǎo)和自身耗時計算后,數(shù)據(jù)將持久化存儲,例如將數(shù)據(jù)存儲到 ClickHouse,供數(shù)據(jù)查詢端使用。
數(shù)據(jù)查詢
診斷剖析數(shù)據(jù)將以 HTTP API 形式對外提供查詢服務(wù),例如可觀測性門戶系統(tǒng),可根據(jù)線程名,鏈路 Trace ID, Span ID 等特征進(jìn)行剖析數(shù)據(jù)的查詢。
[
{
"data": "YXQgc3VuLm5pby5jaC5Vd...",//線程剖析棧
"thread_name": "XNIO-1 I/O-1",//線程名稱
"thread_state": "RUNNABLE",//線程狀態(tài)
"trigger_millisecond": 500,//觸發(fā)閾值
"self_millisecond": 38,//自身耗時
"source_snapshot_count": 153//快照數(shù)
},
{
"data": "YXQgaW8udW5kZXJ0b3cuc2Vy...",
"thread_name": "XNIO-1 task-1",
"thread_state": "RUNNABLE",
"trigger_millisecond": 500,
"self_millisecond": 0,
"source_snapshot_count": 140
}
]
調(diào)用鏈關(guān)聯(lián)
線程剖析能結(jié)合 OpenTelemetry ,借助 OpenTelemetry Java Instrumentation 上下文的生命周期,從而關(guān)聯(lián) Trace ID 、接口名等鏈路特征。
圖片
自身監(jiān)控指標(biāo)
線程剖析功能需要擁有較完善的自身監(jiān)控,以便觀測復(fù)雜剖析流程下對業(yè)務(wù)系統(tǒng)潛在的性能影響。這些監(jiān)控包括:
- 任務(wù)檢測隊列大小
檢測隊列用于給時間輪提供任務(wù),該指標(biāo)的大小給線程剖析的采樣,接口名,線程名稱等條件提供了一定參考。
圖片
- 任務(wù)釋放平均耗時
剖析任務(wù)的釋放將會中斷正在執(zhí)行的剖析任務(wù),其中涉及到剖析、數(shù)據(jù)狀態(tài)機的改變,線程的中斷。多線程情況下,需保證操作的原子性,如果任務(wù)釋放的平均耗時變長,則能反映當(dāng)前業(yè)務(wù)系統(tǒng) CPU 線程上下文切換效率下降。
圖片
- 正在執(zhí)行剖析任務(wù)的個數(shù)
線程剖析是以線程為單位來執(zhí)行的,通過觀測正在進(jìn)行線程剖析的任務(wù)數(shù),可反映出剖析功能繁忙的程度,以及幫助我們決策是否需要對同時剖析的任務(wù)數(shù)進(jìn)行限制。
圖片
- 線程堆棧導(dǎo)出平均耗時
線程棧導(dǎo)出方法的平均耗時,如果該操作耗時顯著升高,且調(diào)用棧未有明顯變化,則代表性能惡化。
圖片
- 數(shù)據(jù)隊列大小
指待發(fā)送到服務(wù)端的數(shù)據(jù)隊列大小。
- 數(shù)據(jù)入隊速率
指待發(fā)送到服務(wù)端的數(shù)據(jù)入隊的速率。
- 數(shù)據(jù)合并平均耗時
數(shù)據(jù)發(fā)送前進(jìn)行預(yù)聚合,將多個線程快照合并為一個,這個過程的平均耗時,該值可供剖析條件提供一定參考。
圖片
- 線程??煺諏?dǎo)出發(fā)送平均字節(jié)
線程快照發(fā)送的請求包平均大小。
圖片
- 數(shù)據(jù)導(dǎo)出速率
線程快照發(fā)送的速率。
圖片
對以上指標(biāo)進(jìn)行監(jiān)控,也方便對相關(guān)參數(shù)進(jìn)行調(diào)優(yōu),從而更好地在診斷剖析功能的完整性與服務(wù)性能之間做相關(guān)取舍。
三、結(jié)語
線程剖析為解決性能問題提供了有力支持。通過采集和分析線程棧信息,它能夠幫助開發(fā)人員定位應(yīng)用程序中的高耗時問題,為性能優(yōu)化提供關(guān)鍵信息。本文詳細(xì)介紹了線程剖析的基本思想和實現(xiàn)思路,以及客戶端和服務(wù)端的設(shè)計架構(gòu)。其核心思想是通過創(chuàng)建特定閾值觸發(fā)的檢測任務(wù),監(jiān)測高耗時問題,并將采集到的數(shù)據(jù)異步發(fā)送到剖析服務(wù)端進(jìn)行進(jìn)一步計算和分析。
此外,線程剖析的自身監(jiān)控指標(biāo),這些指標(biāo)有助于更好地了解剖析功能的性能和繁忙程度,以便進(jìn)行決策和調(diào)優(yōu)。線程剖析不僅提供了性能數(shù)據(jù),還可以與 OpenTelemetry 相結(jié)合,實現(xiàn)鏈路特征的關(guān)聯(lián),從而更全面地理解性能問題。
總的來說,線程剖析可以幫助開發(fā)人員提高應(yīng)用程序的質(zhì)量和性能,快速定位性能問題,以確保應(yīng)用程序的順暢運行,同時,也可以更有效地應(yīng)對性能挑戰(zhàn),提高應(yīng)用程序的可維護(hù)性和性能。