Go でアプリのパフォーマンスを向上させるために計測する(パート 2: プロファイラ)

1. はじめに

e0509e8a07ad5537.png

最終更新日: 2022 年 7 月 14 日

アプリケーションのオブザーバビリティ

オブザーバビリティと継続的プロファイラ

オブザーバビリティとは、システムの属性を表す用語です。オブザーバビリティを備えたシステムでは、チームが積極的にシステムをデバッグできます。そこで、オブザーバビリティの 3 本柱について説明します。ログ、指標、トレースは、システムがオブザーバビリティを取得するための基本的な計測手段です。

また、オブザーバビリティの 3 つの柱に加えて、継続的プロファイリングはオブザーバビリティのもう 1 つの重要なコンポーネントであり、業界でユーザーベースを拡大しています。Cloud Profiler は生成元の一つであり、アプリケーションのコールスタックのパフォーマンス指標をドリルダウンするための簡単なインターフェースを提供します。

この Codelab はシリーズのパート 2 で、継続的なプロファイラ エージェントを計測する方法について説明します。パート 1 では、OpenTelemetry と Cloud Trace を使用した分散トレースについて解説し、パート 1 ではマイクロサービスのボトルネックの特定について詳しく学習します。

作成するアプリの概要

この Codelab では、Google Kubernetes Engine クラスタで実行される「Shakespeare アプリケーション」(Shakesapp)のサーバー サービスに継続プロファイラ エージェントを実装します。Shakesapp のアーキテクチャは次のとおりです。

44e243182ced442f.png

  • Loadgen が HTTP でクライアントにクエリ文字列を送信する
  • クライアントが gRPC でロードジェネレータからサーバーにクエリを渡す
  • サーバーがクライアントからクエリを受け取り、Google Cloud Storage からすべての Shakespare 作品をテキスト形式で取得して、クエリを含む行を検索し、クライアントに一致する行の番号を返す

パート 1 では、ボトルネックが発生しているのはサーバー サービスのどこかであることがわかりましたが、正確な原因は特定できませんでした。

学習内容

  • Profiler エージェントを埋め込む方法
  • Cloud Profiler でボトルネックを調査する方法

この Codelab では、アプリケーションで継続的なプロファイラ エージェントを計測可能にする方法について説明します。

必要なもの

  • Go の基本的な知識
  • Kubernetes の基本的な知識

2. 設定と要件

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

Google アカウント(Gmail または Google Apps)をお持ちでない場合は、1 つ作成する必要があります。Google Cloud Platform のコンソール(console.cloud.google.com)にログインし、新しいプロジェクトを作成します。

すでにプロジェクトが存在する場合は、コンソールの左上にあるプロジェクト選択プルダウン メニューをクリックします。

7a32e5469db69e9.png

[新しいプロジェクト] をクリックします。] ボタンをクリックし、新しいプロジェクトを作成します。

7136b3ee36ebaf89.png

まだプロジェクトが存在しない場合は、次のような最初のプロジェクトを作成するためのダイアログが表示されます。

870a3cbd6541ee86.png

続いて表示されるプロジェクト作成ダイアログでは、新しいプロジェクトの詳細を入力できます。

affdc444517ba805.png

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

次に、Google Cloud リソースを使用して Cloud Trace API を有効にするために、Cloud Console で課金を有効にします(まだ有効にしていない場合)。

15d0ef27a8fbab27.png

この Codelab の操作をすべて行って、費用が生じたとしても、少額です。Google Cloud Trace、Google Kubernetes Engine、Google Artifact Registry の料金については、公式ドキュメントをご覧ください。

Google Cloud Platform の新規ユーザーの皆さんには、$300 の無料トライアルをご利用いただけます。その場合は、この Codelab を完全に無料でご利用いただけます。

Google Cloud Shell のセットアップ

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

この Debian ベースの仮想マシンには、必要な開発ツールがすべて揃っています。永続的なホーム ディレクトリが 5 GB 用意されており、Google Cloud で稼働するため、ネットワークのパフォーマンスと認証が大幅に向上しています。つまり、この Codelab に必要なのはブラウザだけです(はい、Chromebook で動作します)。

