国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

[譯] 我是如何在大型代碼庫上使用 pprof 調(diào)查 Go 中的內(nèi)存泄漏

frolc / 4088人閱讀

摘要:與任何大型系統(tǒng)一樣,可能會在后期階段出現(xiàn)一些問題,包括性能問題,內(nèi)存泄漏等。在本文中,我將介紹如何調(diào)查中的內(nèi)存泄漏,詳細說明尋找,理解和解決它的步驟。畫像是一組顯示導致特定事件實例的調(diào)用順序堆棧的追蹤,例如內(nèi)存分配。棧主要是短周期的內(nèi)存。


原文地址:How I investigated memory leaks in Go using pprof on a large codebase

譯文地址:github.com/watermelo/d…

譯者:咔嘰咔嘰

譯者水平有限,如有翻譯或理解謬誤,煩請幫忙指出

在今年的大部分時間里,我一直在 Orbs 團隊用 Go 語言做可擴展的區(qū)塊鏈的基礎設施開發(fā),這是令人興奮的一年。在 2018 年的時候,我們研究我們的區(qū)塊鏈該選擇哪種語言實現(xiàn)。因為我們知道 Go 擁有一個良好的社區(qū)和一個非常棒的工具集,所以我們選擇了 Go。

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

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

我們可以把內(nèi)存映射想象成一棵樹,遍歷那棵樹我們會得到不同的對象分配和關系。這意味著無論如何 根會保持內(nèi)存而不被 GCing(垃圾回收)內(nèi)存的原因。因為在 Go 中沒有簡單的方法來分析完整的 core dump,所以很難找到一個對象的根沒有被 GC 過。

在撰寫本文時,我們無法在網(wǎng)上找到任何可以幫助我們的工具。由于存在 core dump 格式以及從調(diào)試包中導出該文件的簡單方法,這可能是 Google 使用過的一種方法。網(wǎng)上搜索它看起來像是在 Golang pipeline 中,創(chuàng)建了這樣的 core dump 查看器,但看起來并不像有人在使用它。話雖如此,即使沒有這樣的解決方案,使用現(xiàn)有工具我們通常也可以找到根本原因。

內(nèi)存泄漏

內(nèi)存泄漏或內(nèi)存壓力可以以多種形式出現(xiàn)在整個系統(tǒng)中。通常我們將它們視為 bug,但有時它們的根本原因可能是因為設計的問題。

當我們在新的設計原則下構建我們的系統(tǒng)時,這些考慮并不重要。更重要的是以避免過早優(yōu)化的方式構建系統(tǒng),并使你能夠在代碼成熟后再優(yōu)化它們,而不是從一開始就過度設計它。然而,一些內(nèi)存壓力常見問題的例子是:

內(nèi)存分配太多,數(shù)據(jù)表示不正確

大量使用反射或字符串

使用全局變量

孤兒,沒有結束的 goroutines

在 Go 中,創(chuàng)建內(nèi)存泄漏的最簡單方法是定義全局變量,數(shù)組,然后將該數(shù)據(jù)添加到數(shù)組。這篇博客文章以一種不錯的方式描述了這個例子。

那我為什么還要寫這篇文章呢?當我研究這個例子時,我發(fā)現(xiàn)了很多關于內(nèi)存泄漏的方法。但是,比起這個例子,真實系統(tǒng)有超過 50 行代碼和單個結構。在這種情況下,找到內(nèi)存問題的來源比該示例描述的要復雜得多。

Golang 為我們提供了一個神奇的工具叫pprof。掌握此工具后,可以幫助調(diào)查并發(fā)現(xiàn)最有可能的內(nèi)存問題。它的另一個用途是查找 CPU 問題,但我不會在這篇文章中介紹任何與 CPU 有關的內(nèi)容。

go tool pprof

把這個工具的方方面面講清楚需要多個博客文章。花一點時間找出怎么使用這個工具去獲取有用的東西。在這篇文章里,我將集中在它的內(nèi)存相關功能上。

