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

1. はじめに

e0509e8a07ad5537.png

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

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

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

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

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

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

作成するアプリの概要

この Codelab では、「Shakespeare アプリ」のサーバー サービスで継続的なプロファイラ エージェントを計測します。(別名: Shakesapp)です。Shakesapp のアーキテクチャは次のとおりです。

44e243182ced442f.png

  • Loadgen が HTTP でクエリ文字列をクライアントに送信する
  • クライアントが loadgen から 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] のメニューに移動します。[有効にする]ボタンを押します

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 を設定する
  • Container Registry 用の Artifact Registry リポジトリを作成する
  • Container 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
  • マニフェスト: 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 エディタを開くには、Cloud Shell の右上にあるボタン 776a11bfb2122549.png を押します。左側のペインにあるエクスプローラから 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.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 と管理>[設定]、[シャットダウン] の順にクリックします] ボタンを離します。

45aa37b7d5e1ddd1.png

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