Cloud コンソールから Cloud Shell を有効にするには、「Cloud Shell をアクティブにする」アイコン gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A をクリックします(環境のプロビジョニングと接続には少し時間がかかります)。

JjEuRXGg0AYYIY6QZ8d-66gx_Mtc-_jDE9ijmbXLJSAXFvJt-qUpNtsBsYjNpv2W6BQSrDc1D-ARINNQ-1EkwUhz-iUK-FUCZhJ-NtjvIEx9pIkE-246DomWuCfiGHK78DgoeWkHRw

Screen Shot 2017-06-14 at 10.13.43 PM.png

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

gcloud auth list

コマンド出力

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

コマンド出力

[core]
project = <PROJECT_ID>

なんらかの理由でプロジェクトが設定されていない場合は、次のコマンドを実行します。

gcloud config set project <PROJECT_ID>

PROJECT_ID が見つからない場合は、設定手順で使用した ID を確認するか、Cloud コンソール ダッシュボードで調べます。

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

Cloud Shell では、デフォルトで環境変数もいくつか設定されます。これらの変数は、以降のコマンドを実行する際に有用なものです。

echo $GOOGLE_CLOUD_PROJECT

コマンド出力

<PROJECT_ID>

最後に、デフォルトのゾーンとプロジェクト構成を設定します。

gcloud config set compute/zone us-central1-f

さまざまなゾーンを選択できます。詳しくは、リージョンとゾーン

Go 言語の設定

この Codelab では、すべてのソースコードに Go を使用します。Cloud Shell で次のコマンドを実行して、Go のバージョンが 1.17 以降であることを確認します。

go version

コマンド出力

go version go1.18.3 linux/amd64

Google Kubernetes クラスタを設定する

この Codelab では、Google Kubernetes Engine(GKE)でマイクロサービスのクラスタを実行します。この Codelab のプロセスは次のとおりです。

  1. ベースライン プロジェクトを Cloud Shell にダウンロードする
  2. マイクロサービスをコンテナに構築する
  3. Google Artifact Registry(GAR)にコンテナをアップロードする
  4. コンテナを GKE にデプロイする
  5. トレース計測用にサービスのソースコードを変更する
  6. ステップ 2 に進みます

Kubernetes Engine を有効にする

まず、Shakesapp が GKE で実行される Kubernetes クラスタを設定するため、GKE を有効にする必要があります。[Kubernetes Engine] メニューに移動します。[有効にする]ボタンを押します

548cfd95bc6d344d.png

これで、Kubernetes クラスタを作成する準備が整いました。

Kubernetes クラスタを作成する

Cloud Shell で次のコマンドを実行して、Kubernetes クラスタを作成します。Artifact Registry リポジトリの作成に使用するリージョンの下にゾーン値が存在することを確認してください。リポジトリ リージョンがゾーンをカバーしていない場合は、ゾーン値 us-central1-f を変更します。

gcloud container clusters create otel-trace-codelab2 \
--zone us-central1-f \
--release-channel rapid \
--preemptible \
--enable-autoscaling \
--max-nodes 8 \
--no-enable-ip-alias \
--scopes cloud-platform

コマンド出力

Note: Your Pod address range (`--cluster-ipv4-cidr`) can accommodate at most 1008 node(s).
Creating cluster otel-trace-codelab2 in us-central1-f... Cluster is being health-checked (master is healthy)...done.     
Created [https://container.googleapis.com/v1/projects/development-215403/zones/us-central1-f/clusters/otel-trace-codelab2].
To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/us-central1-f/otel-trace-codelab2?project=development-215403
kubeconfig entry generated for otel-trace-codelab2.
NAME: otel-trace-codelab2
LOCATION: us-central1-f
MASTER_VERSION: 1.23.6-gke.1501
MASTER_IP: 104.154.76.89
MACHINE_TYPE: e2-medium
NODE_VERSION: 1.23.6-gke.1501
NUM_NODES: 3
STATUS: RUNNING

Artifact Registry と skaffold の設定

これで、Kubernetes クラスタをデプロイする準備が整いました。次に、push コンテナとデプロイ コンテナ用の Container Registry を準備します。これらの手順では、Artifact Registry(GAR)とそれを使用するように skaffold を設定する必要があります。

Artifact Registry の設定

[Artifact Registry] のメニューに移動し、[ENABLE] ボタンを押します。

45e384b87f7cf0db.png

しばらくすると、GAR のリポジトリ ブラウザが表示されます。[リポジトリを作成] をクリックします。リポジトリの名前を入力します。

d6a70f4cb4ebcbe3.png

この Codelab では、新しいリポジトリに trace-codelab という名前を付けます。アーティファクトの形式は「Docker」で、ロケーション タイプは「リージョン」です。Google Compute Engine のデフォルト ゾーンに設定したリージョンに近いリージョンを選択します。たとえば、この例では「us-central1-f」を選択しています。ここでは「us-central1 (アイオワ)」を選択します。次に [作成]をクリックして] ボタンを離します。

