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 でクエリ文字列をクライアントに送信する
  • クライアントは、loadgen からサーバーへのクエリを gRPC で渡します。
  • サーバーはクライアントからクエリを受け取り、Google Cloud Storage からテキスト形式のシェイクスピアの全作品を取得し、クエリを含む行を検索して、一致した行の数をクライアントに返します。

パート 1 では、ボトルネックがサーバー サービスのどこかに存在することがわかりましたが、正確な原因を特定できませんでした。

学習内容

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

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

必要なもの

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

2. 設定と要件

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

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

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

7a32e5469db69e9.png

表示されたダイアログで [NEW PROJECT] ボタンをクリックして、新しいプロジェクトを作成します。

7136b3ee36ebaf89.png

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

870a3cbd6541ee86.png

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

affdc444517ba805.png

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

次に、Google Cloud リソースを使用し、Cloud Trace API を有効にするために、Developers 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 では、Google Cloud Shell(Cloud 上で動作するコマンドライン環境)を使用します。

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

Cloud Console から 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 Console ダッシュボードで検索します。

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 してデプロイするためのコンテナ レジストリを準備します。これらの手順では、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

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

「us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab」がコピーされました

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 を設定する
  • コードラボのマイクロサービスが実行される 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 エージェントの計測

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

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

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

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

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

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

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{} オブジェクトで指定されたオプションを詳しく見てみましょう。

  • サービス: プロファイラ ダッシュボードで選択して切り替えることができるサービス名
  • 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 のフレームグラフを分析する

フレームグラフとは

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

  • 各バーは、アプリケーション内のメソッド/関数呼び出しを表します。
  • 垂直方向はコールスタックです。コールスタックは上から下に伸びます
  • 横方向はリソース使用量を示します。長いほど、リソース使用量が多いことを示します。

これを踏まえて、取得したフレームグラフを見てみましょう。

7f80797dddc0128d.png

フレーム グラフの分析

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

6d90760c6c1183cd.png

この例では、grpc.(*Server).serveStreams.func1.2 が CPU 時間の大部分を消費していることが明確に示されています。呼び出しスタックを上から下まで確認すると、時間の大部分が main.(*serverService).GetMatchCount で費やされています。これは、サーバー サービスの gRPC サーバー ハンドラです。

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 ループ内で呼び出されていることがわかります。そのため、この関数の使用が正しくない可能性があります。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

単一バージョンのフレーム グラフを見るだけでなく、2 つのバージョン間の差分を比較することもできます。

841dec77d8ba5595.png

[Compare to] プルダウン リストの値を [Version] に変更し、[Compared version] の値を元のバージョンである [1.0.0] に変更します。

5553844292d6a537.png

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

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

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

ca08d942dc1e2502.png

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

これで、プロファイル データの分析からアプリケーションのパフォーマンスを改善できるようになりました。

概要

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

次のステップ

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

7. 追加の手順: Trace ウォーターフォールで改善を確認する

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

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

このステップでは、分散トレース(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 の後、Kubernetes クラスタを停止し、プロジェクトを削除して、Google Kubernetes Engine、Google Cloud Trace、Google Artifact Registry で予期しない料金が発生しないようにしてください。

まず、クラスタを削除します。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(プロジェクト名ではない)を入力し、シャットダウンを確認します。