返回顶部
首页 > 资讯 > 后端开发 > GO >定位并修复 Go 中的内存泄露问题
  • 721
分享到

定位并修复 Go 中的内存泄露问题

2024-04-02 19:04:59 721人浏览 薄情痞子
摘要

Go 是一门带 GC 的语言,因此,大家很容易认为它不会有内存泄露问题。 大部分时候确实不会,但如果有些时候使用不注意,也会导致泄露。 本文案例来自谷歌云的代码,探讨如何找到并修复

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.pprof
File: serverless-scheduler-proxy
Type: inuse_space
Time: May 4, 2021 at 9:33am (EDT)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top10
Showing nodes accounting for 2129.67kB, 100% of 2129.67kB total
Showing top 10 nodes out of 30
      flat  flat%   sum%        cum   cum%
 1089.33kB 51.15% 51.15%  1089.33kB 51.15%  google.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%  google.golang.org/grpc/metadata.Join
         0     0%   100%   512.17kB 24.05%  cloud.google.com/go/secretmanager/apiv1.(*Client).AccessSecretVersion
         0     0%   100%   512.17kB 24.05%  cloud.google.com/go/secretmanager/apiv1.(*Client).AccessSecretVersion.func1
         0     0%   100%   512.17kB 24.05%  github.com/googleapis/gax-go/v2.Invoke
         0     0%   100%   512.17kB 24.05%  github.com/googleapis/gax-go/v2.invoke
         0     0%   100%   512.17kB 24.05%  google.golang.org/genproto/googleapis/cloud/secretmanager/v1.(*secretManagerServiceClient).AccessSecretVersion
         0     0%   100%   512.17kB 24.05%  google.golang.org/grpc.(*ClientConn).Invoke
         0     0%   100%  1617.50kB 75.95%  google.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.pprof
File: serverless-scheduler-proxy
Type: inuse_space
Time: May 4, 2021 at 9:50am (EDT)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top10
Showing nodes accounting for 94.74MB, 94.49% of 100.26MB total
Dropped 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%  google.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%  google.golang.org/grpc/internal/grpcsync.NewEvent
       1MB     1% 93.50%        1MB     1%  runtime.malg
       1MB     1% 94.49%        1MB     1%  encoding/json.(*decodeState).literalStore

google.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.google.com 上 [9] 文档的大部分 Go 示例。用户注意到我们忘记调用 client.Close 了。

我曾多次看到同样的事情出现,所以我决定调查整个 repo。

我开始粗略估计有多少受影响的文件。使用 grep ,我们可以获得包含 NewClient 样式调用的所有文件的列表,然后将该列表传递给另一个调用 grep 以仅列出不包含 Close 的文件,同时忽略测试文件:

$ 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 开始修复格式。然后,我检查了每个编辑过的文件,以确保它做了正确的事情:

  • 在服务器应用程序中,我们应该关闭客户端,还是应该保留它以备将来的请求使用?
  • 是 Client 实际的名字 client 还是别的什么?
  • 是否有一个以上的 Client 调用了 Close ?

完成后,只剩下 180 个已编辑的文件 [13] 。

最后一项工作是努力使其不再发生在用户身上。我们想到了几种方法:

  1. 更好的示例代码;
  2. 更好的 GoDoc。我们更新了库生成器,在生成库时加上注释,告知 client 需要调用 Close;
  3. 更好的库。有没有办法可以自动 Close 客户端?Finalizers?知道何能做得更好吗?欢迎在 https://github.com/googleapis/google-cloud-go/issues/4498 上交流;

我希望你对 Go、内存泄漏 pprof 、gRPC 和 Bash 有所了解。我很想听听你关于发现的内存泄漏以及修复它们的方法的故事!如果你对我们如何改进我们的 库 [14] 或 示例 [15] 有任何想法,请通过提交 issue 告诉我们。

参考资料

[1]
Google Cloud Go 客户端库: https://github.com/googleapis/google-cloud-go

[2]
Cloud Run: https://cloud.google.com/run/docs/quickstarts/build-and-deploy/go

[3]
Go 服务器: https://github.com/googleapis/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.google.com/secret-manager/docs/quickstart

[7]
自动部署: https://cloud.google.com/build/docs/deploying-builds/deploy-cloud-run

[8]
Cloud 的 Go 示例存储库中: https://github.com/GoogleCloudPlatfORM/golang-samples

[9]
cloud.google.com 上: https://cloud.google.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/googleapis/google-cloud-go

[15]
示例: https://github.com/GoogleCloudPlatform/golang-samples

到此这篇关于定位并修复 Go 中的内存泄露的文章就介绍到这了,更多相关定位Go内存泄露内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

您可能感兴趣的文档:

--结束END--

本文标题: 定位并修复 Go 中的内存泄露问题

