Cloud Profiler で本番環境のパフォーマンスを分析する

1. 概要

クライアント アプリやフロントエンド ウェブ デベロッパーは通常、コードのパフォーマンスを向上させるために Android Studio の CPU ProfilerChrome のプロファイリング ツールなどのツールを使用していますが、バックエンド サービスに携わる開発者は同等の手法を利用できず、採用もされていません。Cloud Profiler を使用すると、コードが Google Cloud Platform で実行されているかそれ以外の場所で実行されているかにかかわらず、開発者にこれらの同じ機能が提供されます。

95c034c70c9cac22.png

このツールは、本番環境のアプリケーションから CPU 使用率とメモリ割り当てに関する情報を収集します。収集した情報をアプリケーションのソースコードに関連付けることで、最もリソースを消費しているアプリケーション部分を特定できるほか、コードのパフォーマンス特性を把握できます。このツールは、オーバーヘッドが少ないため、本番環境での継続的な使用に適しています。

この Codelab では、Go プログラム用に Cloud Profiler を設定する方法を学習し、アプリケーションのパフォーマンスに関するどのような分析情報をツールから取得できますか。

学習内容

  • Cloud Profiler を使用してプロファイリング用に Go プログラムを構成する方法
  • Cloud Profiler を使用してパフォーマンス データを収集、表示、分析する方法。

必要なもの

  • Google Cloud Platform プロジェクト
  • ブラウザ(ChromeFirefox など)
  • Linux の標準的なテキスト エディタ(vim、emacs、nano など)を使い慣れていること

このチュートリアルをどのように使用しますか?

通読するのみ 通読し、演習を行う

Google Cloud Platform の使用経験をどのように評価されますか。

<ph type="x-smartling-placeholder"></ph> 初心者 中級 上達 をご覧ください。

2. 設定と要件

セルフペース型の環境設定

  1. Cloud コンソールにログインして、新しいプロジェクトを作成するか、既存のプロジェクトを再利用しますGmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

プロジェクト ID を忘れないようにしてください。プロジェクト ID はすべての Google Cloud プロジェクトを通じて一意の名前にする必要があります(上記の名前はすでに使用されているので使用できません)。以降、このコードラボでは PROJECT_ID と呼びます。

  1. 次に、Google Cloud リソースを使用するために、Cloud Console で課金を有効にする必要があります。

このコードラボを実行しても、費用はほとんどかからないはずです。このチュートリアル以外で請求が発生しないように、リソースのシャットダウン方法を説明する「クリーンアップ」セクションの手順に従うようにしてください。Google Cloud の新規ユーザーは、$300 USD 分の無料トライアル プログラムをご利用いただけます。

Google Cloud Shell

Google Cloud はノートパソコンからリモートで操作できますが、この Codelab では設定を簡単にするために、Cloud 上で動作するコマンドライン環境である Google Cloud Shell を使用します。

Cloud Shell をアクティブにする

  1. Cloud Console で、[Cloud Shell をアクティブにする] 4292cbf4971c9786.png をクリックします。

bce75f34b2c53987.png

Cloud Shell を初めて起動する場合は、その内容を説明する中間画面(スクロールしなければ見えない範囲)が表示されます。その場合は、[続行] をクリックします(今後表示されなくなります)。この中間画面は次のようになります。

70f315d7b402b476.png

Cloud Shell のプロビジョニングと接続に少し時間がかかる程度です。

fbe3a0674c982259.png

この仮想マシンには、必要な開発ツールがすべて含まれています。仮想マシンは Google Cloud で稼働し、永続的なホーム ディレクトリが 5 GB 用意されているため、ネットワークのパフォーマンスと認証が大幅に向上しています。このコードラボでの作業のほとんどは、ブラウザまたは Chromebook から実行できます。

Cloud Shell に接続すると、すでに認証は完了しており、プロジェクトに各自のプロジェクト ID が設定されていることがわかります。

  1. Cloud Shell で次のコマンドを実行して、認証されたことを確認します。
gcloud auth list

コマンド出力

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Cloud Shell で次のコマンドを実行して、gcloud コマンドがプロジェクトを認識していることを確認します。
gcloud config list project

コマンド出力

[core]
project = <PROJECT_ID>

上記のようになっていない場合は、次のコマンドで設定できます。

gcloud config set project <PROJECT_ID>

コマンド出力

Updated property [core/project].

3. Cloud Profiler に移動する

Cloud コンソールで、[Profiler] をクリックして Profiler UI に移動しますクリックします。

37ad0df7ddb2ad17.png