9c2d1ce65258ef70.png

「trace-codelab」というアクセスできます。

7a3c1f47346bea15.png

後でここに戻ってレジストリパスを確認します。

Skaffold の設定

Skaffold は、Kubernetes 上で動作するマイクロサービスの構築に便利なツールです。小規模なコマンドセットを使用して、アプリケーションのコンテナのビルド、push、デプロイのワークフローを処理します。Skaffold はデフォルトで Docker Registry をコンテナ レジストリとして使用するため、コンテナを push するときに GAR を認識するように skaffold を構成する必要があります。

Cloud Shell をもう一度開き、skaffold がインストールされていることを確認します。(Cloud Shell はデフォルトで skaffold を環境にインストールします)。次のコマンドを実行して、skaffold のバージョンを確認します。

skaffold version

コマンド出力

v1.38.0

これで、skaffold が使用するデフォルトのリポジトリを登録できるようになりました。レジストリパスを取得するには、Artifact Registry ダッシュボードに移動し、前のステップで設定したリポジトリの名前をクリックします。

7a3c1f47346bea15.png

ページの上部にパンくずリストが表示されます。e157b1359c3edc06.png アイコンをクリックして、レジストリパスをクリップボードにコピーします。

e0f2ae2144880b8b.png

コピーボタンをクリックすると、ブラウザの下部に次のようなダイアログが表示されます。

&quot;us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab&quot;コピーされました

Cloud Shell に戻ります。ダッシュボードからコピーした値を指定して、skaffold config set default-repo コマンドを実行します。

skaffold config set default-repo us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab

コマンド出力

set value default-repo to us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab for context gke_stackdriver-sandbox-3438851889_us-central1-b_stackdriver-sandbox

また、レジストリを Docker 構成に構成する必要があります。次のコマンドを実行します。

gcloud auth configure-docker us-central1-docker.pkg.dev --quiet

コマンド出力

{
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud",
    "marketplace.gcr.io": "gcloud",
    "us-central1-docker.pkg.dev": "gcloud"
  }
}
Adding credentials for: us-central1-docker.pkg.dev

これで、GKE に Kubernetes コンテナを設定する次のステップに進む準備ができました。

概要

このステップでは、Codelab 環境を設定します。

  • Cloud Shell を設定する
  • コンテナ レジストリ用の Artifact Registry リポジトリを作成した
  • コンテナ レジストリを使用するように Skaffold を設定する
  • Codelab マイクロサービスが実行される Kubernetes クラスタを作成している

次のステップ

次のステップでは、サーバー サービスに継続プロファイラ エージェントを計測します。

3. マイクロサービスのビルド、push、デプロイ

Codelab の資料をダウンロードする

前のステップで、この Codelab のすべての前提条件を設定しました。これで、その上でマイクロサービス全体を実行する準備が整いました。Codelab の資料は GitHub でホストされているため、次の git コマンドを使用して Cloud Shell 環境にダウンロードします。

cd ~
git clone https://github.com/ymotongpoo/opentelemetry-trace-codelab-go.git
cd opentelemetry-trace-codelab-go

プロジェクトのディレクトリ構造は次のとおりです。

.
├── README.md
├── step0
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step1
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step2
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step3
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step4
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step5
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
└── step6
    ├── manifests
    ├── proto
    ├── skaffold.yaml
    └── src
  • manifests: Kubernetes マニフェスト ファイル
  • proto: クライアントとサーバー間の通信の proto 定義
  • src: 各サービスのソースコードのディレクトリ
  • skaffold.yaml: skaffold の構成ファイル