pprof包創(chuàng)建一個 heap dump 文件,你可以在隨后進行分析/可視化以下兩種內(nèi)存映射:

當前的內(nèi)存分配

總(累積)內(nèi)存分配

該工具可以比較快照。例如,可以讓你比較現(xiàn)在和 30 秒前的差異顯示。對于壓力場景,這可以幫助你定位到代碼中有問題的區(qū)域。

pprof 畫像

pprof 的工作方式是使用畫像(profiles)。

畫像是一組顯示導致特定事件實例的調(diào)用順序堆棧的追蹤,例如內(nèi)存分配。

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

Go 有幾個內(nèi)置的畫像供我們在常見情況下使用:

goroutine - 所有當前 goroutines 的堆棧跟蹤

heap - 活動對象的內(nèi)存分配的樣本

allocs - 過去所有內(nèi)存分配的樣本

threadcreate - 導致創(chuàng)建新 OS 線程的堆棧跟蹤

block - 導致阻塞同步原語的堆棧跟蹤

mutex - 爭用互斥持有者的堆棧跟蹤

在查看內(nèi)存問題時,我們將專注于堆畫像。 allocs 畫像和它在關于數(shù)據(jù)收集方面是相同的。兩者之間的區(qū)別在于 pprof 工具在啟動時讀取的方式不一樣。 allocs 畫像將以顯示自程序啟動以來分配的總字節(jié)數(shù)(包括垃圾收集的字節(jié))的模式啟動 pprof。在嘗試提高代碼效率時,我們通常會使用該模式。

簡而言之,這是 OS(操作系統(tǒng))存儲我們代碼中對象占用內(nèi)存的地方。這塊內(nèi)存隨后會被“垃圾回收”,或者在非垃圾回收語言中手動釋放。

堆不是唯一發(fā)生內(nèi)存分配的地方,一些內(nèi)存也在棧中分配。棧主要是短周期的內(nèi)存。在 Go 中,棧通常用于在函數(shù)閉包內(nèi)發(fā)生的賦值。 Go 使用棧的另一個地方是編譯器“知道”在運行時需要多少內(nèi)存(例如固定大小的數(shù)組)。有一種方法可以使 Go 編譯器將棧“轉(zhuǎn)義”到堆中輸出分析,但我不會在這篇文章中談到它。

堆數(shù)據(jù)需要“釋放”和垃圾回收,而棧數(shù)據(jù)則不需要。這意味著使用棧效率更高。

這是分配不同位置的內(nèi)存的簡要說明。還有更多內(nèi)容,但這不在本文的討論范圍之內(nèi)。

使用 pprof 獲取堆數(shù)據(jù)

獲取此工具的數(shù)據(jù)主要有兩種方式。第一種通常是把代碼加入到測試或分支中,包括導入runtime/pprof,然后調(diào)用pprof.WriteHeapProfile(some_file)來寫入堆信息。

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

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

根據(jù)文檔,WriteHeapProfile可以向后兼容。其余類型的畫像沒有這樣的便捷方式,必須使用Lookup()函數(shù)來獲取其畫像數(shù)據(jù)。

第二個更有意思,是通過 HTTP(基于 Web 的 endpoints)來啟用。這允許你從正在運行的 e2e/test 環(huán)境中的容器中去提取數(shù)據(jù),甚至從“生產(chǎn)”環(huán)境中提取數(shù)據(jù)。這是 Go 運行和工具集所擅長的。整個包文檔可以在這里找到,太長不看版,只需要你將它添加到代碼中:

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

導入net/http/pprof的“副作用”是在/debug/pprof的 web 服務器根目錄下會注冊 pprof 端點。現(xiàn)在使用 curl 我們可以獲取要查看的堆信息文件:

curl -sK -v http://localhost:8080/debug/pprof/heap > heap.out

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

使用 pprof

