2018 年 2 月 28 日應用的效能問題生來就是無法預料的 —— 而且他們總是在最壞的時間露頭。讓情況更糟的是,很多效能分析工具都是冷冰冰的,複雜難懂的,用起來徹頭徹尾令人困惑的 —— 來自於 `valgrind` 和 `gdp` 這樣最受推崇的效能分析工具的使用者體驗。`Flamegraphs` 是由 linux 效能分析大師 Brendan Gegg 創造的一個工具,在一般的 linux 效能追蹤 dump 之上產生一個 SVG 可視化層 ,給定位和解決效能問題這個複雜的過程帶來了一些“溫暖”。在這篇文章中,我們會一步一步地用 `flamegraphs` 對一個簡單的 golang 寫的 web 應用進行效能分析。## 開始之前的一些題外話只有當你知道自己的程式有效能問題時,才對它進行效能分析和最佳化。過早的效能最佳化不僅會在當下浪費你的時間,而且如果以後你不得不進行重構時,這些精心調整過的脆弱的代碼,會拖慢你。## 樣本程式我們將用一個小的 HTTP 伺服器來示範,這個伺服器通過 `GET /ping` 暴露了一個 healthcheck 的 API. 為了可視化,我們同時包含了一個小的 [statsd](https://www.datadoghq.com/blog/statsd/) 用戶端用來記錄伺服器處理的每個請求的延遲。為了保持簡單,我們的代碼僅僅用到 go 的標準庫,不過即使你習慣使用 `gorilla/mux` 或者別的流行的庫,這些代碼對你來說也不會太陌生。```goimport ( "fmt" "log" "net/http" "strings" "net" "time")// SimpleClient is a thin statsd client.type SimpleClient struct { c net.PacketConn ra *net.UDPAddr}// NewSimpleClient instantiates a new SimpleClient instance which binds// to the provided UDP address.func NewSimpleClient(addr string) (*SimpleClient, error){ c, err := net.ListenPacket("udp", ":0") if err != nil { return nil, err } ra, err := net.ResolveUDPAddr("udp", addr) if err != nil { c.Close() return nil, err } return &SimpleClient{ c: c, ra: ra, }, nil}// Timing sends a statsd timing call.func (sc *SimpleClient) Timing(s string, d time.Duration, sampleRate float64, tags map[string]string) error { return sc.send(fmtStatStr( fmt.Sprintf("%s:%d|ms",s, d/ time.Millisecond), tags), )}func (sc *SimpleClient) send(s string) error { _, err := sc.c.(*net.UDPConn).WriteToUDP([]byte(s), sc.ra) if err != nil { return err } return nil}func fmtStatStr(stat string, tags map[string]string) string { parts := []string{} for k, v := range tags { if v != "" { parts = append(parts, fmt.Sprintf("%s:%s", k, v)) } } return fmt.Sprintf("%s|%s", stat, strings.Join(parts, ","))}func main() { stats, err := NewSimpleClient("localhost:6060") if err != nil { log.Fatal("could not start stas client: ", err) } // add handlers to default mux http.HandleFunc("/ping", pingHandler(stats)) s := &http.Server{ Addr: ":8080", } log.Fatal(s.ListenAndServe())}func pingHandler(s *SimpleClient) http.HandlerFunc{ return func(w http.ResponseWriter, r *http.Request) { st := time.Now() defer func() { _ = s.Timing("http.ping", time.Since(st), 1.0, nil) }() w.WriteHeader(200) }}```## 安裝效能分析工具go 的標準庫內建了診斷效能問題的工具,有一套豐富而完整的工具可以嵌入 go 簡單高效的運行時。如果你的應用使用的是預設的 `http.DefaultServeMux` ,那麼整合 `pprof` 無需額外的代碼,只需在你的 `import` 頭部加入以下語句。```goimport ( _ "net/http/pprof")```你可以通過啟動伺服器並在任意瀏覽器訪問 `debug/pprof` 來驗證你的配置是否都正確。對我們的樣本應用來說 —— pprof 介面暴露在 `localhost:8080/debug/pprof` 。## 產生 Flamegraph`flamegraph` 工具的工作是接收你系統的已有的一個堆疊追蹤檔案,進行解析,產生一個 SVG 可視化。為了得到其中一個神秘的堆疊追蹤檔案,我們可以使用隨 go 一起安裝的 [pprof](https://github.com/google/pprof) 工具。為了將東西整合在一起,免受安裝和配置更多軟體之苦,我們將使用 [uber/go-torch](https://github.com/uber/go-torch) 這個出色的庫 —— 它為整個過程提供了非常方便的集裝箱式的工作流程。Flamegraph 可以產生自各種各樣的設定檔,每一個都針對不同的效能指標。你可以使用同樣的工具包和方法論來尋找 CPU 的效能瓶頸、記憶體流失,甚至是死結的進程。下面來為我們的樣本應用產生一個 `flamegraph`,執行以下命令來抓取 `uber/go-torch` 容器並將其指向你的應用。```# run for 30 secondsdocker run uber/go-torch -u http://<host ip>:8080/debug/pprof -p -t=30 > torch.svg```### 產生請求負載如果你的應用伺服器運行在本地,或者是在 staging 環境,複現最初讓系統出現效能問題的情境可能很困難。作為類比生產環境的工作負載的一種途徑,我們將使用一個叫做 [vegeta](https://github.com/tsenart/vegeta) 的負載測試小工具來類比出與線上每台伺服器處理的請求相當的輸送量。`vegeta` 有一個極其強大的可配置API,支援不同類型的負載測試及基準測試情境。在我們的簡單伺服器的案例裡,我們可以通過以下單行指令來產生足夠的流量,來讓事情變得有趣。```# send 250rps for 60 secondsecho "GET http://localhost:8080/ping" | vegeta attack -rate 250 -duration=60s | vegeta report```運行這個指令碼的同時用 `go-torch` 工具進行監聽,就可以產生一個名為 `torch.svg` 的檔案。用 Chrome 開啟這個檔案,就會有一個與你的程式對應的漂亮的 flamegraph 跟你打招呼!```open -a `Google Chrome` torch.svg```![](https://raw.githubusercontent.com/studygolang/gctt-images/master/flamegraphs/1.png)## 讀懂 Flamegraphflamegraph 中的每一個水平區段代表一個棧幀,決定它的寬度的是採樣過程中你的程式被觀察到在對這個幀進行求值的相對(%)時間。這些區段在垂直方向上根據在調用棧中的位置被組織成一個個的 "flame",也就是說在圖的 y軸方向上位於上方的函數是被位於下方的函數調用的 —— 自然地,在上方的函數比下方的函數佔用了更小片的 CPU 時間。如果你想深入視圖中的某一部分,很簡單,只需要點擊某一幀,這樣位於這幀下方的幀都會消失,而且介面會自己調整尺寸。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/flamegraphs/2.png)*註:棧幀的顏色是無意義的,完全隨機——色調和色度的區分是為了讓圖更易讀。*通過直接查看或是點擊幾個幀來縮小範圍——有沒有效能問題及問題是什麼應該會即刻變得顯而易見。記住 [80/20 規律](https://en.wikipedia.org/wiki/Pareto_principle) ,你的大部分效能問題都會出現在做了比它該做的多得多的少部分代碼上——別把你的時間花在flamegraph圖表中那些薄小的穗上。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/flamegraphs/3.png)舉例來說,在我們的程式中,我們可以深入到那些較大片的區段,看到我們花了10%(!) 的時間在將結果通過網路 socket 刷到我們的統計伺服器!幸運的是,修複這個很簡單——通過在我們的代碼中添加一個小的buffer,我們可以解決這個問題,然後產生一個新的,更纖細的圖。### Code Change```gofunc (sc *SimpleClient) send(s string) error { sc.buffer = append(sc.buffer, s) if len(sc.buffer) > bufferCapacity { b := strings.Join(sc.buffer, ",") _, err := sc.c.(*net.UDPConn).WriteToUDP([]byte(b), sc.ra) if err != nil { return err } sc.buffer = nil } return nil}```### New flamegraph![](https://raw.githubusercontent.com/studygolang/gctt-images/master/flamegraphs/4.png)就是它了!Flamegraph是個窺測你的應用效能的簡單且強大的工具。試著為你的應用產生一個 flamegraph——你的發現可能會給你帶來驚喜:)## 擴充閱讀想學習更多?這裡有些不錯的連結:- [Flamegraphs - Brendan Gegg](http://www.brendangregg.com/flamegraphs.html)- [The Flame Graph - ACMQ](https://queue.acm.org/detail.cfm?id=2927301)- [The Mature Optimization Handbook](https://www.facebook.com/notes/facebook-engineering/the-mature-optimization-handbook/10151784131623920/)- [Profiling and Optimizing go Applications](https://www.youtube.com/watch?v=N3PWzBeLX2M)
via: http://brendanjryan.com/golang/profiling/2018/02/28/profiling-go-applications.html
作者:Brendanj Ryan 譯者:krystollia 校對:rxcai
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
109 次點擊