この Codelab では、step4 フォルダにあるソースコードを更新します。また、step[1-6] フォルダのソースコードで最初から変更を確認することもできます。(パート 1 はステップ 0 からステップ 4、パート 2 はステップ 5 と 6 に対応しています)

skaffold コマンドを実行

これで、作成した Kubernetes クラスタにコンテンツ全体をビルド、push、デプロイする準備が整いました。これは複数のステップを含んでいるように見えますが、実際には skaffold がすべての処理を行います。次のコマンドを使用して試してみましょう。

cd step4
skaffold dev

コマンドを実行するとすぐに、docker build のログ出力が表示され、レジストリに正常に push されたことを確認できます。

コマンド出力

...
---> Running in c39b3ea8692b
 ---> 90932a583ab6
Successfully built 90932a583ab6
Successfully tagged us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/serverservice:step1
The push refers to repository [us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/serverservice]
cc8f5a05df4a: Preparing
5bf719419ee2: Preparing
2901929ad341: Preparing
88d9943798ba: Preparing
b0fdf826a39a: Preparing
3c9c1e0b1647: Preparing
f3427ce9393d: Preparing
14a1ca976738: Preparing
f3427ce9393d: Waiting
14a1ca976738: Waiting
3c9c1e0b1647: Waiting
b0fdf826a39a: Layer already exists
88d9943798ba: Layer already exists
f3427ce9393d: Layer already exists
3c9c1e0b1647: Layer already exists
14a1ca976738: Layer already exists
2901929ad341: Pushed
5bf719419ee2: Pushed
cc8f5a05df4a: Pushed
step1: digest: sha256:8acdbe3a453001f120fb22c11c4f6d64c2451347732f4f271d746c2e4d193bbe size: 2001

すべてのサービス コンテナが push されると、Kubernetes の Deployment が自動的に開始されます。

コマンド出力

sha256:b71fce0a96cea08075dc20758ae561cf78c83ff656b04d211ffa00cedb77edf8 size: 1997
Tags used in deployment:
 - serverservice -> us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/serverservice:step4@sha256:8acdbe3a453001f120fb22c11c4f6d64c2451347732f4f271d746c2e4d193bbe
 - clientservice -> us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/clientservice:step4@sha256:b71fce0a96cea08075dc20758ae561cf78c83ff656b04d211ffa00cedb77edf8
 - loadgen -> us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/loadgen:step4@sha256:eea2e5bc8463ecf886f958a86906cab896e9e2e380a0eb143deaeaca40f7888a
Starting deploy...
 - deployment.apps/clientservice created
 - service/clientservice created
 - deployment.apps/loadgen created
 - deployment.apps/serverservice created
 - service/serverservice created

デプロイ後、各コンテナの stdout に実際のアプリケーション ログが出力されます。

コマンド出力

[client] 2022/07/14 06:33:15 {"match_count":3040}
[loadgen] 2022/07/14 06:33:15 query 'love': matched 3040
[client] 2022/07/14 06:33:15 {"match_count":3040}
[loadgen] 2022/07/14 06:33:15 query 'love': matched 3040
[client] 2022/07/14 06:33:16 {"match_count":3040}
[loadgen] 2022/07/14 06:33:16 query 'love': matched 3040
[client] 2022/07/14 06:33:19 {"match_count":463}
[loadgen] 2022/07/14 06:33:19 query 'tear': matched 463
[loadgen] 2022/07/14 06:33:20 query 'world': matched 728
[client] 2022/07/14 06:33:20 {"match_count":728}
[client] 2022/07/14 06:33:22 {"match_count":463}
[loadgen] 2022/07/14 06:33:22 query 'tear': matched 463

この時点では、サーバーからのメッセージを確認します。これで、サービスの分散トレースのために OpenTelemetry を使用してアプリケーションを計測する準備が整いました。

サービスの計測を開始する前に、Ctrl+C を使用してクラスタをシャットダウンしてください。

コマンド出力

