開發

Go內存分配跟蹤調優

廣告
廣告

今天小編為大家分享一篇關于Go內存分配跟蹤調優的文章,文中涉及到一些壓測及跟蹤分析的工具,以及問題查找方法,希望能對大家有所幫助。

Make it work, make it right, make it fast.

– Kent Beck

最近,我決定對我的一個開源的 Go 項目 Flipt 進行一下深入分析,看能不能找到一些容易獲得成績的點,來優化一下,從而獲得性能方面的提升。因為在項目中,路由和中間件都是使用的開源的,所以在這個過程中,我也可以對一些流行的開源項目進行分析,進而可以發現一些它們存在的問題。最終,我的項目可以減少近100倍的內存分配,因此也減少了 GC 回收次數并提高了整體性能。那么來看下我是怎么做到的呢。

1

生成

在開始分析之前,需要先生成大量的網絡請求,有了流量才能看到觀察到項目中目前存在的問題。這就有個問題,因為我沒有在生產環境的項目上使用過 Flipt,那也就沒有真正的流量。所以,我使用一款多功能的 HTTP 壓測工具, Vegeta,來生成模擬流量。

這個很符合我的需求,它可以在某段時間持續的產生請求,我就可以測量諸如堆分配、堆使用情況、goroutine以及 GC 耗費的時間之類的情況。

經過一些試驗,最終得到以下命令:

echo 'POST http://localhost:8080/api/v1/evaluate' | vegeta attack -rate 1000 -duration 1m -body evaluate.json

這個命令以攻擊模式來啟動 vegeta,以1000次每秒的速率發送 HTTP POST 請求到 Flipt 的 REST API,持續一分鐘。發送給 Flipt 用的 JSON 載荷可以不用關心,只要是 Flipt 服務器可以接收的包體即可。

我首先準備向 evaluate 這個接口開炮,因為它里面邏輯比較復雜,在后端有很多的復雜計算,所以應該更有可能暴露出問題。

2

測量

既然我們有解決了流量的問題,那么我們就需要測量,在項目運行時這些流量產生的實際結果。很幸運,Go 自身就提供了一套非常出色的標準工具,我們可以用 pprof 來衡量我們的 Go 應用的性能。關于 pprof 的詳細信息,我們在此不深入討論,后面可能會寫相應的文章來單獨介紹。

因為在 Flipt 中,我是使用了 go-chi/chi 作為 HTTP 路由,所以在項目中可以很簡單的使用 Chi 的配置中間件,來啟用 pprof。

我們再開啟一個窗口來獲取并查看堆的剖面信息:

pprof -http=localhost:9090 localhost:8080/debug/pprof/heap

這里我使用了 Google 的 pprof 工具,該工具可以直接在瀏覽器中可視化剖面數據。

首先,我檢查了 inuse_objects 和 inuse_space 來查看堆現場,但是并沒有真正注意到太多內容。但是,當我切換到 alloc_objects 和 alloc_space 的時候,才真正勾起我的興趣。

看起來好像是調用了 flate.NewWriter,并且在一分鐘的時間內分配了 19370 MB的內存。已經超過 19G!很顯然這其中發生了什么事情。但是呢?如果放大圖并仔細查看,可以看到 flate.NewWriter 是從gzip.(* Writer).Write 調用的,而它是從 middleware.(* compressResponseWriter).Write 調用的。很快我意識到這與 Flipt 本身的代碼無關,而是因為我使用了 Chi 的壓縮中間件庫,該庫中提供的 API 響應的壓縮。

// 沒錯,就是這一行
r.Use(middleware.Compress(gzip.DefaultCompression))

我注釋掉了上面的代碼行,然后重新運行了一下性能測試,并且發現大量的內存分配已經消失了!

在尋求解決方案之前,我想對這些分配以及它是如何影響性能的(尤其是花在 GC 上的時間),有一個新的認識。我想到了 Go 有一個工具 trace 可以讓我們查看某段時間內 Go 程序的執行情況,包括一些重要的統計信息,例如堆使用情況、正在運行的數量、網絡和系統調用以及對我們最重要的 GC 耗時。

