[譯] 我是如何在大型代碼庫上使用 pprof 調查 Go 中的內存泄漏

在今年的大部分時間裏,我一直在 Orbs 團隊用 Go 語言作可擴展的區塊鏈的基礎設施開發,這是使人興奮的一年。在 2018 年的時候,咱們研究咱們的區塊鏈該選擇哪一種語言實現。由於咱們知道 Go 擁有一個良好的社區和一個很是棒的工具集,因此咱們選擇了 Go。node

最近幾周,咱們進入了系統整合的最後階段。與任何大型系統同樣,可能會在後期階段出現一些問題,包括性能問題,內存泄漏等。當整合系統時,咱們找到了一個不錯的方法。在本文中,我將介紹如何調查 Go 中的內存泄漏,詳細說明尋找,理解和解決它的步驟。git

Golang 提供的工具集很是出色但也有其侷限性。首先來看看這個問題,最大的一個問題是查詢完整的 core dumps 能力有限。完整的 core dumps 是程序運行時的進程佔用內存(或用戶內存)的鏡像。github

咱們能夠把內存映射想象成一棵樹,遍歷那棵樹咱們會獲得不一樣的對象分配和關係。這意味着不管如何 根會持有內存而不被 GCing(垃圾回收)內存的緣由。由於在 Go 中沒有簡單的方法來分析完整的 core dump,因此很難找到一個沒有被 GC 過的對象的根。golang

在撰寫本文時,咱們沒法在網上找到任何能夠幫助咱們的工具。因爲存在 core dump 格式以及從 debug 包中導出該文件的簡單方法,這多是 Google 使用過的一種方法。網上搜索它看起來像是在 Golang pipeline 中建立了這樣的 core dump 查看器,但看起來並不像有人在使用它。話雖如此,即便沒有這樣的解決方案,使用現有工具咱們一般也能夠找到根本緣由。web

內存泄漏

內存泄漏或內存壓力能夠以多種形式出如今整個系統中。一般咱們將它們視爲 bug,但有時它們的根本緣由多是由於設計的問題。正則表達式

當咱們在新的設計原則下構建咱們的系統時,這些考慮並不重要。更重要的是以避免過早優化的方式構建系統,並使你可以在代碼成熟後再優化它們,而不是從一開始就過分設計它。然而,一些常見內存壓力的問題是:shell

  • 內存分配太多,數據表示不正確
  • 大量使用反射或字符串
  • 使用全局變量
  • 孤兒,沒有結束的 goroutines

在 Go 中,建立內存泄漏的最簡單方法是定義全局變量,數組,而後將該數據添加到數組。這篇博客文章以一種不錯的方式描述了這個例子。後端

我爲何要寫這篇文章呢?當我研究這個例子時,我發現了不少關於內存泄漏的方法。可是,相比較這個例子,咱們的真實系統有超過 50 行代碼和單個結構。在這種狀況下,找到內存問題的來源比該示例描述的要複雜得多。數組

Golang 爲咱們提供了一個神奇的工具叫 pprof。掌握此工具後,能夠幫助調查並發現最有可能的內存問題。它的另外一個用途是查找 CPU 問題,但我不會在這篇文章中介紹任何與 CPU 有關的內容。瀏覽器

go tool pprof

把這個工具的方方面面講清楚須要不止一篇博客文章。我將花一點時間找出怎麼使用這個工具去獲取有用的東西。在這篇文章裏,將集中在它的內存相關功能上。

pprof 包建立一個 heap dump 文件,你能夠在隨後進行分析/可視化如下兩種內存映射:

  • 當前的內存分配
  • 總(累積)內存分配

該工具能夠比較快照。例如,可讓你比較顯示如今和 30 秒前的差別。對於壓力場景,這能夠幫助你定位到代碼中有問題的區域。

pprof 畫像

pprof 的工做方式是使用畫像。

畫像是一組顯示致使特定事件實例的調用順序堆棧的追蹤,例如內存分配。

文件runtime/pprof/pprof.go包含畫像的詳細信息和實現。

