- 原文地址:How I investigated memory leaks in Go using pprof on a large codebase
- 譯文地址:github.com/watermelo/d…
- 譯者:咔嘰咔嘰
- 譯者水平有限,若有翻譯或理解謬誤,煩請幫忙指出
在今年的大部分時間裏,我一直在 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
在 Go 中,建立內存泄漏的最簡單方法是定義全局變量,數組,而後將該數據添加到數組。這篇博客文章以一種不錯的方式描述了這個例子。後端
我爲何要寫這篇文章呢?當我研究這個例子時,我發現了不少關於內存泄漏的方法。可是,相比較這個例子,咱們的真實系統有超過 50 行代碼和單個結構。在這種狀況下,找到內存問題的來源比該示例描述的要複雜得多。數組
Golang 爲咱們提供了一個神奇的工具叫 pprof
。掌握此工具後,能夠幫助調查並發現最有可能的內存問題。它的另外一個用途是查找 CPU 問題,但我不會在這篇文章中介紹任何與 CPU 有關的內容。瀏覽器
把這個工具的方方面面講清楚須要不止一篇博客文章。我將花一點時間找出怎麼使用這個工具去獲取有用的東西。在這篇文章裏,將集中在它的內存相關功能上。
pprof
包建立一個 heap dump 文件,你能夠在隨後進行分析/可視化如下兩種內存映射:
該工具能夠比較快照。例如,可讓你比較顯示如今和 30 秒前的差別。對於壓力場景,這能夠幫助你定位到代碼中有問題的區域。
pprof 的工做方式是使用畫像。
畫像是一組顯示致使特定事件實例的調用順序堆棧的追蹤,例如內存分配。
文件runtime/pprof/pprof.go包含畫像的詳細信息和實現。
Go 有幾個內置的畫像供咱們在常見狀況下使用:
在查看內存問題時,咱們將專一於堆畫像。 allocs 畫像和它在關於數據收集方面是相同的。二者之間的區別在於 pprof 工具在啓動時讀取的方式不同。 allocs 畫像將以顯示自程序啓動以來分配的總字節數(包括垃圾收集的字節)的模式啓動 pprof。在嘗試提升代碼效率時,咱們一般會使用該模式。
簡而言之,這是 OS(操做系統)存儲咱們代碼中對象佔用內存的地方。這塊內存隨後會被「垃圾回收」,或者在非垃圾回收語言中手動釋放。
堆不是惟一發生內存分配的地方,一些內存也在棧中分配。棧主要是短週期的內存。在 Go 中,棧一般用於在函數閉包內發生的賦值。 Go 使用棧的另外一個地方是編譯器「知道」在運行時須要多少內存(例如固定大小的數組)。有一種方法可使 Go 編譯器將棧「轉義」到堆中輸出分析,但我不會在這篇文章中談到它。
堆數據須要「釋放」和垃圾回收,而棧數據則不須要。這意味着使用棧效率更高。
這是分配不一樣位置的內存的簡要說明。還有更多內容,但這不在本文的討論範圍以內。
獲取數據主要有兩種方式。第一種一般是把代碼加入到測試或分支中,包括導入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 有兩種主要的內存分析策略。一個是查看當前的內存分配(字節或對象計數),稱爲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
的配置值,可能的值爲:
如今在交互命令行中輸入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
。
在輸出列表中,咱們能夠看到兩個值,flat
和cum
。
flat
表示堆棧中當前層函數的內存cum
表示堆棧中直到當前層函數所累積的內存僅僅這個信息有時能夠幫助咱們瞭解是否存在問題。例如,一個函數負責分配了大量內存但沒有保留內存的狀況。這意味着某些其餘對象指向該內存並維護其分配,這說明咱們可能存在系統設計的問題或 bug。
top
實際上運行了top10
。top 命令支持topN
格式,其中N
是你想要查看的條目數。在上面的狀況,若是鍵入top70
將輸出全部節點。
雖然topN
提供了一個文本列表,但 pprof 附帶了幾個很是有用的可視化選項。能夠輸入png
或gif
等等(請參閱go tool pprof -help
獲取完整信息)。
在咱們的系統上,默認的可視化輸出相似於:
這看起來可能有點嚇人,但它是程序中內存分配流程(根據堆棧跟蹤)的可視化。閱讀圖表並不像看起來那麼複雜。帶有數字的白色方塊顯示已分配的空間(在圖形邊緣上是它佔用內存的數量),每一個更寬的矩形顯示調用的函數。
須要注意的是,在上圖中,我從執行模式inuse_space
中取出了一個 png。不少時候你也應該看看inuse_objects
,由於它能夠幫助你找到內存分配問題。
到目前爲止,咱們可以理解應用程序在運行期間內存怎麼分配的。這有助於咱們瞭解咱們程序的行爲(或很差的行爲)。
在咱們的例子中,咱們能夠看到內存由membuffers
持有,這是咱們的數據序列化庫。這並不意味着咱們在該代碼段有內存泄漏,這意味着該函數持有了內存。瞭解如何閱讀圖表以及 pprof 輸出很是重要。在這個例子中,當咱們序列化數據時,意味着咱們將內存分配給結構和原始對象(int,string),它不會被釋放。
跳到結論部分,咱們能夠假設序列化路徑上的一個節點負責持有內存,例如:
咱們能夠看到日誌庫中鏈中的某個地方,控制着>50MB 的已分配內存。這是由咱們的日誌記錄器調用函數分配的內存。通過思考,這其實是預料之中的。日誌記錄器會分配內存,是由於它須要序列化數據以將其輸出到日誌,所以它會形成進程中的內存分配。
咱們還能夠看到,在分配路徑下,內存僅由序列化持有,而不是任何其餘內容。此外,日誌記錄器保留的內存量約爲總量的 30%。綜上告訴咱們,最有可能的問題不在於日誌記錄器。若是它是 100%,或接近它,那麼咱們應該一直找下去 - 但事實並不是如此。這可能意味着它記錄了一些不該該記錄的東西,但不是日誌記錄器的內存泄漏。
是時候介紹另外一個名爲list
的pprof
命令。它接受一個正則表達式,該表達式是內容的過濾器。 「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()
函數)。它是一個有用的日誌,仍是一個好的作法?可能不是,但它不是日誌記錄器端或調用日誌記錄器的代碼產生了內存泄漏,
list
在GOPATH
路徑下搜索能夠找到源代碼。若是它搜索的根不匹配(取決於你電腦的項目構建),則可使用-trim_path
選項。這將有助於修復它並讓你看到帶註釋的源代碼。當正在捕獲堆配置文件時要將 git 設置爲能夠正確提交。
之因此調查是由於懷疑有內存泄漏的問題。咱們發現內存消耗高於系統預期的須要。最重要的是,咱們看到它不斷增長,這是「這裏有問題」的另外一個強有力的指標。
此時,在 Java 或.Net 的狀況下,咱們將打開一些'gc roots'分析或分析器,並獲取引用該數據並形成泄漏的實際對象。正如所解釋的那樣,對於 Go 來講這是不可能的,由於工具問題也是因爲 Go 低等級的內存表示。
沒有詳細說明,咱們不知道 Go 把哪一個對象存儲在哪一個地址(指針除外)。這意味着實際上,瞭解哪一個內存地址表示對象(結構)的哪一個成員將須要把某種映射輸出到堆畫像文件。這可能意味着在進行完整的 core dump 以前,還應該採用堆畫像文件,以便將地址映射到分配的行和文件,從而映射到內存中表示的對象。
此時,由於咱們熟悉咱們的系統,因此很容易理解這再也不是一個 bug。它(幾乎)是設計的。可是讓咱們繼續探索如何從工具(pprof)中獲取信息以找到根本緣由。
設置nodefraction=0
時,咱們將看到已分配對象的整個圖,包括較小的對象。咱們來看看輸出:
咱們有兩個新的子樹。再次提醒,pprof 堆畫像文件是內存分配的採樣。對於咱們的系統而言 - 咱們不會遺漏任何重要信息。這個較長的綠色新子樹的部分是與系統的其他部分徹底斷開的測試運行器,在本篇文章中我沒有興趣考慮它。
較短的藍色子樹,有一條邊鏈接到整個系統是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 堆畫像文件會對有關分配的信息進行採樣。咱們能夠看到flat
和cum
數字是相同的。這代表分配的內存也在這些分配點被保留。
接下來,咱們能夠看到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 的開銷很大。
如上所述,這就是咱們如今看到工具受限制的地方。當咱們調查這個問題時,咱們相信本身可以找到根對象,但沒有取得多大成功。隨着時間的推移,Go 會以很快的速度發展,但在徹底轉儲或內存表示的狀況下,這種演變會帶來代價。完整的堆轉儲格式在修改時不向後兼容。這裏描述的最新版本和寫入完整堆轉儲,可使用debug.WriteHeapDump()
。
雖然如今咱們沒有「陷入困境」,由於沒有很好的解決方案來探索徹底轉儲(full down)。 目前爲止,pprof
回答了咱們全部的問題。
請注意,互聯網會記住許多再也不相關的信息。若是你打算嘗試本身打開一個完整的轉儲,那麼你應該忽略一些事情,從 go1.11 開始:
關於 pprof,要注意的一個細節是它的 UI 功能。在開始調查與使用 pprof 畫像文件相關的問題時能夠節省大量時間。(譯者注:須要安裝 graphviz)
go tool pprof -http=:8080 heap.out
複製代碼
此時它應該打開 Web 瀏覽器。若是沒有,則瀏覽你設置的端口。它使你可以比命令行更快地更改選項並得到視覺反饋。消費信息的一種很是有用的方法。
UI 確實讓我熟悉了火焰圖,它能夠很是快速地暴露代碼的罪魁禍首。
Go 是一種使人興奮的語言,擁有很是豐富的工具集,你能夠用 pprof 作更多的事情。例如,這篇文章沒有涉及到的 CPU 分析。
其餘一些好的文章:
go tool trace
是用來作 CPU 分析的,這是一個關於該分析功能的不錯的帖子。