新提案:Goroutine 運(yùn)行時(shí)追蹤難題,終于要解決了!
今天給大家分享一個(gè)挺有意思的 Go 提案,是關(guān)于在runtime.GoroutineProfile中新增暴露goid和gopc字段。
圖片
乍一眼一看。可能有些同學(xué)會(huì)疑惑,這兩個(gè)字段是干嘛的,為啥要暴露它們?
這背后涉及到 Go 運(yùn)行時(shí)性能分析的一個(gè)老大難 “訴求”。
背景
先來(lái)說(shuō)說(shuō)需求的上下文背景。主要訴求是:在 Go 的性能分析場(chǎng)景中,我們經(jīng)常需要追蹤 Goroutine 的執(zhí)行情況。
目前主流的做法有兩種:
- 第一種:使用
runtime.GoroutineProfile()函數(shù),它能返回當(dāng)前所有 Goroutine 的堆棧信息。但很無(wú)奈的是,返回的StackRecord結(jié)構(gòu)體中沒(méi)有包含任何能夠跨采樣周期識(shí)別同一個(gè) Goroutine 的標(biāo)識(shí)符。 - 第二種:調(diào)用
runtime.Stack(..., true)來(lái)獲取所有 Goroutine 的堆棧跟蹤,但這個(gè)方法 CPU 開(kāi)銷相當(dāng)大,在 Goroutine 數(shù)量多的時(shí)候基本不可用。
如果使用分析工具想要追蹤特定 Goroutine 在多個(gè)時(shí)間點(diǎn)的行為變化。
是沒(méi)有合適的標(biāo)識(shí)符來(lái)關(guān)聯(lián)這些數(shù)據(jù)的。這很尷尬了。
例子
讓我們看個(gè)具體例子來(lái)理解這個(gè)問(wèn)題:
package main
import (
"fmt"
"runtime"
"time"
)
func worker(id int) {
for {
// 模擬一些工作
time.Sleep(time.Millisecond * 100)
fmt.Printf("Worker %d is running\n", id)
}
}
func main() {
// 啟動(dòng)幾個(gè)worker goroutine
for i := 0; i < 3; i++ {
go worker(i)
}
// 定期采集goroutine profile
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for i := 0; i < 3; i++ {
<-ticker.C
// 獲取當(dāng)前的goroutine profile
profiles := make([]runtime.StackRecord, 10)
n, ok := runtime.GoroutineProfile(profiles)
if !ok {
profiles = make([]runtime.StackRecord, n)
n, _ = runtime.GoroutineProfile(profiles)
}
fmt.Printf("=== Sample %d ===\n", i+1)
for j, profile := range profiles[:n] {
fmt.Printf("Goroutine %d: %d stack frames\n", j, len(profile.Stack0))
// 問(wèn)題:無(wú)法知道這個(gè)goroutine的ID,無(wú)法跨采樣關(guān)聯(lián)
}
}
}運(yùn)行這段代碼,你會(huì)發(fā)現(xiàn)每次能采樣得到的StackRecord數(shù)組,是無(wú)法確定哪個(gè)記錄對(duì)應(yīng)的是同一個(gè) Goroutine,無(wú)法知道明確的 GoroutineID。
這對(duì)于性能分析來(lái)說(shuō)是個(gè)致命問(wèn)題。因?yàn)閴焊P(guān)聯(lián)不上。誰(shuí)來(lái)了都沒(méi)辦法。只能另外想辦法關(guān)聯(lián)上了。
提案內(nèi)容
這個(gè)提案最初由 Sentry 團(tuán)隊(duì)的工程師提出,他們?cè)陂_(kāi)發(fā) Go SDK 的性能分析功能時(shí)遇到了這個(gè)困擾。
圖片
簡(jiǎn)單來(lái)說(shuō),他們希望在StackRecord中添加兩個(gè)字段:
goid:Goroutine 的唯一標(biāo)識(shí)符gopc:Goroutine 的起始程序計(jì)數(shù)器
但直接在現(xiàn)有的StackRecord結(jié)構(gòu)體中添加字段也不太合適,因?yàn)?/span>StackRecord在其他地方也有使用,貿(mào)然添加字段會(huì)影響其他方面的兼容性。
可能的解決方案:
新增一個(gè)專門(mén)用于 Goroutine 分析的結(jié)構(gòu)體去記錄:
// 假設(shè)的新結(jié)構(gòu)體
type GoroutineRecord struct {
ID int64 // goroutine ID
GoPC uintptr // goroutine起始PC
Stack []uintptr // 調(diào)用棧
// 其他相關(guān)字段
}
// 對(duì)應(yīng)的新API
func GoroutineProfileWithID() []GoroutineRecord {
// 實(shí)現(xiàn)細(xì)節(jié)...
}這樣就能在采樣時(shí)獲取到 Goroutine 的身份信息了:
func trackGoroutines() {
// 第一次采樣
sample1 := runtime.GoroutineProfileWithID()
time.Sleep(time.Second)
// 第二次采樣
sample2 := runtime.GoroutineProfileWithID()
// 現(xiàn)在可以通過(guò)ID關(guān)聯(lián)同一個(gè)goroutine了
for _, g1 := range sample1 {
for _, g2 := range sample2 {
if g1.ID == g2.ID {
fmt.Printf("Goroutine %d 在兩次采樣中都存在\n", g1.ID)
// 可以分析這個(gè)goroutine的行為變化
}
}
}
}社區(qū)討論
這個(gè)提案在 Go 社區(qū)引起了不少關(guān)注。Go 核心團(tuán)隊(duì)的成員@cherrymui 指出,如果要實(shí)現(xiàn)這個(gè)功能,可能還需要考慮在 pprof 格式的 goroutine profile 中也添加類似的支持。
從技術(shù)實(shí)現(xiàn)角度看,這個(gè)功能的核心在于在 saveg()函數(shù)中將 goroutine 的 ID 和起始 PC 信息復(fù)制到 StackRecord 中。但正如提案作者所說(shuō),這樣做會(huì)讓 StackRecord 變得不夠通用。
我個(gè)人覺(jué)得,這個(gè)提案解決的是一個(gè)實(shí)際存在的痛點(diǎn)。目前很多性能分析工具都受限于無(wú)法追蹤特定 Goroutine 的生命周期,這大大降低了分析的精度。
Sentry 團(tuán)隊(duì)甚至因?yàn)檫@個(gè)問(wèn)題移除了他們 SDK 中的運(yùn)行時(shí)性能分析功能。
實(shí)際影響
讓我們來(lái)看看這個(gè)功能對(duì)實(shí)際開(kāi)發(fā)的價(jià)值。
假設(shè)我們有一個(gè) Web 服務(wù),想要分析某些慢請(qǐng)求的根因:
// 當(dāng)前的困境:無(wú)法跟蹤特定請(qǐng)求的goroutine
func analyzeSlowRequests() {
// 采樣1:請(qǐng)求剛開(kāi)始
profiles1 := getGoroutineProfile()
// 等待一段時(shí)間
time.Sleep(5 * time.Second)
// 采樣2:請(qǐng)求可能仍在處理
profiles2 := getGoroutineProfile()
// 問(wèn)題:無(wú)法確定哪個(gè)goroutine處理的是同一個(gè)請(qǐng)求
// 只能看到所有g(shù)oroutine的快照,但無(wú)法關(guān)聯(lián)
}
// 有了新功能后的可能性
func analyzeSlowRequestsWithID() {
profiles1 := getGoroutineProfileWithID()
time.Sleep(5 * time.Second)
profiles2 := getGoroutineProfileWithID()
// 可以精確跟蹤特定goroutine的執(zhí)行軌跡
for _, g1 := range profiles1 {
for _, g2 := range profiles2 {
if g1.ID == g2.ID {
// 分析同一個(gè)goroutine在不同時(shí)間點(diǎn)的棧信息
// 找出性能瓶頸所在
}
}
}
}這種能力對(duì)于診斷復(fù)雜的性能問(wèn)題非常有價(jià)值,特別是在微服務(wù)架構(gòu)中,一個(gè)請(qǐng)求可能會(huì)創(chuàng)建多個(gè) goroutine 來(lái)處理不同的子任務(wù)。
總結(jié)
這個(gè)提案雖然看起來(lái)只是在現(xiàn)有 API 中添加兩個(gè)字段,但解決的是 Go 運(yùn)行時(shí)性能分析的一個(gè)基礎(chǔ)能力缺失。
從提案的討論情況來(lái)看,已經(jīng)進(jìn)入了 active 狀態(tài):
圖片
這說(shuō)明 Go 核心團(tuán)隊(duì)正在認(rèn)真考慮。
我覺(jué)得這個(gè)功能的落地只是時(shí)間問(wèn)題,畢竟它解決的是一個(gè)廣泛存在的實(shí)際需求。對(duì)于那些依賴精確性能分析的工具和服務(wù)來(lái)說(shuō),這將是一個(gè)重要的改進(jìn)。
后續(xù)就看 Go 核心團(tuán)隊(duì)最終會(huì)選擇什么樣的實(shí)現(xiàn)方案了。是直接擴(kuò)展現(xiàn)有的 API,還是新增專門(mén)的 API,都挺值得期待的。