Go 有幾個內置的畫像供咱們在常見狀況下使用:

  • goroutine - 全部當前 goroutines 的堆棧跟蹤
  • heap - 活動對象的內存分配的樣本
  • allocs - 過去全部內存分配的樣本
  • threadcreate - 致使建立新 OS 線程的堆棧跟蹤
  • block - 致使阻塞同步原語的堆棧跟蹤
  • mutex - 爭用互斥鎖持有者的堆棧跟蹤

在查看內存問題時,咱們將專一於堆畫像。 allocs 畫像和它在關於數據收集方面是相同的。二者之間的區別在於 pprof 工具在啓動時讀取的方式不同。 allocs 畫像將以顯示自程序啓動以來分配的總字節數(包括垃圾收集的字節)的模式啓動 pprof。在嘗試提升代碼效率時,咱們一般會使用該模式。

簡而言之,這是 OS(操做系統)存儲咱們代碼中對象佔用內存的地方。這塊內存隨後會被「垃圾回收」,或者在非垃圾回收語言中手動釋放。

堆不是惟一發生內存分配的地方,一些內存也在棧中分配。棧主要是短週期的內存。在 Go 中,棧一般用於在函數閉包內發生的賦值。 Go 使用棧的另外一個地方是編譯器「知道」在運行時須要多少內存(例如固定大小的數組)。有一種方法可使 Go 編譯器將棧「轉義」到堆中輸出分析,但我不會在這篇文章中談到它。

堆數據須要「釋放」和垃圾回收,而棧數據則不須要。這意味着使用棧效率更高。

這是分配不一樣位置的內存的簡要說明。還有更多內容,但這不在本文的討論範圍以內。

使用 pprof 獲取堆數據

獲取數據主要有兩種方式。第一種一般是把代碼加入到測試或分支中,包括導入runtime/pprof,而後調用pprof.WriteHeapProfile(some_file)來寫入堆信息。

請注意,WriteHeapProfile是用於運行的語法糖:

// lookup takes a profile name
pprof.Lookup("heap").WriteTo(some_file, 0)
複製代碼

根據文檔,WriteHeapProfile能夠向後兼容。其他類型的畫像沒有這樣的便捷方式,必須使用Lookup()函數來獲取其畫像數據。

第二個更有意思,是經過 HTTP(基於 Web 的 endpoints)來啓用。這容許你從正在運行的 e2e/test 環境中的容器中去提取數據,甚至從「生產」環境中提取數據。這是 Go 運行時和工具集所擅長的部分。整個包文檔能夠在這裏找到,太長不看版,只須要你將它添加到代碼中:

import (
  "net/http"
  _ "net/http/pprof"
)
...
func main() {
  ...
  http.ListenAndServe("localhost:8080", nil)
}
複製代碼

導入net/http/pprof 的「反作用」是在/debug/pprof的 web 服務器根目錄下會註冊 pprof endpoint。如今使用 curl 咱們能夠獲取要查看的堆信息文件:

curl -sK -v http://localhost:8080/debug/pprof/heap > heap.out
複製代碼

只有在你的程序以前沒有 http listener 時才須要添加上面的http.ListenAndServe()。若是有的話就沒有必要再監聽了,它會自動處理。還可使用ServeMux.HandleFunc()來設置它,這對於更復雜的 http 程序有意義。

使用 pprof

因此咱們收集了這些數據,如今該幹什麼呢?如上所述,pprof 有兩種主要的內存分析策略。一個是查看當前的內存分配(字節或對象計數),稱爲inuse。另外一個是查看整個程序運行時的全部分配的字節或對象計數,稱爲 alloc。這意味着不管它是否被垃圾回收,都會是全部樣本的總和。

在這裏咱們須要重申一下堆畫像文件是內存分配的樣例。幕後的pprof使用runtime.MemProfile函數,該函數默認按分配字節每 512KB 收集分配信息。能夠修改 MemProfile 以收集全部對象的信息。須要注意的是,這極可能會下降應用程序的運行速度。