所以我們收集了這些數(shù)據(jù),現(xiàn)在該干什么呢?如上所述,pprof 有兩種主要的內(nèi)存分析策略。一個是查看當前的內(nèi)存分配(字節(jié)或?qū)ο笥嫈?shù)),稱為inuse。另一個是查看整個程序運行時的所有分配的字節(jié)或?qū)ο笥嫈?shù),稱為alloc。這意味著無論它是否被垃圾回收,都會是所有樣本的總和。

在這里我們需要重申一下堆畫像文件是內(nèi)存分配的樣例。幕后的pprof使用runtime.MemProfile函數(shù),該函數(shù)默認按分配字節(jié)每 512KB 收集分配信息。可以修改 MemProfile 以收集所有對象的信息。請注意,這很可能會降低應用程序的運行速度。

這意味著默認情況下,對于在 pprof 監(jiān)控下抖動的小對象,可能會出現(xiàn)問題。對于大型代碼庫/長期運行的程序,這不是問題。

一旦收集好畫像文件后,就可以將其加載到 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。這意味著我們正在查看特定時刻的內(nèi)存分配數(shù)據(jù)(當我們捕獲該配置文件時)。type 是sample_index的配置值,可能的值為:

inuse_space - 已分配但尚未釋放的內(nèi)存數(shù)量

inuse_objects - 已分配但尚未釋放的對象數(shù)量

alloc_space - 已分配的內(nèi)存總量(不管是否已釋放)

alloc_objects - 已分配的對象總量(不管是否已釋放)

現(xiàn)在在交互命令行中輸入top,將輸出頂級內(nèi)存的消費者