...
[client] 2022/07/14 06:34:57 {"match_count":1}
[loadgen] 2022/07/14 06:34:57 query 'what's past is prologue': matched 1
^CCleaning up...
 - W0714 06:34:58.464305   28078 gcp.go:120] WARNING: the gcp auth plugin is deprecated in v1.22+, unavailable in v1.25+; use gcloud instead.
 - To learn more, consult https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke
 - deployment.apps "clientservice" deleted
 - service "clientservice" deleted
 - deployment.apps "loadgen" deleted
 - deployment.apps "serverservice" deleted
 - service "serverservice" deleted

概要

このステップでは、環境に Codelab の資料を準備し、skaffold が想定どおりに実行されることを確認しました。

次のステップ

次のステップでは、loadgen サービスのソースコードを変更して、トレース情報を計測できるようにします。

4. Cloud Profiler エージェントのインストルメンテーション

継続的プロファイリングのコンセプト

継続的プロファイリングのコンセプトを説明する前に、まずプロファイリングのコンセプトを理解する必要があります。プロファイリングは、アプリケーションを動的に分析する方法(動的プログラム分析)の一つで、通常はアプリケーション開発時の負荷テストなどのプロセスで実行されます。これは、特定の期間におけるシステム指標(CPU 使用率やメモリ使用量など)を測定するためのシングル ショット アクティビティです。プロファイル データを収集した後、デベロッパーはコードからデータを分析します。

継続的プロファイリングは、通常のプロファイリングを拡張したアプローチです。長時間実行されるアプリケーションに対して短時間のプロファイルを定期的に実行し、一連のプロファイル データを収集します。次に、バージョン番号、デプロイゾーン、測定時間など、アプリケーションの特定の属性に基づいて、統計分析が自動的に生成されます。このコンセプトの詳細については、こちらのドキュメントをご覧ください。

ターゲットは実行中のアプリケーションであるため、プロファイル データを定期的に収集し、統計データの後処理を行うバックエンドに送信する方法があります。これは Cloud Profiler エージェントで、後でサーバー サービスに埋め込みます。

Cloud Profiler エージェントを埋め込む

Cloud Shell の右上にある 776a11bfb2122549.png ボタンを押して、Cloud Shell エディタを開きます。左側のペインにあるエクスプローラから step4/src/server/main.go を開き、main 関数を見つけます。

step4/src/server/main.go

func main() {
        ...
        // step2. setup OpenTelemetry
        tp, err := initTracer()
        if err != nil {
                log.Fatalf("failed to initialize TracerProvider: %v", err)
        }
        defer func() {
                if err := tp.Shutdown(context.Background()); err != nil {
                        log.Fatalf("error shutting down TracerProvider: %v", err)
                }
        }()
        // step2. end setup

        svc := NewServerService()
        // step2: add interceptor
        interceptorOpt := otelgrpc.WithTracerProvider(otel.GetTracerProvider())
        srv := grpc.NewServer(
                grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor(interceptorOpt)),
                grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor(interceptorOpt)),
        )
        // step2: end adding interceptor
        shakesapp.RegisterShakespeareServiceServer(srv, svc)
        healthpb.RegisterHealthServer(srv, svc)
        if err := srv.Serve(lis); err != nil {
                log.Fatalf("error serving server: %v", err)
        }
}

main 関数には、Codelab パート 1 で行った OpenTelemetry と gRPC のセットアップ コードがあります。ここでは、Cloud Profiler エージェントの計測を追加します。initTracer() で行ったように、読みやすくするために initProfiler() という関数を作成します。

step4/src/server/main.go

import (
        ...
        "cloud.google.com/go/profiler" // step5. add profiler package
        "cloud.google.com/go/storage"
        ...
)

// step5: add Profiler initializer
func initProfiler() {
        cfg := profiler.Config{
                Service:              "server",
                ServiceVersion:       "1.0.0",
                NoHeapProfiling:      true,
                NoAllocProfiling:     true,
                NoGoroutineProfiling: true,
                NoCPUProfiling:       false,
        }
        if err := profiler.Start(cfg); err != nil {
                log.Fatalf("failed to launch profiler agent: %v", err)
        }
}