這意味着默認狀況下,對於在 pprof 監控下抖動的小對象,可能會出現問題。對於大型代碼庫/長期運行的程序,這不是問題。

一旦收集好畫像文件後,就能夠將其加載到 pprof 的交互式命令行中了,經過運行:

> go tool pprof heap.out
複製代碼

咱們能夠觀察到顯示的信息

Type: inuse_space
Time: Jan 22, 2019 at 1:08pm (IST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
複製代碼

這裏要注意的事項是Type:inuse_space。這意味着咱們正在查看特定時刻的內存分配數據(當咱們捕獲該配置文件時)。type 是sample_index的配置值,可能的值爲:

  • inuse_space - 已分配但還沒有釋放的內存數量
  • inuse_objects - 已分配但還沒有釋放的對象數量
  • alloc_space - 已分配的內存總量(無論是否已釋放)
  • alloc_objects - 已分配的對象總量(無論是否已釋放)

如今在交互命令行中輸入top,將輸出頂級內存消費者

(pprof) top
Showing nodes accounting for 330.04MB, 93.73% of 352.11MB total
Dropped 19 nodes (cum <= 1.76MB)
Showing top 10 nodes out of 56
      flat  flat%   sum%        cum   cum%
  142.02MB 40.33% 40.33%   142.02MB 40.33%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/membuffers/go.(*InternalMessage).lazyCalcOffsets
      28MB  7.95% 48.29%       28MB  7.95%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.TransactionsBlockProofReader (inline)
   26.51MB  7.53% 55.81%    39.01MB 11.08%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.(*ResultsBlockHeaderBuilder).Build
   25.51MB  7.24% 63.06%    32.51MB  9.23%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.(*ResultsBlockProofBuilder).Build
      23MB  6.53% 69.59%       23MB  6.53%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.ResultsBlockHeaderReader (inline)
   20.50MB  5.82% 75.41%    20.50MB  5.82%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.TransactionsBlockMetadataReader (inline)
      20MB  5.68% 81.09%       20MB  5.68%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.TransactionsBlockHeaderReader (inline)
      16MB  4.54% 85.64%       24MB  6.82%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.(*TransactionsBlockHeaderBuilder).Build
   14.50MB  4.12% 89.76%   122.51MB 34.79%  github.com/orbs-network/orbs-network-go/services/gossip/codec.DecodeBlockPairs
      14MB  3.98% 93.73%       14MB  3.98%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.ResultsBlockProofReader (inline)
複製代碼

咱們能夠看到關於Dropped Nodes的一系列數據,這意味着它們被過濾掉了。一個節點或樹中的一個「節點」就是一整個對象。丟棄節點有利於咱們更快的找到問題,但有時它可能會隱藏內存問題產生的根本緣由。咱們繼續看一個例子。

若是要該畫像文件的全部數據,請在運行 pprof 時添加-nodefraction=0選項,或在交互命令行中鍵入nodefraction=0

在輸出列表中,咱們能夠看到兩個值,flatcum

  • flat 表示堆棧中當前層函數的內存
  • cum 表示堆棧中直到當前層函數所累積的內存

僅僅這個信息有時能夠幫助咱們瞭解是否存在問題。例如,一個函數負責分配了大量內存但沒有保留內存的狀況。這意味着某些其餘對象指向該內存並維護其分配,這說明咱們可能存在系統設計的問題或 bug。

top實際上運行了top10。top 命令支持topN格式,其中N是你想要查看的條目數。在上面的狀況,若是鍵入top70將輸出全部節點。

可視化

雖然topN提供了一個文本列表,但 pprof 附帶了幾個很是有用的可視化選項。能夠輸入pnggif等等(請參閱go tool pprof -help獲取完整信息)。

在咱們的系統上,默認的可視化輸出相似於:

這看起來可能有點嚇人,但它是程序中內存分配流程(根據堆棧跟蹤)的可視化。閱讀圖表並不像看起來那麼複雜。帶有數字的白色方塊顯示已分配的空間(在圖形邊緣上是它佔用內存的數量),每一個更寬的矩形顯示調用的函數。

須要注意的是,在上圖中,我從執行模式inuse_space中取出了一個 png。不少時候你也應該看看inuse_objects,由於它能夠幫助你找到內存分配問題。

深刻挖掘,尋找根本緣由

到目前爲止,咱們可以理解應用程序在運行期間內存怎麼分配的。這有助於咱們瞭解咱們程序的行爲(或很差的行爲)。

在咱們的例子中,咱們能夠看到內存由membuffers持有,這是咱們的數據序列化庫。這並不意味着咱們在該代碼段有內存泄漏,這意味着該函數持有了內存。瞭解如何閱讀圖表以及 pprof 輸出很是重要。在這個例子中,當咱們序列化數據時,意味着咱們將內存分配給結構和原始對象(int,string),它不會被釋放。

跳到結論部分,咱們能夠假設序列化路徑上的一個節點負責持有內存,例如:

subset of the graph

咱們能夠看到日誌庫中鏈中的某個地方,控制着>50MB 的已分配內存。這是由咱們的日誌記錄器調用函數分配的內存。通過思考,這其實是預料之中的。日誌記錄器會分配內存,是由於它須要序列化數據以將其輸出到日誌,所以它會形成進程中的內存分配。

咱們還能夠看到,在分配路徑下,內存僅由序列化持有,而不是任何其餘內容。此外,日誌記錄器保留的內存量約爲總量的 30%。綜上告訴咱們,最有可能的問題不在於日誌記錄器。若是它是 100%,或接近它,那麼咱們應該一直找下去 - 但事實並不是如此。這可能意味着它記錄了一些不該該記錄的東西,但不是日誌記錄器的內存泄漏。

是時候介紹另外一個名爲listpprof命令。它接受一個正則表達式,該表達式是內容的過濾器。 「list」其實是與分配相關的帶註釋的源代碼。在咱們能夠看到在日誌記錄器的上下文中將執行list RequestNew,由於咱們但願看到對日誌記錄器的調用。這些調用來自剛好以相同前綴開頭的兩個函數。

(pprof) list RequestNew
Total: 352.11MB
ROUTINE ======================== github.com/orbs-network/orbs-network-go/services/consensuscontext.(*service).RequestNewResultsBlock in /Users/levison/work/go/src/github.com/orbs-network/orbs-network-go/services/consensuscontext/service.go
         0    77.51MB (flat, cum) 22.01% of Total
         .          .     82:}
         .          .     83:
         .          .     84:func (s *service) RequestNewResultsBlock(ctx context.Context, input *services.RequestNewResultsBlockInput) (*services.RequestNewResultsBlockOutput, error) {
         .          .     85:	logger := s.logger.WithTags(trace.LogFieldFrom(ctx))
         .          .     86:
         .    47.01MB     87:	rxBlock, err := s.createResultsBlock(ctx, input)
         .          .     88:	if err != nil {
         .          .     89:		return nil, err
         .          .     90:	}
         .          .     91:
         .    30.51MB     92:	logger.Info("created Results block", log.Stringable("results-block", rxBlock))
         .          .     93:
         .          .     94:	return &services.RequestNewResultsBlockOutput{
         .          .     95:		ResultsBlock: rxBlock,
         .          .     96:	}, nil
         .          .     97:}