為了有效的捕獲跟蹤信息,我們需要降低 Vegeta 請求速率,因為經常得到服務器返回的 socket: too many open files 錯誤。可能是與我本地機器上的 ulimit 設置太小有關。

重新運行 Vegeta:

echo 'POST http://localhost:8080/api/v1/evaluate' | vegeta attack -rate 100 -duration 2m -body evaluate.json

相比較,現在每秒的請求數量是之前的1/10,但是時間更長,因此我們能夠捕獲有效的跟蹤信息。

另外一個窗口:

wget 'http://localhost:8080/debug/pprof/trace?seconds=60' -O profile/trace

60秒生成一個跟蹤信息文件,保存到我的本地機器上。可以使用以下方法檢查跟蹤信息:

go tool trace profile/trace

在瀏覽器中打開,可以更直觀的查看。(關于 go tool trace 我們這兒也不詳細介紹,有需要后續會寫相應文章來介紹)

如上圖所示,堆好像增長的很快并且伴隨著 GC 頻繁的出現迅速下降。還可以在 GC 通道中看到明顯的藍色條,表示在 GC 中花費的時間。

這就是我們要搜尋問題解決方案的有力證據。

3

修復

為了查找 flate.NewWriter 導致內存分配問題的原因,我需要查一下 Chi 的源碼,首先看下在用版本。

? go list -m all | grep chi

github.com/go-chi/chi v3.3.4+incompatible

在源碼中最終定位到了該方法:

func encoderDeflate(w http.ResponseWriter, level int) io.Writer {
    dw, err := flate.NewWriter(w, level)
    if err != nil {
        return nil
    }
    return dw
}

通過進一步的檢查,可以發現 flate.NewWriter 在每次通過中間件輸出時都會被調用。這與之前以 1000 rps 請求 API 時看到的大量內存分配相對應。

因為不想失去 API 輸出壓縮,所以試著先升級下版本,看能不能解決問題,但是新版本中仍然存在。所以就去扒了下 issues/PR,發現作者有提及到重新中間件壓縮庫。作者提到:對于具有Reset(io.Writer)方法的編碼器,使用 sync.Pool 可減少內存開銷。

發文前已經將 PR 合并到了 master,簡單升級后就解決了這個問題。

4

結果

最后再進行一次運行負載測試和跟蹤分析,可以驗證確實修復了這個問題。

查看新的跟蹤信息,可以看到堆以更穩定的速度增長,GC 的總量和 GC 的花費減少了:

總結

  1. 不要假設(即便是很流行的)開源庫已經優化到極致了。
  2. 一個很小的問題可能會引起巨大的連鎖反應,尤其是在高負載下。
  3. 合適的情況下使用 sync.Pool。
  4. 負載測試和性能分析是很好的工具。

以上就是本次分享的內容~

如果有什么改進建議,也可以在我們評論區留言,供大家參考學習。

  • https://github.com/tsenart/vegeta/
  • https://jvns.ca/blog/2017/09/24/profiling-go-with-pprof/
  • https://github.com/go-chi/chi/
  • https://making.pusher.com/go-tool-trace/
我還沒有學會寫個人說明!

《解密科技》之《萬物互聯 解密傳感新時代》

上一篇

互聯網是如何把“原始人”逼成“機器人”

下一篇

你也可能喜歡

Go內存分配跟蹤調優

長按儲存圖像,分享給朋友

ITPUB 每周精要將以郵件的形式發放至您的郵箱


微信掃一掃

微信掃一掃
双色球常规走势图 体彩20选5 浙江11选5 pc蛋蛋 价值投资股票推荐 配资通 重庆快乐十分 任选9场 好牛168配资 证券公司佣金 股票指数期货期权 青海十一选五 上海时时乐