Go 是定位的内一门带 GC 的言语,是并修以,大年夜师很随便认为它不会有内存泄漏问题 。存泄 除夜部分时辰切实其实不会,漏问但假如有些时辰独霸不寄看,定位的内也会招致泄漏。并修
本案牍例来自谷歌云的存泄代码 ,参议若何找到并修复 Go 中的漏问内存泄漏。(切实其实来讲是定位的内因为成本泄漏招致的内存泄漏,除本文引见的并修 ,另有一些其他泄漏的存泄气候)
这篇文章回想回想了我若何创作创造内存泄漏 、若何修复它 ,漏问和我若何修复 Google 示例 Go 代码中的定位的内近似问题,和我们若何改进我们的并修库以阻拦将来产生发火这类气候。
Google Cloud Go 客户端库 [1] 但凡在布景独霸 gRPC 来邻接 Google Cloud API 。存泄成立 API 客户端时,库会初始化与 API 的邻接,然后贯穿连接该邻接处于翻开外形 ,直到你调用 Client.Close 。
client, err := api.NewClient()// Check err.defer client.Close()
客户端可以安然地同时独霸 ,所以你理应贯穿连接不异 Client 直到你的义务完成 。可是,假定在理应 Close 的时辰不 Close client 会产生发火甚么呢 ?
会展示内存泄漏 。底层邻接永远不会被拾掇。
Google 有一堆 GitHub 主动化机械人来辅佐治理数百个 GitHub 存储库。我们的一些机械人经由过程在 Cloud Run [2] 上运转的 Go 处事器 [3] 代办代办代办代理它们的请求。我们的内存独霸看起来像一个经典的锯齿形内存泄漏:
我经由过程向处事器添加 pprof.Index 措置法度圭表类型最早调试 :
mux.HandleFunc("/debug/pprof/", pprof.Index)`pprof` [4] 供给运转时 profiling 数据,如内存独霸气候。有关更多信息 ,请参阅 Go 官方博客上的 profiling Go 法度圭表类型 [5] 。
然后 ,我在外埠构建并启动了处事器 :
$ go build$ PROJECT_ID=my-project PORT=8080 ./serverless-scheduler-proxy
然后向处事器发送一些请求:
for i in { 1..5}; do curl --header "Content-Type: application/json" --request POST --data '{ "name": "HelloHTTP", "type": "testing", "location": "us-central1"}' localhost:8080/v0/cron echo " -- $i"done切实其实的有效负载和端点特定于我们的处事器,与本文有关 。
为了掉落踪掉落踪正在独霸的内存的基线,我群集了一些初始 pprof 数据:
curl http://localhost:8080/debug/pprof/heap > heap.0.pprof
搜检输进,你可以看到一些内存独霸气候 ,但没有甚么会当即成为一个除夜问题(这很好 !我们刚才启动了处事器!) :
$ go tool pprof heap.0.pprofFile: serverless-scheduler-proxyType: inuse_spaceTime: May 4, 2021 at 9:33am (EDT)Entering interactive mode (type "help" for commands, "o" for options)(pprof) top10Showing nodes accounting for 2129.67kB, 100% of 2129.67kB totalShowing top 10 nodes out of 30 flat flat% sum% cum cum% 1089.33kB 51.15% 51.15% 1089.33kB 51.15% 谷歌.golang.org/grpc/internal/transport.newBufWriter (inline) 528.17kB 24.80% 75.95% 528.17kB 24.80% bufio.NewReaderSize (inline) 512.17kB 24.05% 100% 512.17kB 24.05% 谷歌.golang.org/grpc/metadata.Join 0 0% 100% 512.17kB 24.05% cloud.谷歌.com/go/secretmanager/apiv1.(*Client).AccessSecretVersion 0 0% 100% 512.17kB 24.05% cloud.谷歌.com/go/secretmanager/apiv1.(*Client).AccessSecretVersion.func1 0 0% 100% 512.17kB 24.05% github.com/谷歌apis/gax-go/v2.Invoke 0 0% 100% 512.17kB 24.05% github.com/谷歌apis/gax-go/v2.invoke 0 0% 100% 512.17kB 24.05% 谷歌.golang.org/genproto/谷歌apis/cloud/secretmanager/v1.(*secretManagerServiceClient).AccessSecretVersion 0 0% 100% 512.17kB 24.05% 谷歌.golang.org/grpc.(*ClientConn).Invoke 0 0% 100% 1617.50kB 75.95% 谷歌.golang.org/grpc.(*addrConn).createTransport
下一步是向处事器发送一堆请求 ,看看我们可否可以 (1) 重现大年夜大年夜约的内存泄漏和 (2) 断定泄漏是甚么。
发送 500 个请求 :
for i in { 1..500}; do curl --header "Content-Type: application/json" --request POST --data '{ "name": "HelloHTTP", "type": "testing", "location": "us-central1"}' localhost:8080/v0/cron echo " -- $i"done群集和分化更多 pprof 数据:
$ curl http://localhost:8080/debug/pprof/heap > heap.6.pprof$ go tool pprof heap.6.pprofFile: serverless-scheduler-proxyType: inuse_spaceTime: May 4, 2021 at 9:50am (EDT)Entering interactive mode (type "help" for commands, "o" for options)(pprof) top10Showing nodes accounting for 94.74MB, 94.49% of 100.26MB totalDropped 26 nodes (cum <= 0.50MB)Showing top 10 nodes out of 101 flat flat% sum% cum cum% 51.59MB 51.46% 51.46% 51.59MB 51.46% 谷歌.golang.org/grpc/internal/transport.newBufWriter 19.60MB 19.55% 71.01% 19.60MB 19.55% bufio.NewReaderSize 6.02MB 6.01% 77.02% 6.02MB 6.01% bytes.makeSlice 4.51MB 4.50% 81.52% 10.53MB 10.51% crypto/tls.(*Conn).readHandshake 4MB 3.99% 85.51% 4.50MB 4.49% crypto/x509.parseCertificate 3MB 2.99% 88.51% 3MB 2.99% crypto/tls.Client 2.50MB 2.49% 91.00% 2.50MB 2.49% golang.org/x/net/http2/hpack.(*headerFieldTable).addEntry 1.50MB 1.50% 92.50% 1.50MB 1.50% 谷歌.golang.org/grpc/internal/grpcsync.NewEvent 1MB 1% 93.50% 1MB 1% runtime.malg 1MB 1% 94.49% 1MB 1% encoding/json.(*decodeState).literalStore
谷歌.golang.org/grpc/internal/transport.newBufWriter 独霸除夜量内存真的很凹陷!这是泄漏与甚么相干的第一个迹象 :gRPC 。搜检我们的独霸法度圭表类型源代码,我们独一独霸 gRPC 的中心是 Google Cloud Secret Manager [6] :
client, err := secretmanager.NewClient(ctx) if err != nil { return nil, fmt.Errorf("failed to create secretmanager client: %v", err) }在每个请求成立 client 时,我们没有调用 client.Close() !所以,我添加了一个 Close 调用,问题就磨灭踪踪踪了:
defer client.Close()
我提交了修复 ,然后 主动放置 [7] ,锯齿当即磨灭踪踪踪了!
除夜约在不应时分,用户在我们的 Cloud 的 Go 示例存储库中 [8] 提交了一个问题,个中包含 cloud.谷歌.com 上 [9] 文档的除夜部分 Go 示例。用户寄看到我们遗忘调用 client.Close 了 。
我曾多次看到一样的工作展示,所以我决意查询访谒全数 repo 。
我最早大年夜大年夜致估计有若干很多若干良多若干很多若干良多若干很多若干受影响的文件。独霸 grep ,我们可以掉落踪掉落踪搜聚 NewClient 格式调用的全数文件的列表,然后将该列表传递给此外一个调用 grep 以仅列出不包含 Close 的文件,同时忽视测试文件 :
$ grep -L Close $(grep -El 'New[^(]*Client' **/*.go) | grep -v test
竟然有 207 个文件……就凹凸文而言 ,我们 .go 在 GoogleCloudPlatform/golang-samples [10] 存储库中有除夜约 1300 个文件 。
思虑到问题标局限 ,我认为一些主动化是 值得的 [11] 。我不想写一个无缺的 Go 法度圭表类型来编辑文件,所以我独霸 Bash:
$ grep -L Close $(grep -El 'New[^(]*Client' **/*.go) | grep -v test | xargs sed -i '/New[^(]*Client/,/}/s/}/}\ndefer client.Close()/'
它是完竣的吗 ?不 。它对工作量有很除夜的影响吗 ?是的 !
第一部分(直到 test )与上面无缺不异——掉落踪掉落踪全数大年夜大年夜约受影响的文件的列表(那些似乎成立了 Client 但从没调用 Close 的文件)。
然后 ,我将该文件列表传递给 sed 举解决论编辑。 xargs 调用你给它的呼唤,每行都以 stdin 作为参数传递给给定的呼唤 。
要邃晓该 sed 呼唤 ,搜检 golang-samples repo 示例是甚么容貌有助于邃晓(省略导进和客户端初始化后的全数内容) :
// accessSecretVersion accesses the payload for the given secret version if one// exists. The version can be a version number as a string (e.g. "5") or an// alias (e.g. "latest").func accessSecretVersion(w io.Writer, name string) error { // name := "projects/my-project/secrets/my-secret/versions/5" // name := "projects/my-project/secrets/my-secret/versions/latest" // Create the client. ctx := context.Background() client, err := secretmanager.NewClient(ctx) if err != nil { return fmt.Errorf("failed to create secretmanager client: %v", err) } // ...}在高层次上,我们初始化客户端并搜检可否有偏向 。每当你搜检偏向时,都邑有一个右花括号 ( } )。我独霸这些信息来主动化编辑。
可是,该 sed 呼唤还是很愚蠢 :
sed -i '/New[^(]*Client/,/}/s/}/}\ndefer client.Close()/'
-i 展示直接编辑文件 。这不是问题,因为代码用 git 治理了 。
接上往,我独霸 s 呼唤在搜检偏向 defer client.Close() 后假定的右花括号 ( } )此后拔出。
可是,我不想更调每个 } ,我只想要在 调用 NewClient 后 的 第一个 。要做到这一点,你可以给一个 地址局限 [12] 的 sed 搜刮 。
地址局限可以包含在独霸接上往的任何呼唤之前要婚配的最早和完义务势 。在这类气候下,最早是 /New[^(]*Client/ ,婚配 NewClient 圭表类型调用,停止(由 a 离开 , )是 /}/ ,婚配下一个除夜括号。这意味着我们的搜刮和更调仅合用于调用 NewClient 和停止除夜括号之间 !
经由过程体味上面的偏向措置编制, if err != nil 前提的右除夜括号恰是我们想要拔出 Close 调用的职位。
一旦我主动编辑了全数示例文件,我用 goimports 最早修复格式。然后 ,我搜检了每个编辑过的文件 ,以确保它做了切确的工作:
完成后,只剩下 180 个已编辑的文件 [13] 。
末尾一项工作是戮力使其不再产生发火在用户身上。我们想到了几种编制 :
我希看你对 Go 、内存泄漏 pprof 、gRPC 和 Bash 有所体味 。我很想听听你关于创作创造的内存泄漏和修复它们的编制的故事 !假定你对我们若何改进我们的 库 [14] 或 示例 [15] 有任何设法 ,请经由过程提交 issue 陈述我们。
[1]
Google Cloud Go 客户端库: https://github.com/谷歌apis/谷歌-cloud-go
[2]
Cloud Run: https://cloud.谷歌.com/run/docs/quickstarts/build-and-deploy/go
[3]
Go 处事器: https://github.com/谷歌apis/repo-automation-bots/tree/main/serverless-scheduler-proxy
[4]
pprof: https://pkg.go.dev/net/http/pprof
[5]
profiling Go 法度圭表类型: https://go.dev/blog/pprof
[6]
Google Cloud Secret Manager: https://cloud.谷歌.com/secret-manager/docs/quickstart
[7]
主动放置: https://cloud.谷歌.com/build/docs/deploying-builds/deploy-cloud-run
[8]
Cloud 的 Go 示例存储库中: https://github.com/GoogleCloudPlatform/golang-samples
[9]
cloud.谷歌.com 上: https://cloud.谷歌.com/
[10]
GoogleCloudPlatform/golang-samples: https://github.com/GoogleCloudPlatform/golang-samples
[11]
值得的: https://xkcd.com/1205/
[12]
地址局限: https://www.gnu.org/software/sed/manual/html_node/Addresses.html
[13]
180 个已编辑的文件: https://github.com/GoogleCloudPlatform/golang-samples/pull/2080
[14]
库: https://github.com/谷歌apis/谷歌-cloud-go
[15]
示例: https://github.com/GoogleCloudPlatform/golang-samples
到此这篇关于定位并修复 Go言语 中的内存泄漏的文章就引见到这了,更多相干定位Go内存泄漏内容请搜刮完竣下载之前的文章或延续不雅不雅不雅不雅鉴赏上面的相干文章希看大年夜师往后多多支撑完竣下载!