(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的一系列數(shù)據(jù),這意味著它們被過濾掉了。一個節(jié)點或樹中的一個“節(jié)點”就是一整個對象。丟棄節(jié)點是降噪的好主意,但有時它可能會隱藏內(nèi)存問題產(chǎn)生的根本原因。我們繼續(xù)看一個例子。

如果要該畫像文件的所有數(shù)據(jù),請在運行 pprof 時添加-nodefraction=0選項,或在交互命令行中鍵入nodefraction=0

在輸出列表中,我們可以看到兩個值,flatcum

flat 表示堆棧中當前層函數(shù)的內(nèi)存

cum 表示堆棧中直到當前函數(shù)所累積的內(nèi)存

僅僅這個信息有時可以幫助我們了解是否存在問題。例如,一個函數(shù)負責分配了大量內(nèi)存但沒有保留內(nèi)存的情況。這意味著某些其他對象指向該內(nèi)存并維護其分配,這說明我們可能存在系統(tǒng)設計的問題或 bug。

關于top在交互命令行中的另一個巧妙的技巧是它實際上運行了top10。top 命令支持topN格式,其中N是你想要查看的條目數(shù)。在上面的情況,如果鍵入top70將輸出所有節(jié)點。

可視化

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

在我們的系統(tǒng)上,默認的可視化輸出類似于:

這看起來可能有點嚇人,但它是程序中內(nèi)存分配流(根據(jù)堆棧跟蹤)的可視化。閱讀圖表并不像看起來那么復雜。帶有數(shù)字的白色方塊顯示已分配的空間(在圖形邊緣上是它占用內(nèi)存的數(shù)量),每個更寬的矩形顯示調(diào)用的函數(shù)。

請注意,在上圖中,我從執(zhí)行模式inuse_space中取出了一個 png。很多時候你也應該看看inuse_objects,因為它可以幫助你找到內(nèi)存分配問題。

深入挖掘,尋找根本原因

到目前為止,我們能夠理解應用程序在運行期間內(nèi)存怎么分配的。這有助于我們了解我們程序的行為(或不好的行為)。

在我們的例子中,我們可以看到內(nèi)存由membuffers持有,這是我們的數(shù)據(jù)序列化庫。這并不意味著我們在該代碼段有內(nèi)存泄漏,這意味著該函數(shù)持有了內(nèi)存。了解如何閱讀圖表以及一般的 pprof 輸出非常重要。在這個例子中,當我們序列化數(shù)據(jù)時,意味著我們將內(nèi)存分配給結構和原始對象(int,string),它永遠不會被釋放。

跳到結論部分,我們可以假設序列化路徑上的一個節(jié)點負責持有內(nèi)存,例如:

我們可以看到日志庫中鏈中的某個地方,控制著>50MB 的已分配內(nèi)存。這是由我們的日志記錄器調(diào)用函數(shù)分配的內(nèi)存。經(jīng)過思考,這實際上是預料之中的。日志記錄器會分配內(nèi)存,是因為它需要序列化數(shù)據(jù)以將其輸出到日志,因此它會造成進程中的內(nèi)存分配。

我們還可以看到,在分配路徑下,內(nèi)存僅由序列化持有,而不是任何其他內(nèi)容。此外,日志記錄器保留的內(nèi)存量約為總量的 30%。綜上告訴我們,最有可能的問題不在于日志記錄器。如果它是 100%,或接近它,那么我們應該一直找下去 - 但事實并非如此。這可能意味著它記錄了一些不應該記錄的東西,但不是日志記錄器的內(nèi)存泄漏。

是時候介紹另一個名為listpprof命令。它接受一個正則表達式,該表達式是內(nèi)容的過濾器。 “l(fā)ist”實際上是與分配相關的帶注釋的源代碼。在我們可以看到在日志記錄器的上下文中將執(zhí)行list RequestNew,因為我們希望看到對日志記錄器的調(diào)用。這些調(diào)用來自恰好以相同前綴開頭的兩個函數(shù)。

(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:}

我們可以看到所做的內(nèi)存分配位于cum列中,這意味著分配的內(nèi)存保留在調(diào)用棧中。這與圖表顯示的內(nèi)容相關。此時很容易看出日志記錄器分配內(nèi)存是因為我們發(fā)送了整個“block”對象造成的。這個對象需要序列化它的某些部分(我們的對象是 membuffer 對象,它實現(xiàn)了一些String()函數(shù))。它是一個有用的日志,還是一個好的做法?可能不是,但它不是日志記錄器端或調(diào)用日志記錄器的代碼產(chǎn)生了內(nèi)存泄漏,

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

那為什么內(nèi)存會泄漏

我們之所以調(diào)查是因為懷疑有內(nèi)存泄漏的問題。我們發(fā)現(xiàn)內(nèi)存消耗高于系統(tǒng)預期的需要。最重要的是,我們看到它不斷增加,這是“這里有問題”的另一個強有力的指標。

此時,在 Java 或.Net 的情況下,我們將打開一些"gc roots"分析或分析器,并獲取引用該數(shù)據(jù)并造成泄漏的實際對象。正如所解釋的那樣,對于 Go 來說這是不可能的,因為工具問題也是因為 Go 低等級的內(nèi)存表示。

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

此時,因為我們熟悉我們的系統(tǒng),所以很容易理解這不再是一個 bug。它(幾乎)是設計的。但是讓我們繼續(xù)探索如何從工具(pprof)中獲取信息以找到根本原因。

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

我們有兩個新的子樹。再次提醒,pprof 堆畫像文件是內(nèi)存分配的采樣。對于我們的系統(tǒng)而言 - 我們不會遺漏任何重要信息。這個較長的綠色新子樹的部分是與系統(tǒng)的其余部分完全斷開的測試運行器,在本篇文章中我沒有興趣考慮它。

較短的藍色子樹,有一條邊連接到整個系統(tǒng)是inMemoryBlockPersistance。這個名字也解釋了我們想象的"泄漏"。這是數(shù)據(jù)后端,它將所有數(shù)據(jù)存儲在內(nèi)存中而不是持久化到磁盤。值得注意的是,我們可以看到它持有兩個大的對象。為什么是兩個?因為我們可以看到對象大小為 1.28MB,函數(shù)占用大小為 2.57MB。

這個問題很好理解。我們可以使用 delve(調(diào)試器)(譯者注:deleve)來查看調(diào)試我們代碼中的內(nèi)存情況。

那我們?nèi)绾涡迯湍?/b>

