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

1. はじめに

505827108874614d.png

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

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

オブザーバビリティと OpenTelemetry

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

OpenTelemetry は、オブザーバビリティに必要なテレメトリー データ(ログ、指標、トレース)の計測とエクスポートを高速化する仕様、ライブラリ、エージェントのセットです。OpenTelemetry は、CNCF に基づくオープン スタンダードでコミュニティ主導のプロジェクトです。プロジェクトとそのエコシステムが提供するライブラリを利用することで、デベロッパーはベンダーに依存しない方法で複数のアーキテクチャに対してアプリケーションを計測できるようになります。

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

この Codelab はシリーズのパート 1 で、OpenTelemetry と Cloud Trace を使用してマイクロサービスで分散トレースを計測する方法について説明します。パート 2 では、Cloud Profiler による継続的プロファイリングについて説明します。

分散トレース

ログ、指標、トレースのうち、トレースはシステム内のプロセスの特定の部分のレイテンシを示すテレメトリーです。特にマイクロサービスの時代において、分散トレースは、分散システム全体のレイテンシのボトルネックを見つける強力な推進力となります。

分散トレースを分析する場合、トレースデータの可視化は、システム全体のレイテンシを一目で把握するための鍵となります。分散トレースでは、システム エントリ ポイントへの 1 つのリクエストを処理する一連の呼び出しを、複数のスパンを含む Trace の形式で処理します。

スパンは分散システムで行われる個々の作業単位を表し、開始時刻と終了時刻が記録されます。スパンは、多くの場合、互いに階層関係にあります。下の図では、すべての小さなスパンが大きな /messages スパンの子スパンであり、1 つのトレースに組み込まれ、システム内の作業パスを示します。

トレース

Google Cloud Trace は分散トレース バックエンドのオプションの一つであり、Google Cloud の他のプロダクトと緊密に統合されています。

作成するアプリの概要

この Codelab では、「Shakespeare application」というサービスでトレース情報をインストルメント化します。(別名: Shakesapp)です。Shakesapp のアーキテクチャは次のとおりです。

44e243182ced442f.png

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

リクエスト全体でトレース情報を計測します。その後、サーバーに Profiler エージェントを埋め込み、ボトルネックを調査します。

学習内容

  • Go プロジェクトで OpenTelemetry Trace ライブラリの使用を開始する方法
  • ライブラリでスパンを作成する方法
  • アプリ コンポーネント間のワイヤーを介してスパン コンテキストを伝播する方法
  • トレースデータを Cloud Trace に送信する方法
  • Cloud Trace でトレースを分析する方法

この Codelab では、マイクロサービスを計測する方法について説明します。理解しやすいように、この例では 3 つのコンポーネント(負荷生成ツール、クライアント、サーバー)のみが含まれていますが、この 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 クラスタを作成した

次のステップ

次のステップでは、マイクロサービスを構築、push、クラスタにデプロイします。

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 では、step0 フォルダにあるソースコードを更新します。次の手順では、step[1-6] フォルダ内のソースコードを参照して回答を確認することもできます。(パート 1 はステップ 0 からステップ 4、パート 2 はステップ 5 と 6 に対応しています)

skaffold コマンドを実行

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

cd step0
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. HTTP のインストルメンテーション

トレースの計測と伝播のコンセプト

ソースコードを編集する前に、分散トレースの仕組みを簡単な図で簡単に説明します。

6be42e353b9bfd1d.png

この例では、トレースとスパンの情報を Cloud Trace にエクスポートし、loadgen サービスからサーバー サービスにリクエスト全体でトレース コンテキストを伝播するようにコードをインストルメント化します。

Cloud Trace が同じトレース ID を持つすべてのスパンを 1 つのトレースにまとめるには、アプリケーションがトレース ID やスパン ID などのトレース メタデータを送信する必要があります。また、アプリケーションは、ダウンストリーム サービスのリクエスト時にトレース コンテキスト(親スパンのトレース ID とスパン ID の組み合わせ)を伝播し、処理しているトレース コンテキストを認識できるようにする必要があります。

OpenTelemetry を使用すると、次のことが可能になります。

  • スパン ID とトレース ID を生成できます。
  • トレース ID とスパン ID をバックエンドにエクスポートします。
  • トレース コンテキストを他のサービスに伝播
  • トレースの分析に役立つ追加のメタデータを