profiler.Config{} オブジェクトで指定されたオプションについて詳しく見てみましょう。

  • サービス: Profiler のダッシュボードで選択して切り替えることができるサービス名
  • ServiceVersion: サービス バージョン名。この値に基づいてプロファイル データセットを比較できます。
  • NoHeapProfiling: メモリ使用量のプロファイリングを無効にします。
  • NoAllocProfiling: メモリ割り当てプロファイリングを無効にします。
  • NoGoroutineProfiling: goroutine プロファイリングを無効にします。
  • NoCPUProfiling: CPU プロファイリングを無効にします。

この Codelab では、CPU プロファイリングのみを有効にします。

残す作業は、main 関数でこの関数を呼び出すだけです。インポート ブロックで Cloud Profiler パッケージをインポートしてください。

step4/src/server/main.go

func main() {
        ...
        defer func() {
                if err := tp.Shutdown(context.Background()); err != nil {
                        log.Fatalf("error shutting down TracerProvider: %v", err)
                }
        }()
        // step2. end setup

        // step5. start profiler
        go initProfiler()
        // step5. end

        svc := NewServerService()
        // step2: add interceptor
        ...
}

go キーワードを使用して initProfiler() 関数を呼び出していることに注意してください。profiler.Start() がブロックされているため、別の goroutine で実行する必要があります。ビルドの準備が整いましたデプロイ前に必ず go mod tidy を実行してください。

go mod tidy

新しいサーバー サービスを使用してクラスタをデプロイします。

skaffold dev

通常、Cloud Profiler にフレームグラフが表示されるまでに数分かかります。「profiler」と入力します。をクリックし、Profiler のアイコンをクリックします。

3d8ca8a64b267a40.png

次のようなフレームグラフが表示されます。

7f80797dddc0128d.png

概要

このステップでは、Cloud Profiler エージェントをサーバー サービスに埋め込み、フレームグラフが生成されることを確認しました。

次のステップ

次のステップでは、炎グラフを使用して、アプリケーションのボトルネックの原因を調査します。

5. Cloud Profiler のフレームグラフを分析する

フレームグラフとは何ですか?

フレームグラフは、プロファイル データを可視化する方法の一つです。詳細な説明についてはドキュメントをご覧ください。概要は次のとおりです。

  • 各バーは、アプリ内のメソッド / 関数呼び出しを表します。
  • 縦方向はコールスタックです。コールスタックは上から下に増加します。
  • 水平方向はリソースの使用量です。長くなるほど悪くなります。

取得したフレームグラフを見てみましょう。

7f80797dddc0128d.png

フレームグラフの分析

前のセクションでは、フレームグラフの各バーが関数/メソッドの呼び出しを表し、その長さが関数/メソッドのリソース使用量を表すことを学びました。Cloud Profiler のフレームグラフでは、バーが降順または左から右に長さで並べ替えられます。最初はグラフの左上から確認を開始できます。

6d90760c6c1183cd.png

この例では、grpc.(*Server).serveStreams.func1.2 が CPU 時間の大半を消費していることが明らかであり、コールスタックを上から下に見ると、サーバー サービスの gRPC サーバー ハンドラである main.(*serverService).GetMatchCount がほとんどの時間を費やしています。

GetMatchCount の下に、一連の正規表現regexp関数 regexp.MatchStringregexp.Compile が表示されます。これらは標準パッケージのものです。つまり、パフォーマンスを含む多くの観点で十分にテストする必要があります。しかし、ここでの結果は、regexp.MatchStringregexp.Compile で CPU 時間リソースの使用量が高いことを示しています。このようなことから、ここでは regexp.MatchString の使用がパフォーマンスの問題に関係していると仮定します。それでは、関数が使用されているソースコードを見てみましょう。

step4/src/server/main.go

func (s *serverService) GetMatchCount(ctx context.Context, req *shakesapp.ShakespeareRequest) (*shakesapp.ShakespeareResponse, error) {
        resp := &shakesapp.ShakespeareResponse{}
        texts, err := readFiles(ctx, bucketName, bucketPrefix)
        if err != nil {
                return resp, fmt.Errorf("fails to read files: %s", err)
        }
        for _, text := range texts {
                for _, line := range strings.Split(text, "\n") {
                        line, query := strings.ToLower(line), strings.ToLower(req.Query)
                        isMatch, err := regexp.MatchString(query, line)
                        if err != nil {
                                return resp, err
                        }
                        if isMatch {
                                resp.MatchCount++
                        }
                }
        }
        return resp, nil
}