嗯,這很糟糕,這是一個人為錯誤。雖然這個過程是有教育意義的,我們能不能做得更好呢?

我們?nèi)匀荒堋靶崽降健边@個堆信息。反序列化的數(shù)據(jù)占用了太多的內(nèi)存,為什么 142MB 的內(nèi)存需要大幅減少呢?.. pprof 可以回答這個問題 - 實際上,它確實可以回答這些問題。

要查看函數(shù)的帶注釋的源代碼,我們可以運行list lazy。我們使用lazy,因為我們正在尋找的函數(shù)名是lazyCalcOffsets(),而且我們的代碼中也沒有以 lazy 開頭的其他函數(shù)。當然輸入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數(shù)字是相同的。這表明分配的內(nèi)存也在這些分配點保留。

接下來,我們可以看到make()占用了一些內(nèi)存。這是很正常的,它是指向數(shù)據(jù)結構的指針。然而,我們也看到第 43 行的賦值占用了內(nèi)存,這意味著它分配了內(nèi)存。

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

接下來應該保持警惕:如果內(nèi)存消費是一個相關的考慮因素的話,當數(shù)據(jù)不稀疏或者可以轉(zhuǎn)換為順序索引時,使用map[int]T也沒問題,但是通常應該使用切片實現(xiàn)。然而,當擴容一個大的切片時,切片可能會使操作變慢,在 map 中這種變慢可以忽略不計。優(yōu)化沒有萬金油。

在上面的代碼中,在檢查了我們?nèi)绾问褂迷?map 之后,我們意識到雖然我們想象它是一個稀疏數(shù)組,但它并不是那么稀疏。這與上面描述的情況匹配,我們能馬上想到一個將 map 改為切片的小型重構實際上是可行的,并且可能使該代碼內(nèi)存效率更好。所以我們將其改為:

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

就這么簡單,我們現(xiàn)在使用切片替代了 map。由于我們接收數(shù)據(jù)的方式是懶加載進去的,并且我們隨后如何訪問這些數(shù)據(jù),除了這兩行和保存該數(shù)據(jù)的結構之外,不需要修改其他代碼。這些修改對內(nèi)存消耗有什么影響?

讓我們來看看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%

讀取測試的初始化創(chuàng)建分配的數(shù)據(jù)結構。我們可以看到運行時間提高了約 30%,內(nèi)存分配下降了 50%,內(nèi)存消耗提高了> 90%(!)

由于切片(之前是 map)從未添加過很多數(shù)據(jù),因此這些數(shù)字幾乎顯示了我們將在生產(chǎn)中看到的內(nèi)容。它取決于數(shù)據(jù)熵,但可能在內(nèi)存分配和內(nèi)存消耗還有提升的空間。

從同一測試中獲取堆畫像文件來看一下pprof,我們將看到現(xiàn)在內(nèi)存消耗實際上下降了約 90%。

需要注意的是,對于較小的數(shù)據(jù)集,在切片滿足的情況就不要使用 map,因為 map 的開銷很大。

完整的 core dump

如上所述,這就是我們現(xiàn)在看到工具受限制的地方。當我們調(diào)查這個問題時,我們相信自己能夠找到根對象,但沒有取得多大成功。隨著時間的推移,Go 會以很快的速度發(fā)展,但在完全轉(zhuǎn)儲或內(nèi)存表示的情況下,這種演變會帶來代價。完整的堆轉(zhuǎn)儲格式在修改時不向后兼容。這里描述的最新版本和寫入完整堆轉(zhuǎn)儲,可以使用debug.WriteHeapDump()