OpenTelemetry Trace のコンポーネント

b01f7bb90188db0d.png

OpenTelemetry でアプリケーション トレースを計測可能にするプロセスは次のとおりです。

  1. エクスポータを作成する
  2. エクスポータをバインドする TracerProvider を 1 で作成し、グローバルに設定します。
  3. TextMapPropagaror を設定して伝播メソッドを設定する
  4. TracerProvider からトレーサーを取得する
  5. トレーサーからスパンを生成する

現時点では、各コンポーネントの詳細なプロパティを理解する必要はありませんが、覚えておくべき最も重要な点は次のとおりです。

  • このエクスポータは TracerProvider に接続可能です。
  • TracerProvider は、トレースのサンプリングとエクスポートに関するすべての構成を保持します。
  • すべてのトレースが Tracer オブジェクトに

これを理解したうえで、実際のコーディング作業に進みましょう。

最初のスパンを計測する

計測負荷生成サービス

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

step0/src/loadgen/main.go

func main() {
        ...
        for range t.C {
                log.Printf("simulating client requests, round %d", i)
                if err := run(numWorkers, numConcurrency); err != nil {
                        log.Printf("aborted round with error: %v", err)
                }
                log.Printf("simulated %d requests", numWorkers)
                if numRounds != 0 && i > numRounds {
                        break
                }
                i++
        }
}

main 関数で、run 関数を呼び出すループを確認できます。現在の実装では、関数呼び出しの開始と終了を記録するログ行が 2 行あります。次に、Span 情報をインストルメント化して、関数呼び出しのレイテンシを追跡します。

まず、前のセクションで説明したように、OpenTelemetry の構成全体を設定しましょう。次のように OpenTelemetry パッケージを追加します。

step0/src/loadgen/main.go

import (
        "context" // step1. add packages
        "encoding/json"
        "fmt"
        "io"
        "log"
        "math/rand"
        "net/http"
        "net/url"
        "time"
        // step1. add packages
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
        "go.opentelemetry.io/otel/trace"
        // step1. end add packages
)

読みやすくするために、initTracer というセットアップ関数を作成し、main 関数で呼び出します。

step0/src/loadgen/main.go

// step1. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // create a stdout exporter to show collected spans out to stdout.
        exporter, err := stdout.New(stdout.WithPrettyPrint())
        if err != nil {
                return nil, err
        }

        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

OpenTelemetry の設定手順は、前のセクションで説明したとおりです。この実装では、すべてのトレース情報を構造化形式で stdout にエクスポートする stdout エクスポータを使用します。

次に、main 関数から呼び出します。initTracer() を呼び出し、アプリケーションを閉じるときは必ず TracerProvider.Shutdown() を呼び出します。

step0/src/loadgen/main.go

func main() {
        // step1. 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)
                }
        }()
        // step1. end setup

        log.Printf("starting worder with %d workers in %d concurrency", numWorkers, numConcurrency)
        log.Printf("number of rounds: %d (0 is inifinite)", numRounds)
        ...

設定が完了したら、一意のトレース ID とスパン ID を持つスパンを作成する必要があります。OpenTelemetry には、そのための便利なライブラリが用意されています。計測 HTTP クライアントに新しいパッケージを追加します。

step0/src/loadgen/main.go

import (
        "context"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "math/rand"
        "net/http"
        "net/http/httptrace" // step1. add packages
        "net/url"
        "time"
        // step1. add packages
        "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
        // step1. end add packages
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
        "go.opentelemetry.io/otel/trace"
)

負荷生成ツールは、runQuery 関数で net/http を使用して HTTP でクライアント サービスを呼び出しているため、net/http の contrib パッケージを使用し、httptrace パッケージと otelhttp パッケージの拡張による計測を有効にします。

まず、計測対象のクライアント経由で HTTP リクエストを呼び出すためのパッケージ グローバル変数 httpClient を追加します。

step0/src/loadgen/main.go

var httpClient = http.Client{
        Transport: otelhttp.NewTransport(http.DefaultTransport)
}

次に、runQuery 関数に計測を追加し、OpenTelemetry を使用してカスタムスパンと、カスタム HTTP クライアントから自動生成されたスパンを作成します。演習内容は次のとおりです。

  1. otel.Tracer() を使用してグローバル TracerProvider からトレーサーを取得する
  2. Tracer.Start() メソッドを使用してルートスパンを作成する
  3. 任意のタイミング(この場合は runQuery 関数の終了)でルートスパンを終了します。

