為忙碌開發者準備的 Go 語言效能分析、追蹤和可觀測性指南
原文地址:https://github.com/DataDog/go-profiler-notes/blob/main/guide/README.md
原文作者:Felix Geisendörfer
譯者:cvley
校對:
簡介: 本文內容 · Go 語言的心智模型 · 效能分析與追蹤
使用場景: 降低成本 · 降低延遲 · 記憶體洩露 · 程式掛起(Hanging)· 中斷
Go 效能分析: CPU · 記憶體 · Block · Mutex · Goroutine · ThreadCreate
效能分析視覺化: 命令列 · 火焰圖 · 瀏覽器圖
Go 執行追蹤: 時間線視覺化 · 派生分析
其他工具: time · perf · bpftrace
高階話題: 彙編 · 棧追蹤
Datadog 產品: 持續效能分析器 · APM(分散式追蹤)
? 本文還在不斷撰寫過程中。上面列出的部分會陸續有對應的可點選的地址。關注我的 twitter 獲取更多進展。
簡介 本文內容 本文是實踐指南,目標讀者是那些想要通過使用效能分析和追蹤技術來提升程式的忙碌 gopher。如果你還不熟悉 Go 的內部原理,建議你先閱讀整個簡介。之後你就可以自由閱讀感興趣的章節。
Go 的心智模型 在不理解 Go 語言底層執行機制的情況下,成為一個熟練編寫 Go 程式碼的開發者是可能的。但當面對效能分析和除錯時,理解內部的心智模型將大有裨益。因此下面我們將展示 Go 的基礎模型。這個模型應該足夠讓你避免絕大多數常見的錯誤,但是 所有的模型都是錯誤的,因此鼓勵你探索更深層的資料,以便在將來解決更難的問題。
Go 的首要工作是複用和抽象硬體資源,與作業系統相似。通常使用兩個主要的抽象來實現:
Goroutine 排程器: 管理程式碼如何在系統的 CPU 上執行。 垃圾回收器:提供虛擬記憶體,在需要時自動釋放。 Goroutine 排程器 我們先使用下面的例子來討論排程器:
func main() { res, err := http.Get("https://example.org/") if err != nil { panic(err) } fmt.Printf("%d\n", res.StatusCode) } 上面我有一個執行 main 函式的 goroutine,稱之為 G1。下圖展示了這個 goroutine 可能在單個 CPU 上執行的簡化版的時間線。首先在 CPU 上執行的 G1 用於準備 http 請求。接下來在 goroutine 等待網路時, CPU 變成閒置(idle)狀態。最後它會再次被排程到 CPU 上並列印出狀態碼。
從排程器的角度看,上面的程式的執行情況如下圖所示。首先, G1 在 CPU 1 上 執行。接下來 goroutine 在等待 網路時離開 CPU。一旦發現網路有響應(使用非阻塞 I/O,與 Node.js 相似),排程器將 goroutine 標記為 可執行。一旦 CPU 核可用,goroutine 會再次開始 執行。在我們的例子中,所有的 CPU 核都可用,所以 G1 不需要在可執行狀態花費時間,就可以立即回到一個 CPU 上執行 fmt.Printf() 函式。
大多數情況下,Go 程式都執行多個 goroutines,因此會有一些 goroutines 在部分 CPU 核上執行,大量的 goroutines 因為各種原因等待,理想情況下沒有 goroutines 在可執行狀態,除非程式佔用了非常高的 CPU 負載。示例如下圖所示。
當然上面的模型忽略了非常多的細節。實際上正相反,Go 排程器執行在作業系統管理的執行緒之上,甚至 CPU 本身也能夠有超執行緒這樣的排程形式。所以如果你感興趣,可以通過 Ardan 的 Go 排程 這一實驗室系列文章或相似的資料,像愛麗絲一樣繼續在這個兔子洞中繼續探索。
然而,上面的模型已足夠用於理解本文餘下的內容。特別是可以明確一點,對於不同 Go 效能分析器所衡量的時間,本質上應該是 goroutine 在執行和等待狀態上花費的時間,如下圖所示。
垃圾回收器 Go 的另一個主要抽象是垃圾回收期。像 C 語言這樣的語言,開發者需要通過 malloc() 和 free() 來手動分配和釋放記憶體。這提供了巨大的控制權,但實際上卻非常容易出錯。垃圾回收器可以減少這個負擔,但記憶體的自動管理很容易成為效能瓶頸。這部分內容將展示 Go 語言 GC 的一個簡單模型,對於發現和優化記憶體管理相關的問題非常有用。
堆疊 我們從基礎開始。Go 可以在兩個地方分配記憶體,棧或堆。各個 goroutine 都有各自的棧,它們是記憶體的一段連續區域。此外還有一大塊可以 goroutine 間共享的記憶體區域,叫做堆。如下圖所示。
當一個函式呼叫另一個函式時,它會獲取自身棧上叫做棧幀的區域,用於存放區域性變數。棧指標用於標示幀中下一個可用的位置。當函式返回時,通過將棧指標移回到之前幀的末尾這個簡單的方法,將最後幀中的資料丟棄。幀中的資料本身還會存在於棧上,並在下次函式呼叫時被覆蓋。這麼做非常簡單高效,因為 Go 不需要追蹤每個變數。
為了更直觀地表述,我們來看下面的例子:
func main() { sum := 0 sum = add(23, 42) fmt.Println(sum) }
func add(a, b int) int { return a + b } 其中,我們有個 main() 函式,開始時會在棧上為變數 sum 預留一些空間。當 add() 函式被呼叫時,它會在自己的幀上保留區域性的 a 和 b 引數。一旦 add() 函式返回,棧指標會移回到 main() 函式幀的末尾,這樣資料就被丟棄了,而 sum 變數會更新為結果的值。同時 add() 的舊值在下次函式呼叫重寫覆蓋棧指標之前,還會保留。下圖是這個過程的視覺化圖:
上面的例子是對返回值、幀指標、返回地址和函式嵌入等地高度簡化,並省略了大量細節。實際上,在 Go 1.17 中,上面的程式可能並不會需要任何棧空間,因為編譯器可以使用 CPU 暫存器來管理小量的資料。但這樣沒問題。這個模型對於重要的 Go 程式分配和丟棄棧上區域性變數的方式依舊有意義。
此時你可能想知道的一點是,如果棧上的空間用完了會發生什麼。在像 C 這樣的語言中,這會導致一個棧溢位的錯誤。而 Go 可以通過複製出一個 2 倍的棧來自動解決這個問題。這讓 goroutines 可以使用非常小,一般 2KiB 的棧空間來啟動,而這也是讓 goroutines 比作業系統執行緒更可擴充套件的成功因素。
棧的另一個要素是用於建立棧追蹤的方式。這有點過於高階,但如果你感興趣,可以查閱本專案的 Go 棧追蹤 的文件。
堆 棧分配很棒,但在許多場景下 Go 卻無法使用。最常見的一個就是返回一個函式的區域性變數的指標。把上面的 add() 示例做些修改,就可以看到這個問題:
func main() { fmt.Println(*add(23, 42)) }
func add(a, b int) *int { sum := a + b return &sum } 正常情況下,Go 可以在 add() 函式內部的棧上分配 sum 變數。但正如我們所知,這個資料在 add() 函式返回時會被丟棄。因此為了安全地返回 &sum 指標,Go 需要在棧外的記憶體上為它分配空間。而這就是堆的來源。
堆用於儲存那些生命週期長於建立它們的函式的記憶體,也包括那些使用指標在 goroutine 間共享的資料。然而,這會丟擲這塊記憶體如何釋放的問題。因為不像棧的分配,堆的分配在建立它們的函式返回時並不會丟棄它們。
Go 使用內建的垃圾回收器解決這個問題。它的實現細節非常複雜,但粗略來看,它會如下圖一樣追蹤記憶體的使用情況。其中你可以看到,對於堆上的綠色分片空間,有三個 goroutine 有指標指向它們。這些分配的空間,有一些也會通過指標指向其他綠色的分配空間。另外,灰色分配空間可能會指向綠色分片空間,或者互相指向但並不被綠色分配空間所引用。這些分配空間曾經可以訪問,但現在被認為是垃圾。當分配棧指標的函式返回時,或值被覆蓋重寫時,就會發生這種情況。GC 的職責是自動發信並釋放這些分配空間。
GC 操作包括大量耗時的圖遍歷和快取命中。它甚至包括了停止整個程式執行的 stop-the-world 的階段。幸運的是,Go 的最近版本已經把這個耗時降低毫秒之下,但許多剩餘的問題與 GC 有關。事實上,一個 Go 程式 20%
-30% 的執行時間都花在記憶體管理上,而這非常常見。
一般來說,GC 的花費與程式進行的堆分配空間成正比。因此當優化程式記憶體相關的情況時,箴言如下:
降低: 嘗試將堆記憶體分配改為棧記憶體分配,或避免同時發生兩種分配的情況。 重用: 重複使用堆分配空間,而不是使用新的空間。 迴圈: 對於無法避免的堆分配,讓 GC 來迴圈並關注於其他問題。 正如本篇指南中前面的心智模型所說,上面的所有內容都是實際情況的超級簡化。但希望它對於剩餘部分足夠有意義,能啟發你閱讀更多相關內容的文章。其中一篇你應該閱讀的是瞭解 Go:Go 垃圾回收器之旅,它提供了 Go GC 逐年發展的內容和提升規劃。
免責宣告 我是 felixge,就職於 Datadog ,主要工作內容為 Go 的 持續效能優化。你應該瞭解下。我們也在招聘 : ).
本頁面的資訊可認為正確,但不提供任何保證。歡迎反饋!
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- GO語言————6.4 defer 和追蹤Go
- Go 語言效能分析Go
- go語言開發入門:GO 開發者對 GO 初學者的建議Go
- PHP 效能追蹤及分析工具(XHPROF)PHP
- 硬核觀察 #704 谷歌釋出開源開發語言 Carbon,準備替代 C++谷歌C++
- Go 語言基準測試入門Go
- Linux 核心網路包路徑追蹤利器 skbtracer,Go 語言版本LinuxGo
- OpenTelemetry - 雲原生下可觀測性的新標準
- Go語言開發者福利 - 國內版 The Go PlaygroundGo
- Go語言開發者福利 – 國內版 The Go PlaygroundGo
- Go 語言的詞法分析和語法分析(1)Go詞法分析語法分析
- go的鏈路追蹤Go
- Rust 語言的全鏈路追蹤庫 tracingRust
- Go語言精進之路讀書筆記第46條——為被測物件建立效能基準Go筆記物件
- Istio可觀測性
- Go語言的前景分析Go
- 為什麼很多公司都轉型go語言開發?Go語言能做什麼Go
- goproxy.cn - 為中國 Go 語言開發者量身打造的模組代理Go
- 探索 Go1.16 io/fs 包以提高測試效能和可測試性Go
- 開源可觀測性平臺SigNoz
- go語言高效能快取元件ccache分析Go快取元件
- 2021年Go語言開發者調查結果Go
- Yaegi,讓你用標準 Go 語法開發可熱插拔的指令碼和外掛Go指令碼
- 如何追蹤Go動態Go
- TiDB 5.3 發版 —— 跨越可觀測性鴻溝,實現 HTAP 效能和穩定性的新飛躍TiDB
- 雲原生ASP.NET Core程式的可監測性和可觀察性ASP.NET
- 如何客觀的評價 Go 語言Go
- Go 語言的詞法分析和語法分析(2)—Import宣告的解析Go詞法分析語法分析Import
- go語言安卓開發Go安卓
- Dapr-可觀測性
- Go 語言區塊鏈測試實踐指南(一):GO單元測試Go區塊鏈
- 那年追過的開發者測試工具
- GO語言必備的五大開源工具!Go開源工具
- go語言標準庫 - timeGo
- go語言標準庫 - strconvGo
- go語言標準庫 - regexpGo
- go語言標準庫 - logGo
- 使用 OpenTelemetry 的 .NET 可觀測性