雖然現(xiàn)在我們沒有“陷入困境”,因為沒有很好的解決方案來探索完全轉(zhuǎn)儲(full down)。 目前為止,pprof回答了我們所有的問題。

請注意,互聯(lián)網(wǎng)會記住許多不再相關的信息。如果你打算嘗試自己打開一個完整的轉(zhuǎn)儲,那么你應該忽略一些事情,從 go1.11 開始:

沒有辦法在 MacOS 上打開和調(diào)試完整的 core dump,只有 Linux 可以。

github.com/randall77/h…上的工具適用于 Go1.3,它存在 1.7+的分支,但它也不能正常工作(不完整)。

在github.com/golang/debu…上查看并不真正編譯。它很容易修復(內(nèi)部的包指向 golang.org 而不是 github.com),但是,在 MacOS 或者 Linux 上可能都不起作用。

此外,github.com/randall77/c…在 MacOS 也會失敗

pprof UI

關于 pprof,要注意的最后一個細節(jié)是它的 UI 功能。在開始調(diào)查與使用 pprof 畫像文件相關的任何問題時可以節(jié)省大量時間。(譯者注:需要安裝 graphviz)

go tool pprof -http=:8080 heap.out

此時它應該打開 Web 瀏覽器。如果沒有,則瀏覽你設置的端口。它使你能夠比命令行更快地更改選項并獲得視覺反饋。消費信息的一種非常有用的方法。

UI 確實讓我熟悉了火焰圖,它可以非常快速地暴露代碼的罪魁禍首。

結論

Go 是一種令人興奮的語言,擁有非常豐富的工具集,你可以用 pprof 做更多的事情。例如,這篇文章沒有涉及到的 CPU 分析。

其他一些好的文章:

rakyll.org/archive/ - 我相信這是圍繞性能監(jiān)控的主要貢獻者之一,她的博客上有很多好帖子

github.com/google/gops - 由JBD(運行 rakyll.org)編寫,此工具保證是自己的博客文章。

medium.com/@cep21/usin… - go tool trace是用來做 CPU 分析的,這是一個關于該分析功能的不錯的帖子。

文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/6922.html

相關文章

  • [] 我是如何大型代碼庫上使用 pprof 調(diào)查 Go 中的內(nèi)存泄漏

    摘要:與任何大型系統(tǒng)一樣,可能會在后期階段出現(xiàn)一些問題,包括性能問題,內(nèi)存泄漏等。在本文中,我將介紹如何調(diào)查中的內(nèi)存泄漏,詳細說明尋找,理解和解決它的步驟。畫像是一組顯示導致特定事件實例的調(diào)用順序堆棧的追蹤,例如內(nèi)存分配。棧主要是短周期的內(nèi)存。 原文地址:How I investigated memory leaks in Go using pprof on a large codebase 譯...

    番茄西紅柿 評論0 收藏0
  • [] Go 語言命令概覽

    摘要:原文地址原文作者譯文出自掘金翻譯計劃本文永久鏈接譯者校對者我偶爾會被人問到你為什么喜歡使用語言我經(jīng)常會提到的就是工具命令,它是與語言一同存在的一部分。 原文地址:An Overview of Gos Tooling 原文作者:Alex Edwards 譯文出自:掘金翻譯計劃 本文永久鏈接:github.com/xitu/gold-m… 譯者:iceytea 校對者:jianboy, cyr...

    antyiwei 評論0 收藏0
  • [] Go 語言命令概覽

    摘要:原文地址原文作者譯文出自掘金翻譯計劃本文永久鏈接譯者校對者我偶爾會被人問到你為什么喜歡使用語言我經(jīng)常會提到的就是工具命令,它是與語言一同存在的一部分。 原文地址:An Overview of Gos Tooling 原文作者:Alex Edwards 譯文出自:掘金翻譯計劃 本文永久鏈接:github.com/xitu/gold-m… 譯者:iceytea 校對者:jianboy, cyr...

    番茄西紅柿 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<