step0/src/loadgen/main.go

        reqURL.RawQuery = v.Encode()
        // step1. replace http.Get() with custom client call
        // resp, err := http.Get(reqURL.String())

        // step1. instrument trace
        ctx := context.Background()
        tr := otel.Tracer("loadgen")
        ctx, span := tr.Start(ctx, "query.request", trace.WithAttributes(
                semconv.TelemetrySDKLanguageGo,
                semconv.ServiceNameKey.String("loadgen.runQuery"),
                attribute.Key("query").String(s),
        ))
        defer span.End()
        ctx = httptrace.WithClientTrace(ctx, otelhttptrace.NewClientTrace(ctx))
        req, err := http.NewRequestWithContext(ctx, "GET", reqURL.String(), nil)
        if err != nil {
                return -1, fmt.Errorf("error creating HTTP request object: %v", err)
        }
        resp, err := httpClient.Do(req)
        // step1. end instrumentation
        if err != nil {
                return -1, fmt.Errorf("error sending request to %v: %v", reqURL.String(), err)
        }

これで、loadgen(HTTP クライアント アプリケーション)でのインストルメンテーションが完了しました。go mod コマンドを使用して go.modgo.sum を更新してください。

go mod tidy

クライアント サービスを計測する

前のセクションでは、下の図の赤い長方形で囲まれた部分をインストルメント化しました。負荷生成サービスでスパン情報を計測可能にしました。負荷生成サービスと同様に、クライアント サービスをインストルメント化する必要があります。負荷生成サービスとの違いは、クライアント サービスが HTTP ヘッダー内の負荷生成サービスから伝播したトレース ID 情報を抽出し、その ID を使用してスパンを生成することです。

bcaccd06691269f8.png

負荷生成サービスの場合と同様に、Cloud Shell エディタを開いて必要なパッケージを追加します。

step0/src/client/main.go

import (
        "context"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "net/url"
        "os"
        "time"

        "opentelemetry-trace-codelab-go/client/shakesapp"
        // step1. add new import
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        "go.opentelemetry.io/otel/trace"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
        // step1. end new import
)

ここでも、OpenTelemtry を設定する必要があります。loadgen から initTracer 関数をコピーして貼り付け、クライアント サービスの main 関数でも呼び出すだけです。

step0/src/client/main.go

// step1. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // create a stdout exporter to show collected spans out to stdout.
        exporter, err := stdout.New(stdout.WithPrettyPrint())
        if err != nil {
                return nil, err
        }

        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

次はスパンを計測します。クライアント サービスは loadgen サービスからの HTTP リクエストを受け入れる必要があるため、ハンドラを計測する必要があります。クライアント サービスの HTTP サーバーは net/http で実装されており、loadgen で行ったように otelhttp パッケージを使用できます。

まず、ハンドラ登録を otelhttp Handler に置き換えます。main 関数で、HTTP ハンドラが http.HandleFunc() に登録されている行を見つけます。

step0/src/client/main.go

        // step1. change handler to intercept OpenTelemetry related headers
        // http.HandleFunc("/", svc.handler)
        otelHandler := otelhttp.NewHandler(http.HandlerFunc(svc.handler), "client.handler")
        http.Handle("/", otelHandler)
        // step1. end intercepter setting
        http.HandleFunc("/_genki", svc.health)

次に、ハンドラ内で実際のスパンを計測します。func (*clientService) handler() を見つけて、trace.SpanFromContext() を使用して span 計測を追加します。

step0/src/client/main.go