これは regexp.MatchString が呼び出される場所です。ソースコードを読むと、ネストされた for-loop 内で関数が呼び出されていることにお気づきでしょう。そのため、この関数の使用は誤っている可能性があります。regexp の GoDoc を検索してみましょう。

80b8a4ba1931ff7b.png

このドキュメントによると、regexp.MatchString は呼び出しごとに正規表現パターンをコンパイルします。リソースの大量消費の原因はこうです

概要

このステップでは、フレームグラフを分析して、リソース消費の原因を推定しました。

次のステップ

次のステップでは、サーバー サービスのソースコードを更新し、バージョン 1.0.0 からの変更を確認します。

6. ソースコードを更新してフレームグラフの差分を確認する

ソースコードを更新する

前のステップでは、regexp.MatchString の使用がリソースの大量消費に関係していると仮定しました。では、この問題を解決しましょう。コードを開き、その部分を少し変更します。

step4/src/server/main.go

func (s *serverService) GetMatchCount(ctx context.Context, req *shakesapp.ShakespeareRequest) (*shakesapp.ShakespeareResponse, error) {
        resp := &shakesapp.ShakespeareResponse{}
        texts, err := readFiles(ctx, bucketName, bucketPrefix)
        if err != nil {
                return resp, fmt.Errorf("fails to read files: %s", err)
        }

        // step6. considered the process carefully and naively tuned up by extracting
        // regexp pattern compile process out of for loop.
        query := strings.ToLower(req.Query)
        re := regexp.MustCompile(query)
        for _, text := range texts {
                for _, line := range strings.Split(text, "\n") {
                        line = strings.ToLower(line)
                        isMatch := re.MatchString(line)
                        // step6. done replacing regexp with strings
                        if isMatch {
                                resp.MatchCount++
                        }
                }
        }
        return resp, nil
}

ご覧のとおり、正規表現パターンのコンパイル プロセスが regexp.MatchString から抽出され、ネストされた for ループの外部に移動されています。

このコードをデプロイする前に、initProfiler() 関数でバージョン文字列を更新してください。

step4/src/server/main.go

func initProfiler() {
        cfg := profiler.Config{
                Service:              "server",
                ServiceVersion:       "1.1.0", // step6. update version
                NoHeapProfiling:      true,
                NoAllocProfiling:     true,
                NoGoroutineProfiling: true,
                NoCPUProfiling:       false,
        }
        if err := profiler.Start(cfg); err != nil {
                log.Fatalf("failed to launch profiler agent: %v", err)
        }
}

では、その仕組みを見てみましょう。skaffold コマンドを使用してクラスタをデプロイします。

skaffold dev

しばらくしてから、Cloud Profiler ダッシュボードを再読み込みして、状況を確認します。

283cfcd4c13716ad.png

バージョン 1.1.0 のプロファイルのみが表示されるように、バージョンを "1.1.0" に変更してください。ご覧のとおり、GetMatchCount のバーが短くなり、CPU 時間の使用率が低下しています(バーが短くなっています)。

e3a1456b4aada9a5.png

1 つのバージョンの炎グラフだけでなく、2 つのバージョンの差分も比較できます。

841dec77d8ba5595.png

[比較対象] の値を変更する[Version] のプルダウン リストから[比較バージョン]の値を変更します「1.0.0」に変更します。

5553844292d6a537.png

このようなフレームグラフが表示されます。グラフの形状は 1.1.0 と同じですが、色分けが異なります。比較モードの場合、色の意味は次のようになります。

  • : 値(リソース消費量)を削減
  • オレンジ: 得られた価値(リソース消費)
  • グレー: どちらとも言えない

凡例を踏まえて、関数を詳しく見てみましょう。拡大したいバーをクリックすると、グルーピング内の詳細情報が表示されます。main.(*serverService).GetMatchCount バーをクリックしてください。また、バーにカーソルを合わせると、比較の詳細が表示されます。