ROUTINE ======================== github.com/orbs-network/orbs-network-go/services/consensuscontext.(*service).RequestNewTransactionsBlock in /Users/levison/work/go/src/github.com/orbs-network/orbs-network-go/services/consensuscontext/service.go
         0    64.01MB (flat, cum) 18.18% of Total
         .          .     58:}
         .          .     59:
         .          .     60:func (s *service) RequestNewTransactionsBlock(ctx context.Context, input *services.RequestNewTransactionsBlockInput) (*services.RequestNewTransactionsBlockOutput, error) {
         .          .     61:	logger := s.logger.WithTags(trace.LogFieldFrom(ctx))
         .          .     62:	logger.Info("starting to create transactions block", log.BlockHeight(input.CurrentBlockHeight))
         .    42.50MB     63:	txBlock, err := s.createTransactionsBlock(ctx, input)
         .          .     64:	if err != nil {
         .          .     65:		logger.Info("failed to create transactions block", log.Error(err))
         .          .     66:		return nil, err
         .          .     67:	}
         .          .     68:
         .          .     69:	s.metrics.transactionsRate.Measure(int64(len(txBlock.SignedTransactions)))
         .    21.50MB     70:	logger.Info("created transactions block", log.Int("num-transactions", len(txBlock.SignedTransactions)), log.Stringable("transactions-block", txBlock))
         .          .     71:	s.printTxHash(logger, txBlock)
         .          .     72:	return &services.RequestNewTransactionsBlockOutput{
         .          .     73:		TransactionsBlock: txBlock,
         .          .     74:	}, nil
         .          .     75:}