func (cs *clientService) handler(w http.ResponseWriter, r *http.Request) {
        ...
        ctx := r.Context()
        ctx, cancel := context.WithCancel(ctx)
        defer cancel()
        // step1. instrument trace
        span := trace.SpanFromContext(ctx)
        defer span.End()
        // step1. end instrument
        ...

このインストルメンテーションでは、handler メソッドの先頭から最後までのスパンを取得します。スパンの分析を容易にするために、一致した数を格納する属性をクエリに追加します。ログ行の直前に次のコードを追加します。

step0/src/client/main.go

func (cs *clientService) handler(w http.ResponseWriter, r *http.Request) {
        ...
        // step1. add span specific attribute
        span.SetAttributes(attribute.Key("matched").Int64(resp.MatchCount))
        // step1. end adding attribute
        log.Println(string(ret))
        ...

上記のすべての計測で、loadgen と client の間のトレース計測を完了しました。では、その仕組みを見てみましょう。skaffold でコードを再度実行します。

skaffold dev

GKE クラスタでサービスを実行すると、次のような大量のログメッセージが表示されます。

コマンド出力

[loadgen] {
[loadgen]       "Name": "query.request",
[loadgen]       "SpanContext": {
[loadgen]               "TraceID": "cfa22247a542beeb55a3434392d46b89",
[loadgen]               "SpanID": "18b06404b10c418b",
[loadgen]               "TraceFlags": "01",
[loadgen]               "TraceState": "",
[loadgen]               "Remote": false
[loadgen]       },
[loadgen]       "Parent": {
[loadgen]               "TraceID": "00000000000000000000000000000000",
[loadgen]               "SpanID": "0000000000000000",
[loadgen]               "TraceFlags": "00",
[loadgen]               "TraceState": "",
[loadgen]               "Remote": false
[loadgen]       },
[loadgen]       "SpanKind": 1,
[loadgen]       "StartTime": "2022-07-14T13:13:36.686751087Z",
[loadgen]       "EndTime": "2022-07-14T13:14:31.849601964Z",
[loadgen]       "Attributes": [
[loadgen]               {
[loadgen]                       "Key": "telemetry.sdk.language",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "go"
[loadgen]                       }
[loadgen]               },
[loadgen]               {
[loadgen]                       "Key": "service.name",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "loadgen.runQuery"
[loadgen]                       }
[loadgen]               },
[loadgen]               {
[loadgen]                       "Key": "query",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "faith"
[loadgen]                       }
[loadgen]               }
[loadgen]       ],
[loadgen]       "Events": null,
[loadgen]       "Links": null,
[loadgen]       "Status": {
[loadgen]               "Code": "Unset",
[loadgen]               "Description": ""
[loadgen]       },
[loadgen]       "DroppedAttributes": 0,
[loadgen]       "DroppedEvents": 0,
[loadgen]       "DroppedLinks": 0,
[loadgen]       "ChildSpanCount": 5,
[loadgen]       "Resource": [
[loadgen]               {
[loadgen]                       "Key": "service.name",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "unknown_service:loadgen"
...

stdout エクスポータは、これらのメッセージを出力します。loadgen によるすべてのスパンの親には TraceID: 00000000000000000000000000000000 があります。これがルートスパン、つまりトレースの最初のスパンであるためです。また、埋め込み属性 "query" には、クライアント サービスに渡されるクエリ文字列があることがわかります。

概要

このステップでは、HTTP で通信する負荷生成サービスとクライアント サービスを計測可能にしました。また、トレース コンテキストをサービス間で正常に伝播し、両方のサービスから Span 情報を stdout にエクスポートできることを確認しました。

次のステップ

次のステップでは、クライアント サービスとサーバー サービスを計測して、gRPC を介してトレース コンテキストを伝播する方法を確認します。

5. gRPC のインストルメンテーション

前のステップでは、リクエストの前半をこのマイクロサービスに計測しました。このステップでは、クライアント サービスとサーバー サービス間の gRPC 通信を計測します。(下の画像の緑色と紫色の長方形)。

75310d8e0e3b1a30.png

gRPC クライアントのインストルメンテーションを事前にビルドする

OpenTelemetry のエコシステムには、デベロッパーがアプリケーションを計測する際に役立つライブラリが多数用意されています。前のステップでは、net/http パッケージにビルド前のインストルメンテーションを使用しました。このステップでは、gRPC を介してトレース コンテキストを伝播するため、そのライブラリを使用します。

まず、otelgrpc というビルド済みの gRPC パッケージをインポートします。

step0/src/client/main.go

import (
        "context"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "net/url"
        "os"
        "time"

        "opentelemetry-trace-codelab-go/client/shakesapp"
        // step2. add prebuilt gRPC package (otelgrpc) 
        "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        "go.opentelemetry.io/otel/trace"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
)

今回は、クライアント サービスはサーバー サービスに対する gRPC クライアントであるため、gRPC クライアントを計測する必要があります。mustConnGRPC 関数を見つけて、クライアントがサーバーにリクエストを行うたびに新しいスパンを計測する gRPC インターセプタを追加します。

step0/src/client/main.go

// Helper function for gRPC connections: Dial and create client once, reuse.
func mustConnGRPC(ctx context.Context, conn **grpc.ClientConn, addr string) {
        var err error
        // step2. add gRPC interceptor
        interceptorOpt := otelgrpc.WithTracerProvider(otel.GetTracerProvider())
        *conn, err = grpc.DialContext(ctx, addr,
                grpc.WithTransportCredentials(insecure.NewCredentials()),
                grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor(interceptorOpt)),
                grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor(interceptorOpt)),
                grpc.WithTimeout(time.Second*3),
        )
        // step2: end adding interceptor
        if err != nil {
                panic(fmt.Sprintf("Error %s grpc: failed to connect %s", err, addr))
        }
}