または、Cloud コンソールの検索バーを使用して Profiler UI に移動することもできます。「Cloud Profiler」と入力します。見つかったアイテムを選択します。どちらの場合も、Profiler UI に「No data to display」と表示されます。次のようなメッセージが表示されます。このプロジェクトは新しいため、プロファイリング データはまだ収集されていません。

d275a5f61ed31fb2.png

いよいよプロファイリングします。

4. ベンチマークのプロファイリングを行う

GitHub で入手可能なシンプルな Go 合成アプリケーションを使用します。開いたままの Cloud Shell ターミナルで(Profiler UI に「No data to display」メッセージが表示されている間に)次のコマンドを実行します。

$ go get -u github.com/GoogleCloudPlatform/golang-samples/profiler/...

次に、アプリケーション ディレクトリに切り替えます。

$ cd ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/profiler/hotapp

ディレクトリには "main.go" が含まれるプロファイリング エージェントが有効な合成アプリです。

main.go

...
import (
        ...
        "cloud.google.com/go/profiler"
)
...
func main() {
        err := profiler.Start(profiler.Config{
                Service:        "hotapp-service",
                DebugLogging:   true,
                MutexProfiling: true,
        })
        if err != nil {
                log.Fatalf("failed to start the profiler: %v", err)
        }
        ...
}

プロファイリング エージェントは、デフォルトで CPU、ヒープ、スレッドのプロファイルを収集します。このコードは、ミューテックス(「競合」とも呼ばれる)プロファイルの収集を可能にします。

プログラムを実行します。

$ go run main.go

プログラムが実行されると、プロファイリング エージェントは、構成された 5 つのタイプのプロファイルを定期的に収集します。収集は時間とともにランダム化されるため(タイプごとに 1 分あたり 1 つのプロファイルの平均レートで)、タイプごとに収集されるまでに最大 3 分かかる場合があります。プロファイルが作成されるとプログラムから通知されます。メッセージは、上記の構成の DebugLogging フラグによって有効になります。それ以外の場合、エージェントは通知なく実行されます。

$ go run main.go
2018/03/28 15:10:24 profiler has started
2018/03/28 15:10:57 successfully created profile THREADS
2018/03/28 15:10:57 start uploading profile
2018/03/28 15:11:19 successfully created profile CONTENTION
2018/03/28 15:11:30 start uploading profile
2018/03/28 15:11:40 successfully created profile CPU
2018/03/28 15:11:51 start uploading profile
2018/03/28 15:11:53 successfully created profile CONTENTION
2018/03/28 15:12:03 start uploading profile
2018/03/28 15:12:04 successfully created profile HEAP
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:04 successfully created profile THREADS
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:25 successfully created profile HEAP
2018/03/28 15:12:25 start uploading profile
2018/03/28 15:12:37 successfully created profile CPU
...

最初のプロファイルが収集されると、すぐに UI が更新されます。その後は自動更新されないため、新しいデータを表示するには、Profiler UI を手動で更新する必要があります。これを行うには、時間間隔選択ツールの [現在] ボタンを 2 回クリックします。

650051097b651b91.png

UI が更新されると、次のように表示されます。

47a763d4dc78b6e8.png

プロファイル タイプ セレクタには、使用可能な 5 つのプロファイル タイプが表示されます。

b5d7b4b5051687c9.png

各プロファイルの種類と重要な UI 機能を確認してから、いくつかのテストを実施しましょう。この段階では Cloud Shell ターミナルは必要ないので、Ctrl+C キーを押して「exit」と入力して終了します。

5. Profiler データを分析する

データを収集したところで、さらに詳しく見てみましょう。本番環境でのさまざまなパフォーマンスの問題に共通する動作をシミュレートする合成アプリ(ソースは GitHub で入手可能)を使用しています。

CPU 使用率の高いコード

CPU プロファイル タイプを選択します。UI が読み込まれると、フレームグラフに load 関数の 4 つのリーフブロックが表示されます。これにより、すべての CPU 消費量がまとめて表示されます。

fae661c9fe6c58df.png

この関数は、タイトなループを実行して多くの CPU サイクルを消費するように作成されています。

main.go

func load() {
        for i := 0; i < (1 << 20); i++ {
        }
}

この関数は、busyloop → {foo1, foo2} → {bar, baz} → load の 4 つの呼び出しパスを介して間接的に busyloop() から呼び出されます。関数ボックスの幅は、特定の呼び出しパスの相対的なコストを表します。この場合、4 つのパスの費用はほぼ同じです。実際のプログラムでは、パフォーマンスの観点から最も重要な呼び出しパスの最適化に重点を置く必要があります。フレームグラフでは、コストの高いパスが大きなボックスで視覚的に強調されているため、これらのパスを簡単に識別できます。