複製代碼

咱們能夠看到所作的內存分配位於cum列中,這意味着分配的內存保留在調用棧中。這與圖表顯示的內容相關。此時很容易看出日誌記錄器分配內存是由於咱們發送了整個「block」對象形成的。這個對象須要序列化它的某些部分(咱們的對象是 membuffer 對象,它實現了一些String()函數)。它是一個有用的日誌,仍是一個好的作法?可能不是,但它不是日誌記錄器端或調用日誌記錄器的代碼產生了內存泄漏,

listGOPATH路徑下搜索能夠找到源代碼。若是它搜索的根不匹配(取決於你電腦的項目構建),則可使用-trim_path選項。這將有助於修復它並讓你看到帶註釋的源代碼。當正在捕獲堆配置文件時要將 git 設置爲能夠正確提交。

內存泄漏緣由

之因此調查是由於懷疑有內存泄漏的問題。咱們發現內存消耗高於系統預期的須要。最重要的是,咱們看到它不斷增長,這是「這裏有問題」的另外一個強有力的指標。

此時,在 Java 或.Net 的狀況下,咱們將打開一些'gc roots'分析或分析器,並獲取引用該數據並形成泄漏的實際對象。正如所解釋的那樣,對於 Go 來講這是不可能的,由於工具問題也是因爲 Go 低等級的內存表示。

沒有詳細說明,咱們不知道 Go 把哪一個對象存儲在哪一個地址(指針除外)。這意味着實際上,瞭解哪一個內存地址表示對象(結構)的哪一個成員將須要把某種映射輸出到堆畫像文件。這可能意味着在進行完整的 core dump 以前,還應該採用堆畫像文件,以便將地址映射到分配的行和文件,從而映射到內存中表示的對象。

此時,由於咱們熟悉咱們的系統,因此很容易理解這再也不是一個 bug。它(幾乎)是設計的。可是讓咱們繼續探索如何從工具(pprof)中獲取信息以找到根本緣由。

設置nodefraction=0時,咱們將看到已分配對象的整個圖,包括較小的對象。咱們來看看輸出:

memory visualization with nodefraction=0

咱們有兩個新的子樹。再次提醒,pprof 堆畫像文件是內存分配的採樣。對於咱們的系統而言 - 咱們不會遺漏任何重要信息。這個較長的綠色新子樹的部分是與系統的其他部分徹底斷開的測試運行器,在本篇文章中我沒有興趣考慮它。

system was configured to 「leak」 😞

較短的藍色子樹,有一條邊鏈接到整個系統是inMemoryBlockPersistance。這個名字也解釋了咱們想象的'泄漏'。這是數據後端,它將全部數據存儲在內存中而不是持久化到磁盤。值得注意的是,咱們能夠看到它持有兩個大的對象。爲何是兩個?由於咱們能夠看到對象大小爲 1.28MB,函數佔用大小爲 2.57MB。

這個問題很好理解。咱們可使用 delve(調試器)(譯者注:deleve)來查看調試咱們代碼中的內存狀況。

如何修復