前のセクションですでに OpenTelemetry を設定しているため、その必要はありません。

gRPC サーバー用に事前に構築されたインストルメンテーション

gRPC クライアントの場合と同様に、gRPC サーバーに対して事前構築済みのインストルメンテーションを呼び出します。新しいパッケージをインポート セクションに次のように追加します。

step0/src/server/main.go

import (
        "context"
        "fmt"
        "io/ioutil"
        "log"
        "net"
        "os"
        "regexp"
        "strings"

        "opentelemetry-trace-codelab-go/server/shakesapp"

        "cloud.google.com/go/storage"
        // step2. add OpenTelemetry packages including otelgrpc
        "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
        "go.opentelemetry.io/otel"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        "google.golang.org/api/iterator"
        "google.golang.org/api/option"
        "google.golang.org/grpc"
        healthpb "google.golang.org/grpc/health/grpc_health_v1"
)

今回はサーバーを計測するのが初めてなので、loadgen サービスとクライアント サービスで行ったのと同じように、まず OpenTelemetry をセットアップする必要があります。

step0/src/server/main.go

// step2. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // create a stdout exporter to show collected spans out to stdout.
        exporter, err := stdout.New(stdout.WithPrettyPrint())
        if err != nil {
                return nil, err
        }
        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

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
        ...

次に、サーバー インターセプタを追加する必要があります。main 関数で、grpc.NewServer() が呼び出される場所を見つけて、インターセプタを関数に追加します。

step0/src/server/main.go

func main() {
        ...
        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)
        ...

マイクロサービスを実行してトレースを確認する

次に、変更したコードを skaffold コマンドを使用して実行します。

skaffold dev

再び stdout に多数のスパン情報が表示されます。

コマンド出力

