<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          定位并修復(fù) Go 中的內(nèi)存泄露

          共 3269字,需瀏覽 7分鐘

           ·

          2021-10-28 19:18

          Go 是一門帶 GC 的語言,因此,大家很容易認(rèn)為它不會(huì)有內(nèi)存泄露問題。大部分時(shí)候確實(shí)不會(huì),但如果有些時(shí)候使用不注意,也會(huì)導(dǎo)致泄露。

          本文案例來自谷歌云的代碼,探討如何找到并修復(fù) Go 中的內(nèi)存泄露。(確切來說是因?yàn)橘Y源泄露導(dǎo)致的內(nèi)存泄露,除了本文介紹的,還有一些其他泄露的情況)


          這篇文章回顧了我如何發(fā)現(xiàn)內(nèi)存泄漏、如何修復(fù)它,以及我如何修復(fù) Google 示例 Go 代碼中的類似問題,以及我們?nèi)绾胃倪M(jìn)我們的庫以防止將來發(fā)生這種情況。

          Google Cloud Go 客戶端庫[1] 通常在后臺(tái)使用 gRPC 來連接 Google Cloud API。創(chuàng)建 API 客戶端時(shí),庫會(huì)初始化與 API 的連接,然后保持該連接處于打開狀態(tài),直到你調(diào)用 Client.Close。

          client,?err?:=?api.NewClient()
          //?Check?err.
          defer?client.Close()

          客戶端可以安全地同時(shí)使用,所以你應(yīng)該保持相同Client直到你的任務(wù)完成。但是,如果在應(yīng)該 Close 的時(shí)候不 Close client 會(huì)發(fā)生什么呢?

          會(huì)出現(xiàn)內(nèi)存泄漏。底層連接永遠(yuǎn)不會(huì)被清理。


          Google 有一堆 GitHub 自動(dòng)化機(jī)器人來幫助管理數(shù)百個(gè) GitHub 存儲(chǔ)庫。我們的一些機(jī)器人通過在 Cloud Run[2] 上運(yùn)行的Go 服務(wù)器[3]代理它們的請求。我們的內(nèi)存使用看起來像一個(gè)經(jīng)典的鋸齒形內(nèi)存泄漏:

          我通過向服務(wù)器添加 pprof.Index 處理程序開始調(diào)試:

          mux.HandleFunc("/debug/pprof/",?pprof.Index)

          `pprof`[4]提供運(yùn)行時(shí) profiling 數(shù)據(jù),如內(nèi)存使用情況。有關(guān)更多信息,請參閱 Go 官方博客上的 profiling Go 程序[5]。

          然后,我在本地構(gòu)建并啟動(dòng)了服務(wù)器:

          $?go?build
          $?PROJECT_ID=my-project?PORT=8080?./serverless-scheduler-proxy

          然后向服務(wù)器發(fā)送一些請求:

          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

          確切的有效負(fù)載和端點(diǎn)特定于我們的服務(wù)器,與本文無關(guān)。

          為了獲得正在使用的內(nèi)存的基線,我收集了一些初始pprof數(shù)據(jù):

          curl?http://localhost:8080/debug/pprof/heap?>?heap.0.pprof

          檢查輸出,你可以看到一些內(nèi)存使用情況,但沒有什么會(huì)立即成為一個(gè)大問題(這很好!我們剛剛啟動(dòng)了服務(wù)器?。?/p>

          $?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

          下一步是向服務(wù)器發(fā)送一堆請求,看看我們是否可以 (1) 重現(xiàn)可能的內(nèi)存泄漏和 (2) 確定泄漏是什么。

          發(fā)送 500 個(gè)請求:

          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數(shù)據(jù):

          $?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使用大量內(nèi)存真的很突出!這是泄漏與什么相關(guān)的第一個(gè)跡象:gRPC。查看我們的應(yīng)用程序源代碼,我們唯一使用 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)?
          }

          在每個(gè)請求創(chuàng)建 client 時(shí),我們沒有調(diào)用 client.Close()!所以,我添加了一個(gè)Close調(diào)用,問題就消失了:

          defer?client.Close()

          我提交了修復(fù),然后自動(dòng)部署[7],鋸齒立即消失了!


          大約在同一時(shí)間,用戶在我們的 Cloud 的 Go 示例存儲(chǔ)庫中[8]提交了一個(gè)問題,其中包含 cloud.google.com 上[9] 文檔的大部分 Go 示例。用戶注意到我們忘記調(diào)用 client.Close 了。

          我曾多次看到同樣的事情出現(xiàn),所以我決定調(diào)查整個(gè) repo。

          我開始粗略估計(jì)有多少受影響的文件。使用grep,我們可以獲得包含NewClient樣式調(diào)用的所有文件的列表,然后將該列表傳遞給另一個(gè)調(diào)用grep以僅列出不包含 Close 的文件,同時(shí)忽略測試文件:

          $?grep?-L?Close?$(grep?-El?'New[^(]*Client'?**/*.go)?|?grep?-v?test

          竟然有 207 個(gè)文件……就上下文而言,我們 .goGoogleCloudPlatform/golang-samples[10] 存儲(chǔ)庫中有大約 1300 個(gè)文件。

          考慮到問題的規(guī)模,我認(rèn)為一些自動(dòng)化是值得的[11]。我不想寫一個(gè)完整的 Go 程序來編輯文件,所以我使用 Bash:

          $?grep?-L?Close?$(grep?-El?'New[^(]*Client'?**/*.go)?|?grep?-v?test?|?xargs?sed?-i?'/New[^(]*Client/,/}/s/}/}\ndefer?client.Close()/'

          它是完美的嗎?不。它對工作量有很大的影響嗎?是的!

          第一部分(直到test)與上面完全相同——獲取所有可能受影響的文件的列表(那些似乎創(chuàng)建了Client但從沒調(diào)用 Close 的文件)。

          然后,我將該文件列表傳遞給sed進(jìn)行實(shí)際編輯。xargs調(diào)用你給它的命令,每一行都以 stdin 作為參數(shù)傳遞給給定的命令。

          要理解該sed命令,查看 golang-samples repo 示例是什么樣子有助于理解(省略導(dǎo)入和客戶端初始化后的所有內(nèi)容):

          //?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)
          ????}
          ????//?...
          }

          在高層次上,我們初始化客戶端并檢查是否有錯(cuò)誤。每當(dāng)你檢查錯(cuò)誤時(shí),都會(huì)有一個(gè)右花括號 ( })。我使用這些信息來自動(dòng)化編輯。

          但是,該sed命令仍然很笨拙:

          sed?-i?'/New[^(]*Client/,/}/s/}/}\ndefer?client.Close()/'

          -i 表示直接編輯文件。這不是問題,因?yàn)榇a用 git 管理了。

          接下來,我使用s命令在檢查錯(cuò)誤defer client.Close()后假定的右花括號 ( })之后插入。

          但是,我不想替換每個(gè) },我只想要在調(diào)用NewClient第一個(gè)。要做到這一點(diǎn),你可以給一個(gè)地址范圍[12]sed搜索。

          地址范圍可以包括在應(yīng)用接下來的任何命令之前要匹配的開始和結(jié)束模式。在這種情況下,開始是/New[^(]*Client/,匹配NewClient類型調(diào)用,結(jié)束(由 a 分隔,)是/}/,匹配下一個(gè)大括號。這意味著我們的搜索和替換僅適用于調(diào)用NewClient和結(jié)束大括號之間!

          通過了解上面的錯(cuò)誤處理模式,if err != nil條件的右大括號正是我們想要插入Close調(diào)用的位置。


          一旦我自動(dòng)編輯了所有示例文件,我用goimports開始修復(fù)格式。然后,我檢查了每個(gè)編輯過的文件,以確保它做了正確的事情:

          • 在服務(wù)器應(yīng)用程序中,我們應(yīng)該關(guān)閉客戶端,還是應(yīng)該保留它以備將來的請求使用?
          • Client實(shí)際的名字client還是別的什么?
          • 是否有一個(gè)以上的Client調(diào)用了Close?

          完成后,只剩下180 個(gè)已編輯的文件[13]。


          最后一項(xiàng)工作是努力使其不再發(fā)生在用戶身上。我們想到了幾種方法:

          1. 更好的示例代碼;
          2. 更好的 GoDoc。我們更新了庫生成器,在生成庫時(shí)加上注釋,告知 client 需要調(diào)用 Close;
          3. 更好的庫。有沒有辦法可以自動(dòng)Close客戶端?Finalizers?知道何能做得更好嗎?歡迎在 https://github.com/googleapis/google-cloud-go/issues/4498 上交流;

          我希望你對 Go、內(nèi)存泄漏pprof、gRPC 和 Bash 有所了解。我很想聽聽你關(guān)于發(fā)現(xiàn)的內(nèi)存泄漏以及修復(fù)它們的方法的故事!如果你對我們?nèi)绾胃倪M(jìn)我們的[14]示例[15]有任何想法,請通過提交 issue 告訴我們。

          原文鏈接:https://dev.to/googlecloud/finding-and-fixing-memory-leaks-in-go-1k1h

          參考資料

          [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 服務(wù)器: 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]

          自動(dòng)部署: https://cloud.google.com/build/docs/deploying-builds/deploy-cloud-run

          [8]

          Cloud 的 Go 示例存儲(chǔ)庫中: 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 個(gè)已編輯的文件: https://github.com/GoogleCloudPlatform/golang-samples/pull/2080

          [14]

          庫: https://github.com/googleapis/google-cloud-go

          [15]

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




          往期推薦


          我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標(biāo)準(zhǔn)庫》等。


          堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio


          瀏覽 64
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲天堂在线观看免费 | 97办公室三级电影中文字幕 | 内射视频网站 | 国产成人做爱视频 | 亚洲天堂18 |