プロファイル データのフィルタを使用して、表示をさらに絞り込むことができます。たとえば、「スタックを表示」を「baz」を指定するフィルタ使用します。次のスクリーンショットのように、load() への 4 つの呼び出しパスのうち 2 つのみが表示されています。この 2 つのパスが、文字列「baz」の関数を通す唯一のパスです。含まれます。このようなフィルタリングは、大規模なプログラムの一部分に焦点を当てたい場合に便利です(たとえば、その一部しか所有していない場合)。

eb1d97491782b03f.png

メモリ使用量の多いコード

[ヒープ] に切り替えます選択します。以前のテストで作成したフィルタは必ず削除してください。alloc によって呼び出される allocImpl が、アプリのメモリの主な消費側として表示されているフレームグラフが表示されます。

f6311c8c841d04c4.png

フレームグラフの上にあるサマリー テーブルは、アプリで使用されているメモリの合計量が平均で約 57.4 MiB で、そのほとんどが allocImpl 関数によって割り当てられていることを示しています。この関数が実装されていることを考えると、これは驚くことではありません。

main.go

func allocImpl() {
        // Allocate 64 MiB in 64 KiB chunks
        for i := 0; i < 64*16; i++ {
                mem = append(mem, make([]byte, 64*1024))
        }
}

この関数は一度実行され、64 MiB を小さなチャンクに割り当てた後、これらのチャンクへのポインタをグローバル変数に格納して、ガベージ コレクションの対象から保護します。プロファイラで使用されるメモリ量は 64 MiB とは若干異なることに注意してください。Go ヒープ プロファイラは統計ツールであるため、測定値はオーバーヘッドは低くても、バイト単位では正確ではありません。このように 10% ほどの違いがあることに驚かないでください。

IO 負荷の高いコード

[スレッド]を選択した場合プロファイル タイプ セレクタを選択すると、幅のほとんどが wait 関数と waitImpl 関数で使用されるフレームグラフに切り替わります。

ebd57fdff01dede9.png

フレームグラフの上にある概要では、wait 関数からのコールスタックを増やす goroutine が 100 個あることがわかります。これらの待機を開始するコードは次のようになります。

main.go

func main() {
        ...
        // Simulate some waiting goroutines.
        for i := 0; i < 100; i++ {
                go wait()
        }

このプロファイル タイプは、プログラムが(I/O など)予期しない待機時間を費やしていないかどうかを確認するのに役立ちます。このようなコールスタックは通常、CPU 時間のほとんどを消費しないため、CPU Profiler によってサンプリングされることはありません。[グルーピングを隠す]をスレッド プロファイルを使用したフィルタ。たとえば、gopark, の呼び出しで終わるすべてのスタックを非表示にします。これらは多くの場合アイドル状態の goroutine であり、I/O で待機する Goroutine よりも関心が低いためです。

スレッド プロファイル タイプは、プログラムの別の部分が所有するミューテックスをスレッドが長時間待機しているプログラム内のポイントを特定するのにも役立ちますが、次のプロファイル タイプの方が有用です。

競合の多いコード

Contention プロファイル タイプでは、プログラムがロックされます。このプロファイル タイプは Go プログラムで使用できますが、「MutexProfiling: true」を指定して明示的に有効にする必要がありますエージェント構成コードで 指定する必要がありますこの収集は、「競合」指標の下で、特定のロックが goroutine A によってロック解除されているときに、別の goroutine B がロックのロック解除を待機した回数を記録することで機能します。また、ブロックされた goroutine がロックを待機していた時間も(「Delay」指標の下に)記録されます。この例では、競合スタックが 1 つあり、ロックの合計待機時間は 10.5 秒です。

83f00dca4a0f768e.png

このプロファイルを生成するコードは、ミューテックスに対して競合する 4 つの goroutine から構成されます。

main.go

func contention(d time.Duration) {
        contentionImpl(d)
}

func contentionImpl(d time.Duration) {
        for {
                mu.Lock()
                time.Sleep(d)
                mu.Unlock()
        }
}
...
func main() {
        ...
        for i := 0; i < 4; i++ {
                go contention(time.Duration(i) * 50 * time.Millisecond)
        }
}

6. まとめ

このラボでは、Cloud Profiler で使用する Go プログラムの構成方法を学びました。また、このツールを使用してパフォーマンス データを収集、表示、分析する方法についても学びました。これで、新しいスキルを Google Cloud Platform で実行する実際のサービスに応用できるようになりました。

7. 完了

ここでは、Cloud Profiler を構成して使用する方法を学びました。

詳細

ライセンス

この作業はクリエイティブ・コモンズの表示 2.0 汎用ライセンスにより使用許諾されています。