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