メモリリークの発見と固定


このポストは、私がどのようにメモリリークを見つけたか、どのように私はそれを修正したか、Googleのサンプル・ゴー・コードの類似した問題をどのように修正したか、そして、将来これを防ぐためにどのようにライブラリを改善しているかをレビューします.
The Google Cloud Client Libraries for Go 一般的にGoogleのクラウドAPIと接続するためにフードの下にGRPCを使用します.APIクライアントを作成すると、ライブラリはAPIへの接続を初期化し、呼び出しを開始するまで接続を開放しますCloseClient .
client, err := api.NewClient()
// Check err.
defer client.Close()
クライアントは同時に使用するのが安全ですClient 閉じるこの動画はお気に入りから削除されています.しかし、あなたがそうしないならば、何が起こりますかClose あなたがそうするべきクライアント?
あなたはメモリリークを取得します.根底にある接続は決してクリーンアップされません.
Googleはgithub reposの何百もの管理に役立つgithubオートメーションボットの束を持っています.我々のボットのいくつかは、彼らの要求をAGo server 走るCloud Run . 我々の記憶用法は古典的なのこぎりのメモリリークのように見えました:

追加してデバッグを開始pprof.Index サーバへのハンドラ
mux.HandleFunc("/debug/pprof/", pprof.Index)
pprof メモリ使用のような実行時プロファイリングデータを提供します.参照Profiling Go Programs 詳細については、ブログを移動します.
それから、ローカルでサーバーを構築して起動しました.
$ 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を使用していた唯一の場所はGoogle Cloud Secret Manager :
client, err := secretmanager.NewClient(ctx) 
if err != nil { 
    return nil, fmt.Errorf("failed to create secretmanager client: %v", err) 
}
私たちは決してclient.Close() を作成し、Client すべてのリクエストで!だから、私はClose 呼び出しと問題は消えました:
defer client.Close()
私は修正を提出したautomatically deployed , そして、鋸歯はすぐに去りました!

ウーホー!🎉🎉🎉
同じ頃、ユーザーは私たちの問題を提起したGo sample repo for Cloud , これはdocs用のgoサンプルのほとんどを含んでいますcloud.google.com . ユーザーは私たちが忘れたことに気づいたClose the Client 我々のサンプルの1つで!
私は同じことがいくつかの他の時代にポップを見ていたので、私は全体のレポを調査することを決めた.
私はどのように多くの影響を受けたファイルがあったの概算を始めた.使用grep , を含むすべてのファイルのリストを得ることができますNewClient スタイルの呼び出し、次に、そのリストを別の呼び出しに渡すgrep 含まないファイルをリストするにはClose , テストファイルの無視
$ grep -L Close $(grep -El 'New[^(]*Client' **/*.go) | grep -v test
おっ!コンテキスト用に207ファイルがありました.go 中のファイルGoogleCloudPlatform/golang-samples レポ.
問題の規模を考えれば、オートメーションがいくつかあると思いましたworth it ラフスタートを得る.私はファイルを編集するために完全なオン・ゴー・プログラムを書きたくなかったので、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)
    }
    // ...
}
高いレベルで、クライアントを初期化し、エラーがあるかどうかを確認します.あなたがエラーをチェックするときはいつでも、閉じているカーリーブレースがあります} ). 私はその情報を使って編集を自動化した.
The sed コマンドはまだダメです.
sed -i '/New[^(]*Client/,/}/s/}/}\ndefer client.Close()/'
The -i 代わりにファイルを編集します.私はこれで大丈夫git 私が混乱するならば、私を救うことができます.
次に、私はs 挿入コマンドdefer client.Close() 推定クローズカーリーブレース直後} ) エラーをチェックすることから.
しかし、私はすべてを交換する必要はありません} , 私は、呼び出しの後、最初のものが欲しいだけですNewClient . そのためには、address range for sed 探す.
アドレス範囲には、次のコマンドが適用される前にマッチする開始パターンと終了パターンが含まれます.この場合、スタートは/New[^(]*Client/ , マッチングNewClient 呼び出しをタイプし、最後に, ) is /}/ , 次のカーリーブレースをマッチング.つまり、私たちの検索と置換はNewClient 閉じるこの動画はお気に入りから削除されています.
上記のエラー処理パターンを知ることからif err != nil 条件は正確に我々が挿入する場所ですClose コール.
一度自動的にすべてのサンプルを編集した、私は走ったgoimports 書式を修正するにはそれから、それぞれの編集ファイルを通して正しいことを確認しました.
  • サーバアプリケーションでは、クライアントを実際に閉じる必要があります.
  • Client 実際にclient またはそれは何か他のですか?
  • 複数以上あるかClient to Close ?
  • 一旦それが済んだら、私は去った180 files edited .
    ビジネスの最後の順序は、これがユーザーにもう発生しないようにしようとしています.いくつかの方法があります.
  • より良いサンプル.上記参照.
  • より良いGodoc.私たちはライブラリジェネレータを更新するにはClose the Client あなたがそれを終えたとき.参照https://github.com/googleapis/google-cloud-go/issues/3031 .
  • より良いライブラリ.自動的にできる方法はありますかClose クライアント?フィナライザ?どのように我々はこれを行うことができますのアイデアがありますか?お知らせくださいhttps://github.com/googleapis/google-cloud-go/issues/4498 .
  • 私はあなたがGO、メモリリークについて少し学んだことを願っています.pprof , grpcとbash私はあなたが発見したメモリリークについてのあなたの話を聞くのが大好きだし、それを修正するために取ったもの!あなたが我々が我々を改善することができる方法についてのアイデアがあるならばlibraries or samples , 私たちは問題をファイリングしてお知らせください.