本文链接: https://lsjlt.com/news/155553.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 定位并修复 Go 中的内存泄露问题
    Go 是一门带 GC 的语言,因此,大家很容易认为它不会有内存泄露问题。 大部分时候确实不会,但如果有些时候使用不注意,也会导致泄露。 本文案例来自谷歌云的代码,探讨如何找到并修复 ...
    99+
    2024-04-02
  • 如何解决定位并修复Go 中的内存泄露问题
    这篇文章将为大家详细讲解有关如何解决定位并修复Go 中的内存泄露问题,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。Google Cloud Go 客户端库 [1] 通常在后台使用 gRPC 来连接 Goo...
    99+
    2023-06-25
  • linux内存泄露问题怎么定位
    定位 Linux 内存泄漏问题可以采取以下几种方法: 使用top命令或htop命令查看进程的内存使用情况,观察内存占用的增长情况...
    99+
    2024-02-29
    linux
  • android内存泄露的问题
    Java内存泄漏是每个Java程序员都会遇到的问题,程序在本地运行一切正常,可是布署到远端会出现内存无限制的增长,后系统瘫痪,那么如何快好的检测程序的稳定性,防止系统崩盘,...
    99+
    2022-06-06
    Android
  • android的GC内存泄露问题
    1. android内存泄露概念 不少人认为JAVA程序,因为有垃圾回收机制,应该没有内存泄露。其实如果我们一个程序中,已经不再使用某个对象,但是因为仍然有引用指向它,垃圾回收...
    99+
    2022-06-06
    Android
  • Go 中 time.After 可能导致的内存泄露问题解析
    目录一、Time 包中定时器函数定时函数:NewTicker,NewTimer 和 time.After 介绍二、time.After 导致的内存泄露基本用法有问题代码用pprof分...
    99+
    2023-05-18
    go time.After 内存泄露 time.After 内存泄露 go time.After 
  • Java中的内存泄露问题和解决办法
    目录为什么会产生内存泄漏?内存泄漏对程序的影响?如何检查和分析内存泄漏?常见的内存泄漏及解决方法1、单例造成的内存泄漏2、非静态内部类创建静态实例造成的内存泄漏【已无】3、Handl...
    99+
    2024-04-02
  • 怎么调试Python程序的内存泄露问题
    这篇文章主要讲解了“怎么调试Python程序的内存泄露问题”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么调试Python程序的内存泄露问题”吧!如果大家在 Linux 或者 macOS ...
    99+
    2023-06-16
  • 如何解决Go语言中的并发内存泄漏问题?
    如何解决Go语言中的并发内存泄漏问题?引言:随着大数据和云计算时代的到来,对于并发编程的需求变得越来越迫切。而Go语言作为一门支持高并发的语言,受到了广泛的关注和应用。然而,并发编程不仅仅带来了高性能和高效率,同时也带来了一些风险,其中最常...
    99+
    2023-10-22
    内存管理 并发控制 内存泄漏修复
  • 如何通过wrap malloc定位C/C++的内存泄漏问题
    目录前言什么是内存泄漏?怎么查内存泄漏?什么是动态内存分配器?new/delete跟malloc/free的关系chunkwrap malloc怎么去定位内存泄漏呢?怎么跟踪到每种s...
    99+
    2024-04-02
  • 怎么通过wrap malloc定位C/C++的内存泄漏问题
    小编给大家分享一下怎么通过wrap malloc定位C/C++的内存泄漏问题,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!前言用C/C++开发的程序执行效率很高,...
    99+
    2023-06-15
  • JavaScript在IE9之前版本中内存泄露问题的示例分析
    这篇文章主要介绍了JavaScript在IE9之前版本中内存泄露问题的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。IE9之前的版本...
    99+
    2024-04-02
  • 浅析Node.js中的内存泄漏问题
    这篇文章是由Mozilla的Identity团队带来的 A Node.JS Holiday Season系列文章的首篇,该团队上个月发布了 Persona的第一个测试版本。在开发Persona时我们构建了...
    99+
    2022-06-04
    内存 Node js
  • 如何理解Go语言的HTTP标准库中的内存泄漏问题
    这篇文章给大家介绍如何理解Go语言的HTTP标准库中的内存泄漏问题,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。使用一个go库实现的一个http服务器:package mai...
    99+
    2024-04-02
  • 如何解决PHP开发中的内存泄漏问题
    导语:内存泄漏是指程序执行时无法释放已经分配的内存,导致内存占用不断增加,最终导致程序崩溃。在PHP开发中,内存泄漏是一个普遍存在的问题。本文将介绍如何解决PHP开发中的内存泄漏问题,并提供具体的代码示例。一、使用unset()函数手动释放...
    99+
    2023-10-21
    内存泄漏 解决方法 PHP开发
  • 如何解决Go语言中的并发内存访问冲突问题?
    如何解决Go语言中的并发内存访问冲突问题?在Go语言中,我们可以使用goroutine来实现并发编程,这无疑给我们带来了更强大的性能和并行处理能力。然而,并发编程也会引发一些问题,其中最常见的就是内存访问冲突。内存访问冲突问题是指多个gor...
    99+
    2023-10-22
  • 如何解决ie中img标签内存泄漏的问题
    这篇文章主要为大家展示了“如何解决ie中img标签内存泄漏的问题”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何解决ie中img标签内存泄漏的问题”这篇文章吧...
    99+
    2024-04-02
  • C++中内存泄漏问题的分析与解决方案
    C++中内存泄漏问题的分析与解决方案概述:内存泄漏是指程序在动态分配内存后,没有及时释放导致内存无法再被程序使用的情况。在C++开发中,内存泄漏是一个常见且严重的问题,一旦发生,会导致程序运行效率下降,最终可能导致程序崩溃。本文将对C++中...
    99+
    2023-10-22
    分析(Analysis) 解决方案(Solution) 内存泄漏(Memory Leak)
  • ASP编程中,如何避免容器的内存泄漏问题?
    在ASP编程中,我们经常使用容器来存储数据,如数组、哈希表、列表等。这些容器在使用过程中,可能会出现内存泄漏的问题,导致程序运行变慢、内存占用过高等问题。本文将介绍如何避免容器的内存泄漏问题,并提供演示代码。 一、什么是内存泄漏? 内存泄...
    99+
    2023-06-01
    leetcode 编程算法 容器
  • C++技术中的调试:内存问题侦查与修复指南
    c++++ 技术中的内存问题可通过 gdb、valgrind 和 addresssanitizer 侦查与修复。使用 gdb 可查找段错误,valgrind 可检测内存泄漏,而 addr...
    99+
    2024-05-07
    调试 c++
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作