成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專(zhuān)欄INFORMATION COLUMN

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

frolc / 4261人閱讀

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


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

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

譯者:咔嘰咔嘰

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

在今年的大部分時(shí)間里,我一直在 Orbs 團(tuán)隊(duì)用 Go 語(yǔ)言做可擴(kuò)展的區(qū)塊鏈的基礎(chǔ)設(shè)施開(kāi)發(fā),這是令人興奮的一年。在 2018 年的時(shí)候,我們研究我們的區(qū)塊鏈該選擇哪種語(yǔ)言實(shí)現(xiàn)。因?yàn)槲覀冎?Go 擁有一個(gè)良好的社區(qū)和一個(gè)非常棒的工具集,所以我們選擇了 Go。

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

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

我們可以把內(nèi)存映射想象成一棵樹(shù),遍歷那棵樹(shù)我們會(huì)得到不同的對(duì)象分配和關(guān)系。這意味著無(wú)論如何 根會(huì)保持內(nèi)存而不被 GCing(垃圾回收)內(nèi)存的原因。因?yàn)樵?Go 中沒(méi)有簡(jiǎn)單的方法來(lái)分析完整的 core dump,所以很難找到一個(gè)對(duì)象的根沒(méi)有被 GC 過(guò)。

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

內(nèi)存泄漏

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

當(dāng)我們?cè)谛碌脑O(shè)計(jì)原則下構(gòu)建我們的系統(tǒng)時(shí),這些考慮并不重要。更重要的是以避免過(guò)早優(yōu)化的方式構(gòu)建系統(tǒng),并使你能夠在代碼成熟后再優(yōu)化它們,而不是從一開(kāi)始就過(guò)度設(shè)計(jì)它。然而,一些內(nèi)存壓力常見(jiàn)問(wèn)題的例子是:

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

大量使用反射或字符串

使用全局變量

孤兒,沒(méi)有結(jié)束的 goroutines

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

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

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

go tool pprof

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

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

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

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

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

pprof 畫(huà)像

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

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

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

Go 有幾個(gè)內(nèi)置的畫(huà)像供我們?cè)诔R?jiàn)情況下使用:

goroutine - 所有當(dāng)前 goroutines 的堆棧跟蹤

heap - 活動(dòng)對(duì)象的內(nèi)存分配的樣本

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

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

block - 導(dǎo)致阻塞同步原語(yǔ)的堆棧跟蹤

mutex - 爭(zhēng)用互斥持有者的堆棧跟蹤

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

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

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

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

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

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

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

請(qǐng)注意,WriteHeapProfile是用于運(yùn)行的語(yǔ)法糖:

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

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

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

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

導(dǎo)入net/http/pprof的“副作用”是在/debug/pprof的 web 服務(wù)器根目錄下會(huì)注冊(cè) pprof 端點(diǎn)?,F(xiàn)在使用 curl 我們可以獲取要查看的堆信息文件:

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

只有在你的程序之前沒(méi)有 http listener 時(shí)才需要添加上面的http.ListenAndServe()。如果有的話(huà)就沒(méi)有必要再監(jiān)聽(tīng)了,它會(huì)自動(dòng)處理。還可以使用ServeMux.HandleFunc()來(lái)設(shè)置它,這對(duì)于更復(fù)雜的 http 程序有意義。

使用 pprof

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

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

這意味著默認(rèn)情況下,對(duì)于在 pprof 監(jiān)控下抖動(dòng)的小對(duì)象,可能會(huì)出現(xiàn)問(wèn)題。對(duì)于大型代碼庫(kù)/長(zhǎng)期運(yùn)行的程序,這不是問(wèn)題。

一旦收集好畫(huà)像文件后,就可以將其加載到 pprof 的交互式命令行中了,通過(guò)運(yùn)行:

> 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)

這里要注意的事項(xiàng)是Type:inuse_space。這意味著我們正在查看特定時(shí)刻的內(nèi)存分配數(shù)據(jù)(當(dāng)我們捕獲該配置文件時(shí))。type 是sample_index的配置值,可能的值為:

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

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

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

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

