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

          『每周譯Go』定位并修復(fù) Go 中的內(nèi)存泄漏

          共 3313字,需瀏覽 7分鐘

           ·

          2021-11-04 16:58

          這篇文章回顧了我是如何發(fā)現(xiàn)內(nèi)存泄漏、如何修復(fù)它、如何修復(fù) Google 中的 Go 示例代碼中的類似問題,以及我們是如何改進(jìn)我們的基礎(chǔ)庫防止未來再次發(fā)生這種情況。

          Google 云的 Go 的客戶端基礎(chǔ)庫(https://github.com/googleapis/google-cloud-go)通常在底層使用 gRPC 來連接 Google 云的接口。當(dāng)你創(chuàng)建一個客戶端時,庫會初始化一個與該 接口的連接,然后讓這個連接保持打開狀態(tài),直到你在該客戶端上調(diào)用 Close 操作。

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

          客戶端并發(fā)地使用是安全的,所以應(yīng)該在使用完之前保留同一個客戶端。但是,如果在應(yīng)該關(guān)閉客戶端的時候而沒有關(guān)閉,會發(fā)生什么呢?

          你會得到一個內(nèi)存泄漏:底層連接從未被釋放過。

          Google 有一堆的 GitHub 自動化機(jī)器人,幫助管理數(shù)以百計(jì)的 GitHub 倉庫。我們的一些機(jī)器人通過云上運(yùn)行的(https://cloud.google.com/run/docs/quickstarts/build-and-deploy/go)上的Go 服務(wù)(https://github.com/googleapis/repo-automation-bots/tree/main/serverless-scheduler-proxy)代理它們的請求。我們的內(nèi)存使用情況看起來就是典型的鋸齒狀內(nèi)存泄漏情況。

          我通過在程序中添加 pprof.Index 開始調(diào)試:

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

          pprof 提供運(yùn)行時的分析數(shù)據(jù),比如內(nèi)存使用量。訪問 Go 官方博客中的 分析 Go 程序(https://blog.golang.org/pprof)獲取更多信息.

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

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

          然后,我向這個服務(wù)發(fā)送了一些構(gòu)造的請求:

          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ù)載和路徑是針對我們服務(wù)的,與本文無關(guān)。為了獲取一個關(guān)于內(nèi)存使用情況的基線數(shù)據(jù),我收集了一些初始的 pprof 數(shù)據(jù)。

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

          通過檢查輸出的結(jié)果,可以看到一些內(nèi)存的使用情況,但并沒有發(fā)現(xiàn)徒增的問題(這很好!因?yàn)槲覀儎倓倖臃?wù)!)。

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

          接下來繼續(xù)向服務(wù)發(fā)送一批請求,看看我們是否會出現(xiàn)(1)重現(xiàn)前面的內(nèi)存泄漏,(2)確定泄漏是什么。

          發(fā)送 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 數(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)存! 這就是內(nèi)存泄漏與什么有關(guān)的第一個跡象:gRPC。結(jié)合源碼,我們唯一使用 gRPC 的地方是Google 云秘鑰管理部分(https://cloud.google.com/secret-manager/docs/quickstart)。

          client, err := secretmanager.NewClient(ctx) if err != nil { return nil, fmt.Errorf("failed to create secretmanager client: %v", err) } 我們從未調(diào)用過client.Close(),并且在每個請求中都創(chuàng)建了一個Client! 所以,我添加了一個Close調(diào)用,問題就解決了。

          defer?client.Close()

          我提交了這個修復(fù), 它 自動部署完成后(https://cloud.google.com/build/docs/deploying-builds/deploy-cloud-run), 毛刺顯現(xiàn)立即消失了!

          哇嗚! ??????

          大約在同一時間,一個用戶在我們的云上的 Go 實(shí)例代碼庫(https://github.com/GoogleCloudPlatform/golang-samples)上提出了一個問題,其中包含了cloud.google.com上文檔的大部分 Go 示例程序。該用戶注意到我們在其中一個程序中忘記了 client.Close() 關(guān)閉客戶端!

          我看到同樣的事情出現(xiàn)過幾次,所以我決定調(diào)查整個倉庫。

          我從粗略估計(jì)有多少受影響的文件開始。使用 grep 命令,我們可以得到一個包含 NewClient 風(fēng)格調(diào)用的所有文件的列表,然后把這個列表傳遞給另一個 grep 調(diào)用,只列出不包含Close的文件,同時忽略測試文件。

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

          譯者注:列出包含New[^(]*Client,但不包含Close的所有 go 文件

          哇嗚! 總共有 207 個文件,而整個 GoogleCloudPlatform/golang-samples(https://github.com/GoogleCloudPlatform/golang-samples) 倉庫中有大約 1300 個 .go 文件.

          鑒于問題的規(guī)模,我認(rèn)為一些簡單的自動化會很值得(https://xkcd.com/1205/)。我不想寫一個完整的 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 倉庫中的示例程序通常是什么樣子(省略導(dǎo)入和客戶端初始化后)會很有幫助。

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

          在高層次上,我們初始化客戶端并檢查是否有錯誤。每當(dāng)你檢查錯誤時,都有一個閉合的大括號(})。我使用這些信息來確定如何自動編輯。

          不過,sed 命令仍然是個麻煩。

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

          -i 參數(shù)表明是原地編輯并替換文件。對此我是沒問題,因?yàn)槿绻阍伊耍?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">git 可以救我。

          接下來,我使用 s 命令在檢查錯誤時的關(guān)閉大括號(})之后插入 defer client.Close()

          但是,我不想替換每一個},我只想替換調(diào)用 NewClient* 后的第一個。要做到這一點(diǎn),你可以給一個讓 sed 去搜索地址范圍(https://www.gnu.org/software/sed/manual/html_node/Addresses.html)。

          地址范圍可以包括開始和結(jié)束模式,以便在應(yīng)用接下來的任何命令之前進(jìn)行匹配。在這個例子中,開始是/New[^(]*Client/,匹配NewClient 類型的調(diào)用,結(jié)束(用,分隔)是/}/,匹配下一個大括號。這意味著我們的搜索和替換將只適用于對 NewClient 的調(diào)用和結(jié)尾的大括號之間。

          通過了解上面的錯誤處理模式,if err != nil 條件的結(jié)束括號正是我們要插入 Close 調(diào)用的地方。

          一旦自動編輯了所有的文件,運(yùn)行 goimports 來修復(fù)格式化。然后,檢查了每個編輯過的文件,確保它做了正確的事情。

          • 在服務(wù)器應(yīng)用程序中,我們是應(yīng)該真正關(guān)閉客戶端,還是應(yīng)該為未來的請求保留它?
          • 客戶端的名字實(shí)際上是 client,還是別的什么?
          • 是否有更多的客戶端需要 Close

          一旦完成這些,我留下了180 個編輯的文件(https://github.com/GoogleCloudPlatform/golang-samples/pull/2080)

          最后的任務(wù)是努力使用戶不再發(fā)生這種情況。我們想到了幾種方法:

          1. 更好的示例程序。
          2. 更好的 GoDoc。我們更新了我們的庫生成器,在生成的庫中加入了一個注釋,說當(dāng)你用完后要 Close 客戶端。參見https://github.com/googleapis/google-cloud-go/issues/3031。
          3. 更好的基礎(chǔ)庫。有什么辦法可以讓我們自動 Close 客戶?Finalizer 方法?有什么想法我們可以做得更好嗎?請?jiān)趆ttps://github.com/googleapis/google-cloud-go/issues/4498 上告訴我們。

          希望你能學(xué)到一些關(guān)于 Go、內(nèi)存泄漏、pprofgRPCBash 的知識。我很想聽聽你關(guān)于你所發(fā)現(xiàn)的內(nèi)存泄露的故事,以及你是如何解決這些問題的! 如果你對我們的代碼庫(https://github.com/googleapis/google-cloud-go)或示例程序(https://github.com/GoogleCloudPlatform/golang-samples)有什么想法,歡迎提交問題讓我們知道。

          # 原文信息 #

          • 原文地址:https://dev.to/googlecloud/finding-and-fixing-memory-leaks-in-go-1k1h

          • 原文作者:Tyler Bui-Palsulich

          • 本文永久鏈接:https://github.com/gocn/translator/blob/master/2021/w42_Finding_and_fixing_memory_leaks_in_Go.md

          • 譯者:Fivezh



          想要了解關(guān)于 Go 的更多資訊,還可以通過掃描的方式,進(jìn)群一起探討哦~




          瀏覽 92
          點(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>
                  亚洲 日韩 欧美 国产 | 天天澡日日久综 | 欧美夫妻天天 | 国产中文成人 | 欧美a一级片免费 |