...
[server] {
[server]        "Name": "shakesapp.ShakespeareService/GetMatchCount",
[server]        "SpanContext": {
[server]                "TraceID": "89b472f213a400cf975e0a0041649667",
[server]                "SpanID": "96030dbad0061b3f",
[server]                "TraceFlags": "01",
[server]                "TraceState": "",
[server]                "Remote": false
[server]        },
[server]        "Parent": {
[server]                "TraceID": "89b472f213a400cf975e0a0041649667",
[server]                "SpanID": "cd90cc3859b73890",
[server]                "TraceFlags": "01",
[server]                "TraceState": "",
[server]                "Remote": true
[server]        },
[server]        "SpanKind": 2,
[server]        "StartTime": "2022-07-14T14:05:55.74822525Z",
[server]        "EndTime": "2022-07-14T14:06:03.449258891Z",
[server]        "Attributes": [
...
[server]        ],
[server]        "Events": [
[server]                {
[server]                        "Name": "message",
[server]                        "Attributes": [
...
[server]                        ],
[server]                        "DroppedAttributeCount": 0,
[server]                        "Time": "2022-07-14T14:05:55.748235489Z"
[server]                },
[server]                {
[server]                        "Name": "message",
[server]                        "Attributes": [
...
[server]                        ],
[server]                        "DroppedAttributeCount": 0,
[server]                        "Time": "2022-07-14T14:06:03.449255889Z"
[server]                }
[server]        ],
[server]        "Links": null,
[server]        "Status": {
[server]                "Code": "Unset",
[server]                "Description": ""
[server]        },
[server]        "DroppedAttributes": 0,
[server]        "DroppedEvents": 0,
[server]        "DroppedLinks": 0,
[server]        "ChildSpanCount": 0,
[server]        "Resource": [
[server]                {
...
[server]        ],
[server]        "InstrumentationLibrary": {
[server]                "Name": "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc",
[server]                "Version": "semver:0.33.0",
[server]                "SchemaURL": ""
[server]        }
[server] }
...

スパン名は埋め込まれておらず、trace.Start() または span.SpanFromContext() を使用して手動で作成したスパンも埋め込まれていません。それでも、gRPC インターセプタによって生成されるため、多数のスパンが生成されます。

概要

このステップでは、OpenTelemetry エコシステム ライブラリのサポートを利用して、gRPC ベースの通信を計測しました。

次のステップ

次のステップでは、最後に Cloud Trace を使用してトレースを可視化し、収集したスパンを分析する方法を学習します。

6. Cloud Trace でトレースを可視化する

OpenTelemetry を使用して、システム全体のトレースを計測しました。ここまでで、HTTP サービスと gRPC サービスを計測する方法を学習しました。データを計測する方法は学んできましたが、まだデータの分析方法は学んでいません。このセクションでは、stdout エクスポータを Cloud Trace エクスポータに置き換え、トレースを分析する方法を学習します。

Cloud Trace エクスポータを使用する

OpenTelemetry の優れた特徴の一つは、プラグイン性です。計測によって収集されたすべてのスパンを可視化するには、stdout エクスポータを Cloud Trace エクスポータに置き換えるだけです。

各サービスの main.go ファイルを開き、initTracer() 関数を見つけます。その行を削除して stdout エクスポータを生成し、代わりに Cloud Trace エクスポータを作成します。

step0/src/loadgen/main.go

import (
        ...
        // step3. add OpenTelemetry for Cloud Trace package
        cloudtrace "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
)

// step1. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // step3. replace stdout exporter with Cloud Trace exporter
        // cloudtrace.New() finds the credentials to Cloud Trace automatically following the
        // rules defined by golang.org/x/oauth2/google.findDefaultCredentailsWithParams.
        // https://pkg.go.dev/golang.org/x/oauth2/google#FindDefaultCredentialsWithParams
        exporter, err := cloudtrace.New()
        // step3. end replacing exporter
        if err != nil {
                return nil, err
        }

        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

クライアント サービスとサーバー サービスでも同じ関数を編集する必要があります。

マイクロサービスを実行してトレースを確認する

編集後は、skaffold コマンドを使用して通常どおりにクラスタを実行します。

skaffold dev

エクスポータを Cloud Trace に置き換えるため、stdout では構造化ログ形式のスパン情報があまり表示されなくなります。

コマンド出力

[loadgen] 2022/07/14 15:01:07 simulated 20 requests
[loadgen] 2022/07/14 15:01:07 simulating client requests, round 37
[loadgen] 2022/07/14 15:01:14 query 'sweet': matched 958
[client] 2022/07/14 15:01:14 {"match_count":958}
[client] 2022/07/14 15:01:14 {"match_count":3040}
[loadgen] 2022/07/14 15:01:14 query 'love': matched 3040
[client] 2022/07/14 15:01:15 {"match_count":349}
[loadgen] 2022/07/14 15:01:15 query 'hello': matched 349
[client] 2022/07/14 15:01:15 {"match_count":484}
[loadgen] 2022/07/14 15:01:15 query 'faith': matched 484
[loadgen] 2022/07/14 15:01:15 query 'insolence': matched 14
[client] 2022/07/14 15:01:15 {"match_count":14}
[client] 2022/07/14 15:01:21 {"match_count":484}
[loadgen] 2022/07/14 15:01:21 query 'faith': matched 484
[client] 2022/07/14 15:01:21 {"match_count":728}
[loadgen] 2022/07/14 15:01:21 query 'world': matched 728
[client] 2022/07/14 15:01:22 {"match_count":484}
[loadgen] 2022/07/14 15:01:22 query 'faith': matched 484
[loadgen] 2022/07/14 15:01:22 query 'hello': matched 349
[client] 2022/07/14 15:01:22 {"match_count":349}
[client] 2022/07/14 15:01:23 {"match_count":1036}
[loadgen] 2022/07/14 15:01:23 query 'friend': matched 1036
[loadgen] 2022/07/14 15:01:28 query 'tear': matched 463
...

次に、すべてのスパンが Cloud Trace に正しく送信されているかどうかを確認します。Cloud コンソールにアクセスし、[トレースリスト] に移動します。検索ボックスから簡単にアクセスできます。それ以外の場合は、左ペインのメニューをクリックします。8b3f8411bd737e06.png

レイテンシ グラフ全体に多くの青い点が分散していることがわかります。各スポットは 1 つのトレースを表します。

3ecf131423fc4c40.png

いずれかをクリックすると、トレース内の詳細が表示されます。4fd10960c6648a03.png

このように簡単なチェックだけでも、すでに多くの分析情報が確認できます。たとえば、ウォーターフォール グラフを見ると、レイテンシの原因のほとんどが shakesapp.ShakespeareService/GetMatchCount という名前のスパンにあることがわかります。(上の画像の 1 を参照)これはサマリー テーブルで確認できます。(右端の列は各スパンの期間を示しています)。また、このトレースは「friend」というクエリに対するものです。(上の画像の 2 を参照)

このように短い分析を行うと、GetMatchCount メソッド内でより詳細なスパンを知る必要があることがわかります。stdout 情報と比較して、可視化は強力です。Cloud Trace について詳しくは、公式ドキュメントをご覧ください。

概要

このステップでは、stdout エクスポータを Cloud Trace One に置き換え、トレースを Cloud Trace で可視化しました。また、トレースの分析を開始する方法も学びました。

次のステップ

次のステップでは、サーバー サービスのソースコードを変更して、GetMatchCount にサブスパンを追加します。

7. サブスパンを追加して分析精度を上げる

前のステップでは、loadgen から観測されたラウンドトリップ時間の原因のほとんどが、GetMatchCount メソッド内のプロセス、gRPC ハンドラであることを特定しました。ただし、ハンドラ以外には何も計測できなかったため、ウォーターフォール グラフから詳細な分析情報を見つけることはできません。これは、マイクロサービスの計測を開始するときによく見られるケースです。

3b63a1e471dddb8c.png

このセクションでは、サーバーが Google Cloud Storage を呼び出すサブスパンを計測します。これは、一部の外部ネットワーク I/O の処理に時間がかかることが一般的であり、呼び出しが原因かどうかを特定することが重要であるためです。

サーバーにサブスパンを計測可能にする

サーバーで main.go を開き、関数 readFiles を見つけます。この関数は、Google Cloud Storage へのリクエストを呼び出して、シェイクスピア作品のすべてのテキスト ファイルを取得します。この関数では、クライアント サービスで HTTP サーバー計測の場合と同様に、サブスパンを作成できます。

step0/src/server/main.go

func readFiles(ctx context.Context, bucketName, prefix string) ([]string, error) {
        type resp struct {
                s   string
                err error
        }

        // step4: add an extra span
        span := trace.SpanFromContext(ctx)
        span.SetName("server.readFiles")
        span.SetAttributes(attribute.Key("bucketname").String(bucketName))
        defer span.End()
        // step4: end add span
        ...

新しいスパンの追加は以上ですアプリを実行して、どのようになるか見てみましょう。

マイクロサービスを実行してトレースを確認する

編集後は、skaffold コマンドを使用して通常どおりにクラスタを実行します。

skaffold dev

トレースリストから query.request という名前のトレースを 1 つ選択します。同様のトレース ウォーターフォール グラフが表示されますが、shakesapp.ShakespeareService/GetMatchCount の下に新しいスパンがあります。(以下の赤い長方形で囲まれたスパン)

3d4a891aa30d7a32.png

このグラフからわかるように、Google Cloud Storage への外部呼び出しがレイテンシの大部分を占めていますが、レイテンシの大部分は他の要因によって占められています。

トレースのウォーターフォール グラフでいくつか確認するだけで、すでに多くの分析情報を得ています。アプリケーションのさらなるパフォーマンスの詳細をどのように入手しますか。プロファイラの登場です。ここでは、この Codelab の最後に、プロファイラのチュートリアルはすべてパート 2 に任せましょう。

概要

このステップでは、サーバー サービスに別のスパンを計測し、システム レイテンシについてさらに詳細な分析情報を取得しました。

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 を入力し、シャットダウンを確認します。