現(xiàn)在在交互命令行中輸入top,將輸出頂級(jí)內(nèi)存的消費(fè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)

我們可以看到關(guān)于Dropped Nodes的一系列數(shù)據(jù),這意味著它們被過(guò)濾掉了。一個(gè)節(jié)點(diǎn)或樹(shù)中的一個(gè)“節(jié)點(diǎn)”就是一整個(gè)對(duì)象。丟棄節(jié)點(diǎn)是降噪的好主意,但有時(shí)它可能會(huì)隱藏內(nèi)存問(wèn)題產(chǎn)生的根本原因。我們繼續(xù)看一個(gè)例子。

如果要該畫(huà)像文件的所有數(shù)據(jù),請(qǐng)?jiān)谶\(yùn)行 pprof 時(shí)添加-nodefraction=0選項(xiàng),或在交互命令行中鍵入nodefraction=0。

在輸出列表中,我們可以看到兩個(gè)值,flatcum。

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

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

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

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

可視化

雖然topN提供了一個(gè)文本列表,但 pprof 附帶了幾個(gè)非常有用的可視化選項(xiàng)??梢暂斎?b>png或gif等等(請(qǐng)參閱go tool pprof -help獲取完整列表)。

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

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

請(qǐng)注意,在上圖中,我從執(zhí)行模式inuse_space中取出了一個(gè) png。很多時(shí)候你也應(yīng)該看看inuse_objects,因?yàn)樗梢詭椭阏业絻?nèi)存分配問(wèn)題。

深入挖掘,尋找根本原因

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

在我們的例子中,我們可以看到內(nèi)存由membuffers持有,這是我們的數(shù)據(jù)序列化庫(kù)。這并不意味著我們?cè)谠摯a段有內(nèi)存泄漏,這意味著該函數(shù)持有了內(nèi)存。了解如何閱讀圖表以及一般的 pprof 輸出非常重要。在這個(gè)例子中,當(dāng)我們序列化數(shù)據(jù)時(shí),意味著我們將內(nèi)存分配給結(jié)構(gòu)和原始對(duì)象(int,string),它永遠(yuǎn)不會(huì)被釋放。

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

我們可以看到日志庫(kù)中鏈中的某個(gè)地方,控制著>50MB 的已分配內(nèi)存。這是由我們的日志記錄器調(diào)用函數(shù)分配的內(nèi)存。經(jīng)過(guò)思考,這實(shí)際上是預(yù)料之中的。日志記錄器會(huì)分配內(nèi)存,是因?yàn)樗枰蛄谢瘮?shù)據(jù)以將其輸出到日志,因此它會(huì)造成進(jìn)程中的內(nèi)存分配。

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

是時(shí)候介紹另一個(gè)名為listpprof命令。它接受一個(gè)正則表達(dá)式,該表達(dá)式是內(nèi)容的過(guò)濾器。 “l(fā)ist”實(shí)際上是與分配相關(guān)的帶注釋的源代碼。在我們可以看到在日志記錄器的上下文中將執(zhí)行list RequestNew,因?yàn)槲覀兿M吹綄?duì)日志記錄器的調(diào)用。這些調(diào)用來(lái)自恰好以相同前綴開(kāi)頭的兩個(gè)函數(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)容相關(guān)。此時(shí)很容易看出日志記錄器分配內(nèi)存是因?yàn)槲覀儼l(fā)送了整個(gè)“block”對(duì)象造成的。這個(gè)對(duì)象需要序列化它的某些部分(我們的對(duì)象是 membuffer 對(duì)象,它實(shí)現(xiàn)了一些String()函數(shù))。它是一個(gè)有用的日志,還是一個(gè)好的做法?可能不是,但它不是日志記錄器端或調(diào)用日志記錄器的代碼產(chǎn)生了內(nèi)存泄漏,

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

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

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