ca08d942dc1e2502.png

合計 CPU 時間が 5.26 秒から 2.88 秒に短縮されたことがわかります(合計は 10 秒 = サンプリング ウィンドウ)。これは大きな改善点です。

プロファイル データの分析から、アプリケーションのパフォーマンスを向上させることができます。

概要

このステップでは、サーバー サービスを編集し、Cloud Profiler の比較モードが改善されたことを確認しました。

次のステップ

次のステップでは、サーバー サービスのソースコードを更新し、バージョン 1.0.0 からの変更を確認します。

7. 追加のステップ: Trace ウォーターフォールで改善されたことを確認する

分散トレースと継続的プロファイリングの違い

Codelab のパート 1 では、リクエストパスのマイクロサービス全体でボトルネック サービスを特定できることと、特定のサービスにおけるボトルネックの正確な原因を特定できないことを確認しました。このパート 2 の Codelab では、継続的プロファイリングによって、1 つのサービス内のボトルネックをコールスタックから特定できることを学びました。

このステップでは、分散トレース(Cloud Trace)のウォーターフォール グラフを確認し、継続的プロファイリングとの違いを確認します。

このウォーターフォール グラフは、「love」というクエリを含むトレースの 1 つです。合計で約 6.7 秒(6,700 ミリ秒)かかります。

e2b7dec25926ee51.png

同じクエリの改善後の実行時間は次のとおりです。おわかりのように、合計レイテンシは 1.5 秒(1,500 ミリ秒)になり、以前の実装から大幅に改善されています。

feeb7207f36c7e5e.png

ここで重要なのは、分散トレースのウォーターフォール チャートでは、あらゆる場所のスパンを計測しなければコールスタック情報を利用できないことです。また、分散トレースはサービス全体のレイテンシに注目するだけですが、継続的なプロファイリングは単一のサービスのコンピュータ リソース(CPU、メモリ、OS スレッド)に焦点を当てます。

別の観点では、分散トレースのイベントベース、継続プロファイルは統計的です。トレースごとにレイテンシ グラフが異なり、レイテンシの変化の傾向を確認するには、分布などの異なる形式が必要です。

概要

このステップでは、分散トレース機能と継続的プロファイリングの違いを確認しました。

8. 完了

OpenTelemery を使用して分散トレースを作成し、Google Cloud Trace でマイクロサービス全体のリクエストのレイテンシを確認できました。

より長い演習が必要な場合は、以下のトピックをご自身で試してください。

  • 現在の実装では、ヘルスチェックによって生成されたすべてのスパンが送信されます。(grpc.health.v1.Health/Check)Cloud Trace からこれらのスパンを除外するにはどうすればよいですか?こちらのヒントをご覧ください。
  • イベントログをスパンと関連付けて、Google Cloud Trace と Google Cloud Logging でどのように機能するかを確認する。こちらのヒントをご覧ください。
  • 一部のサービスを別の言語のサービスに置き換えて、その言語の OpenTelemetry で計測してみます。

また、プロファイラについてさらに詳しくお知りになりたい場合は、パート 2 にお進みください。その場合は、以下のクリーンアップの説明をスキップできます。

クリーンアップ

この Codelab の後、Google Kubernetes Engine、Google Cloud Trace、Google Artifact Registry で予期しない請求が発生しないように、Kubernetes クラスタを停止し、プロジェクトを削除してください。

まず、クラスタを削除します。skaffold dev でクラスタを実行している場合は、Ctrl+C キーを押すだけで済みます。skaffold run でクラスタを実行している場合は、次のコマンドを実行します。

skaffold delete

コマンド出力

Cleaning up...
 - deployment.apps "clientservice" deleted
 - service "clientservice" deleted
 - deployment.apps "loadgen" deleted
 - deployment.apps "serverservice" deleted
 - service "serverservice" deleted

クラスタを削除したら、メニューペインで [IAM と管理&gt;[設定]、[シャットダウン] の順にクリックします] ボタンを離します。

45aa37b7d5e1ddd1.png

次に、ダイアログのフォームに(プロジェクト名ではなく)プロジェクト ID を入力し、シャットダウンを確認します。