這是一個糟糕的人爲錯誤。雖然這個過程是有教育意義的,咱們能不能作得更好呢?

咱們仍然能「嗅探到」這個堆信息。反序列化的數據佔用了太多的內存,爲何 142MB 的內存須要大幅減小呢?.. pprof 能夠回答這個問題 - 實際上,它確實能夠回答這些問題。

要查看函數的帶註釋的源代碼,咱們能夠運行list lazy。咱們使用lazy,由於咱們正在尋找的函數名是lazyCalcOffsets(),並且咱們的代碼中也沒有以 lazy 開頭的其餘函數。固然輸入list lazyCalcOffsets也能夠。

(pprof) list lazy
Total: 352.11MB
ROUTINE ======================== github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/membuffers/go.(*InternalMessage).lazyCalcOffsets in /Users/levison/work/go/src/github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/membuffers/go/message.go
  142.02MB   142.02MB (flat, cum) 40.33% of Total
         .          .     29:
         .          .     30:func (m *InternalMessage) lazyCalcOffsets() bool {
         .          .     31:	if m.offsets != nil {
         .          .     32:		return true
         .          .     33:	}
      36MB       36MB     34:	res := make(map[int]Offset)
         .          .     35:	var off Offset = 0
         .          .     36:	var unionNum = 0
         .          .     37:	for fieldNum, fieldType := range m.scheme {
         .          .     38:		// write the current offset
         .          .     39:		off = alignOffsetToType(off, fieldType)
         .          .     40:		if off >= m.size {
         .          .     41:			return false
         .          .     42:		}
  106.02MB   106.02MB     43:		res[fieldNum] = off
         .          .     44:
         .          .     45:		// skip over the content to the next field
         .          .     46:		if fieldType == TypeUnion {
         .          .     47:			if off + FieldSizes[TypeUnion] > m.size {
         .          .     48:				return false
複製代碼

咱們能夠看到兩個有趣的信息。一樣,請記住 pprof 堆畫像文件會對有關分配的信息進行採樣。咱們能夠看到flatcum數字是相同的。這代表分配的內存也在這些分配點被保留。

接下來,咱們能夠看到make()佔用了一些內存。這是很正常的,它是指向數據結構的指針。然而,咱們也看到第 43 行的賦值佔用了內存,這意味着它分配了內存。

這讓咱們學習了映射 map,其中 map 的賦值不是簡單的變量賦值。本文詳細介紹了 map 的工做原理。簡而言之,map 與切片相比,map 開銷更大,「成本」更大,元素更多。

接下來應該保持警戒:若是內存消費是一個相關的考慮因素的話,當數據不稀疏或者能夠轉換爲順序索引時,使用map[int]T也沒問題,可是一般應該使用切片實現。然而,當擴容一個大的切片時,切片可能會使操做變慢,在 map 中這種變慢能夠忽略不計。優化沒有萬能的方法。

在上面的代碼中,在檢查了咱們如何使用該 map 以後,咱們意識到雖然咱們想象它是一個稀疏數組,但它並非那麼稀疏。這與上面描述的狀況匹配,咱們能立刻想到一個將 map 改成切片的小型重構其實是可行的,而且可能使該代碼內存效率更好。因此咱們將其改成:

func (m *InternalMessage) lazyCalcOffsets() bool {
	if m.offsets != nil {
		return true
	}
	res := make([]Offset, len(m.scheme))
	var off Offset = 0
	var unionNum = 0
	for fieldNum, fieldType := range m.scheme {
		// write the current offset
		off = alignOffsetToType(off, fieldType)
		if off >= m.size {
			return false
		}
		res[fieldNum] = off
複製代碼

就這麼簡單,咱們如今使用切片替代了 map。因爲咱們接收數據的方式是懶加載進去的,而且咱們隨後如何訪問這些數據,除了這兩行和保存該數據的結構以外,不須要修改其餘代碼。這些修改對內存消耗有什麼影響?

讓咱們來看看benchcmp的幾回測試

benchmark                       old ns/op     new ns/op     delta
BenchmarkUint32Read-4           2047          1381          -32.54%
BenchmarkUint64Read-4           507           321           -36.69%
BenchmarkSingleUint64Read-4     251           164           -34.66%
BenchmarkStringRead-4           1572          1126          -28.37%

benchmark                       old allocs     new allocs     delta
BenchmarkUint32Read-4           14             7              -50.00%
BenchmarkUint64Read-4           4              2              -50.00%
BenchmarkSingleUint64Read-4     2              1              -50.00%
BenchmarkStringRead-4           12             6              -50.00%

benchmark                       old bytes     new bytes     delta
BenchmarkUint32Read-4           1120          80            -92.86%
BenchmarkUint64Read-4           320           16            -95.00%
BenchmarkSingleUint64Read-4     160           8             -95.00%
BenchmarkStringRead-4           960           32            -96.67%
複製代碼

讀取測試的初始化建立分配的數據結構。咱們能夠看到運行時間提升了約 30%,內存分配降低了 50%,內存消耗提升了> 90%(!)

因爲切片(以前是 map)從未添加過不少數據,所以這些數字幾乎顯示了咱們將在生產中看到的內容。它取決於數據熵,但可能在內存分配和內存消耗還有提高的空間。

從同一測試中獲取堆畫像文件來看一下pprof,咱們將看到如今內存消耗實際上降低了約 90%。

須要注意的是,對於較小的數據集,在切片知足的狀況就不要使用 map,由於 map 的開銷很大。

完整的 core dump

如上所述,這就是咱們如今看到工具受限制的地方。當咱們調查這個問題時,咱們相信本身可以找到根對象,但沒有取得多大成功。隨着時間的推移,Go 會以很快的速度發展,但在徹底轉儲或內存表示的狀況下,這種演變會帶來代價。完整的堆轉儲格式在修改時不向後兼容。這裏描述的最新版本和寫入完整堆轉儲,可使用debug.WriteHeapDump()

雖然如今咱們沒有「陷入困境」,由於沒有很好的解決方案來探索徹底轉儲(full down)。 目前爲止,pprof回答了咱們全部的問題。

請注意,互聯網會記住許多再也不相關的信息。若是你打算嘗試本身打開一個完整的轉儲,那麼你應該忽略一些事情,從 go1.11 開始:

  • 沒有辦法在 MacOS 上打開和調試完整的 core dump,只有 Linux 能夠。
  • github.com/randall77/h…上的工具適用於 Go1.3,它存在 1.7+的分支,但它也不能正常工做(不完整)。
  • github.com/golang/debu…上查看並不真正編譯。它很容易修復(內部的包指向 golang.org 而不是 github.com),可是,在 MacOS 或者 Linux 上可能都不起做用。
  • 此外,github.com/randall77/c…在 MacOS 也會失敗

pprof UI

關於 pprof,要注意的一個細節是它的 UI 功能。在開始調查與使用 pprof 畫像文件相關的問題時能夠節省大量時間。(譯者注:須要安裝 graphviz)

go tool pprof -http=:8080 heap.out
複製代碼

此時它應該打開 Web 瀏覽器。若是沒有,則瀏覽你設置的端口。它使你可以比命令行更快地更改選項並得到視覺反饋。消費信息的一種很是有用的方法。

UI 確實讓我熟悉了火焰圖,它能夠很是快速地暴露代碼的罪魁禍首。

結論

Go 是一種使人興奮的語言,擁有很是豐富的工具集,你能夠用 pprof 作更多的事情。例如,這篇文章沒有涉及到的 CPU 分析。

其餘一些好的文章:

  • rakyll.org/archive/ - 我相信這是圍繞性能監控的主要貢獻者之一,她的博客上有不少好帖子
  • github.com/google/gops - 由JBD(運行 rakyll.org)編寫,此工具保證是本身的博客文章。
  • medium.com/@cep21/usin… - go tool trace是用來作 CPU 分析的,這是一個關於該分析功能的不錯的帖子。
相關文章
相關標籤/搜索