此時(shí),在 Java 或.Net 的情況下,我們將打開(kāi)一些"gc roots"分析或分析器,并獲取引用該數(shù)據(jù)并造成泄漏的實(shí)際對(duì)象。正如所解釋的那樣,對(duì)于 Go 來(lái)說(shuō)這是不可能的,因?yàn)楣ぞ邌?wèn)題也是因?yàn)?Go 低等級(jí)的內(nèi)存表示。

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

此時(shí),因?yàn)槲覀兪煜の覀兊南到y(tǒng),所以很容易理解這不再是一個(gè) bug。它(幾乎)是設(shè)計(jì)的。但是讓我們繼續(xù)探索如何從工具(pprof)中獲取信息以找到根本原因。

設(shè)置nodefraction=0時(shí),我們將看到已分配對(duì)象的整個(gè)圖,包括較小的對(duì)象。我們來(lái)看看輸出:

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

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

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

那我們?nèi)绾涡迯?fù)呢

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

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

要查看函數(shù)的帶注釋的源代碼,我們可以運(yùn)行list lazy。我們使用lazy,因?yàn)槲覀冋趯ふ业暮瘮?shù)名是lazyCalcOffsets(),而且我們的代碼中也沒(méi)有以 lazy 開(kāi)頭的其他函數(shù)。當(dāng)然輸入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

我們可以看到兩個(gè)有趣的信息。同樣,請(qǐng)記住 pprof 堆畫(huà)像文件會(huì)對(duì)有關(guān)分配的信息進(jìn)行采樣。我們可以看到flatcum數(shù)字是相同的。這表明分配的內(nèi)存也在這些分配點(diǎn)保留。

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

這讓我們學(xué)習(xí)了映射 map,其中 map 的賦值不是簡(jiǎn)單的變量賦值。本文詳細(xì)介紹了 map 的工作原理。簡(jiǎn)而言之,map 與切片相比,map 開(kāi)銷(xiāo)更大,“成本”更大,元素更多。

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

在上面的代碼中,在檢查了我們?nèi)绾问褂迷?map 之后,我們意識(shí)到雖然我們想象它是一個(gè)稀疏數(shù)組,但它并不是那么稀疏。這與上面描述的情況匹配,我們能馬上想到一個(gè)將 map 改為切片的小型重構(gòu)實(shí)際上是可行的,并且可能使該代碼內(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

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

讓我們來(lái)看看benchcmp的幾次測(cè)試

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%

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

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

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

需要注意的是,對(duì)于較小的數(shù)據(jù)集,在切片滿(mǎn)足的情況就不要使用 map,因?yàn)?map 的開(kāi)銷(xiāo)很大。

完整的 core dump

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

雖然現(xiàn)在我們沒(méi)有“陷入困境”,因?yàn)闆](méi)有很好的解決方案來(lái)探索完全轉(zhuǎn)儲(chǔ)(full down)。 目前為止,pprof回答了我們所有的問(wèn)題。

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

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

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

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

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

pprof UI

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

go tool pprof -http=:8080 heap.out

此時(shí)它應(yīng)該打開(kāi) Web 瀏覽器。如果沒(méi)有,則瀏覽你設(shè)置的端口。它使你能夠比命令行更快地更改選項(xiàng)并獲得視覺(jué)反饋。消費(fèi)信息的一種非常有用的方法。

UI 確實(shí)讓我熟悉了火焰圖,它可以非??焖俚乇┞洞a的罪魁禍?zhǔn)住?/p> 結(jié)論

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

其他一些好的文章:

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

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

medium.com/@cep21/usin… - go tool trace是用來(lái)做 CPU 分析的,這是一個(gè)關(guān)于該分析功能的不錯(cuò)的帖子。

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

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/6922.html

相關(guān)文章

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

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

    番茄西紅柿 評(píng)論0 收藏0
  • [] Go 語(yǔ)言命令概覽

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

    antyiwei 評(píng)論0 收藏0
  • [] Go 語(yǔ)言命令概覽

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

    番茄西紅柿 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<