この記事は、Microservices July 2023: マイクロサービスのStart Delivering Microservicesの方法を実行するための4つのチュートリアルの1つです。
マイクロサービスアーキテクチャには、チームの独立性の向上やスケーリング、と導入における柔軟性の向上など、多くの利点があります。その反面、システム内のサービスが増えるほど(マイクロサービスアプリのサービスは数十から数百になることもあります)、システムの全体的な運用状況を明確に把握することが難しくなります。私たちは、複雑なソフトウェアシステムを構築および保守しているので、明確に把握することが極めて重要であることを知っています。可観測性ツールは、数多くのサービスやサポートインフラストラクチャ全体で状況を把握できます。
このチュートリアルでは、マイクロサービスアプリにとって非常に重要な可観測性の1つであるトレーシングに焦点を当てます。本題に入る前に、可観測性を議論するときによく使われる用語を定義しておきます。
これらすべての概念を使用して、マイクロサービスのパフォーマンスに関するインサイトを得ることができます。トレーシングは、リクエストが処理される際、複数の、一般的にゆるくつながっている、コンポーネントの全体でいったい何が起こっているかを示す「全体像」を把握できるので、可観測性戦略において特に有用な部分です。また、パフォーマンスのボトルネックの特定にも特に有効な方法です。
このチュートリアルでは、OpenTelemetry(OTel)のトレーシングツールキットを使用します。OTelは、テレメトリを収集、処理、エクスポートするためのベンダーニュートラルなオープンソース規格であり、急速に人気が高まってきています。OTelの概念では、トレースは、データフロー(複数のサービスを含むこともある)を時系列に並んだ一連の「チャンク」に分割し、以下のことを簡単に理解できるようにすることとされています。
OTelをご存じない方は、OpenTelemetryとは何か?で、この規格の概要や実装に関する注意事項をご覧ください。
このチュートリアルは、OTelを使用してマイクロサービスアプリの動作をトレースすることに焦点を当てています。このチュートリアルの以下の4つの課題では、システムでやり取りされるリクエストを追跡する方法や、あなたのマイクロサービスに関するあらゆる疑問に答える方法を学習できます。
これらの課題は、初めてトレーシングを設定するときに私たちが推奨している以下のようなプロセスを説明しています。
注:このチュートリアルの目的は、テレメトリに関するいくつかの中心概念を説明することであり、マイクロサービスを本番環境に導入する適切な方法を示すことではありません。実際の「マイクロサービス」アーキテクチャを使用していますが、いくつかの重要な注意事項があります。
以下の図は、チュートリアルで使用されるマイクロサービスやその他の要素間における全体的なアーキテクチャとデータのフローを示しています。
以下の2つのマイクロサービスが使用されます。
サポートインフラストラクチャは以下の3つです。
一旦構成図からOTelを外し、私たちがトレースする一連のイベント、つまり、ユーザーが新しいチャットメッセージを送信し、受信者がそれを通知されたときに何が起こるか、ということについて整理します。
データフローは以下のように分けられます。
以下を同時に実行する。
テレメトリによる計測を設定する場合、「すべての情報を送信してインサイトがあることを期待する」ことから始めるのではなく、より詳細な計測の目的を定義することから始めることをお勧めします。このチュートリアルでは、以下の3つの主要なテレメトリの目的を設定します。
これらの目的は、システムの技術的な操作とユーザーエクスペリエンスの両方に関連していることに注意してください。
チュートリアルをご自身の環境で完了させるためには、以下のことが必要です。
Linux/Unixに対応した環境
注:このチュートリアルにおいて、NGINXのトレーシングに関連するアクティビティは、NGINX用のOpenTelemetryモジュールとの互換性がないため、ARMベースのプロセッサでは動作しません(これには、Linux aarch64アーキテクチャとM1またはM2チップを搭載したAppleマシンが含まれます)。messengerおよびnotifierサービスに関連するアクティビティは、すべてのアーキテクチャで動作します。
bash
に関する基本的な知識(ただし、すべてのコードとコマンドは提供され、説明されているので、限られた知識でも問題ありません)
Node.js 19.x以上
curl
(ほとんどのシステムですでにインストールされています)
注:messengerおよびnotifierサービスはNode.jsで書かれているため、チュートリアルではJavaScript SDKを使用しています。また、OTelから利用できる情報の種類に慣れることができるように、OTel自動計測機能(自動計測とも呼ばれます)を設定します。このチュートリアルでは、OTel Node.js SDKについて知っておくべきことをすべて説明していますが、詳細については、OTelのドキュメントをご覧ください。
ホームディレクトリで、microservices-marchディレクトリを作成して、このチュートリアルのGitHubリポジトリのクローンをその中に作成します(別のディレクトリ名を使用し、それに応じて手順を合わせることもできます)。
注:このチュートリアルでは、ターミナルにコマンドをコピー&ペーストしやすいように、Linuxコマンドラインのプロンプトを省略しています。チルダ(~
)は、実際のホームディレクトリを表します。)
mkdir ~/microservices-marchcd ~/microservices-march
git clone https://github.com/microservices-march/messenger --branch mm23-metrics-start
git clone https://github.com/microservices-march/notifier --branch mm23-metrics-start
git clone https://github.com/microservices-march/platform --branch mm23-metrics-start
この課題では、messengerサービスを開始し、OTel自動計測を設定し、テレメトリをコンソールに送信します。
platformリポジトリに移動し、Docker Composeを起動します。
cd ~/microservices-march/platformdocker compose up -d --build
これにより、以降の課題で使用するRabbitMQとJaegerが起動します。
‑d
フラグを使用すると、Docker Composeはコンテナをデタッチモードで起動します(このフラグを使用しないと、コンテナがターミナルに接続されたままになります)。--build
フラグを使用すると、Docker Composeは起動時にすべてのイメージを再構築します。これにより、実行中のイメージは、ファイルの潜在的な変更が常に反映された状態に保たれます。
messengerリポジトリのappディレクトリに移動し、Node.jsをインストールします(必要に応じて別の方法で代用できます)。
cd ~/microservices-march/messenger/appasdf install
依存関係をインストールします。
npm install
messengerサービス用のPostgreSQLデータベースを起動します。
docker compose up -d
データベーススキーマとテーブルを作成して、いくつかのシードデータを挿入します。
npm run refresh-db
OTel自動計測では、トレーシング設定のためにmessengerのコードベースの何かを変更する必要はありません。すべてのトレーシング設定は、アプリケーションのコード自体にインポートされるのではなく、実行時にNode.jsプロセスにインポートされるスクリプトで定義されます。
ここでは、トレースの送信先として最も基本的なコンソールで、messengerサービスの自動計測を構成します。課題2では、外部コレクターとしてJaegerにトレースを送信するように設定を変更します。
messengerリポジトリのappディレクトリのまま、OTel Node.jsのコアパッケージをインストールします。
npm install @opentelemetry/sdk-node@0.36.0 \ @opentelemetry/auto-instrumentations-node@0.36.4
これらのライブラリは、以下の機能を提供します。
@opentelemetry/sdk-node
– OTelデータの生成とエクスポートを行います。@opentelemetry/auto-instrumentations-node
– 最も一般的なNode.jsを計測するために必要となるデフォルト設定を用いたOTelの自動設定を行います。
注:OTelにおける特質として、JavaScript SDKは非常に細かく分かれています。そのため、このチュートリアルの基本的な例のためだけにも、さらにいくつかのパッケージをインストールすることになります。このチュートリアルでは扱われていないOTelの計測で必要とするパッケージを理解するには、(非常に優れた)OTelの入門ガイドを熟読して、OTelGitHubリポジトリを調べてください。
OTelトレーシング用の設定および構成コードを含むtracing.mjsという新しいファイルを作成します。
touch tracing.mjs
お好みのテキストエディタで、tracing.mjsを開き、以下のコードを追加します。
//1import opentelemetry from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
//2
const sdk = new opentelemetry.NodeSDK({
traceExporter: new opentelemetry.tracing.ConsoleSpanExporter(),
instrumentations: [getNodeAutoInstrumentations()],
});
//3
sdk.start();
このコードは以下のことを実行します。
NodeSDKの新しいインスタンスを作成して、以下のことを実行するように構成する。
ConsoleSpanExporter
).
基本の計測のセットとして自動計測を使用します。この計測は、最も一般的な自動計測ライブラリをすべてロードします。このチュートリアルに関連するものは以下のとおりです。
@opentelemetry/instrumentation-pg
– Postgresデータベースライブラリ (pg
)用@opentelemetry/instrumentation-express
– Node.js Expressフレームワーク用@opentelemetry/instrumentation-amqplib
– RabbitMQのライブラリ(amqplib
)用
ステップ3で作成した自動計測のスクリプトをインポートして、messengerサービスを起動します。
node --import ./tracing.mjs index.mjs
しばらくすると、トレーシングに関する大量の出力がコンソール(ターミナル)に表示されます。
...{
traceId: '9c1801593a9d3b773e5cbd314a8ea89c',
parentId: undefined,
traceState: undefined,
name: 'fs statSync',
id: '2ddf082c1d609fbe',
kind: 0,
timestamp: 1676076410782000,
duration: 3,
attributes: {},
status: { code: 0 },
events: [],
links: []
}
...
注:課題2で再利用するため、ターミナルセッションは開いたままにしておきます。
トレースの表示や解析に使えるツールはたくさんありますが、このチュートリアルではJaegerを使用します。Jaegerはオープンソースで、シンプルなエンドツーエンド分散トレースフレームワークであり、スパンやその他のトレースデータを表示するためのWebベースのユーザーインターフェイスが組み込まれています。プラットフォームリポジトリで提供されるインフラストラクチャには、Jaegerが含まれているので(課題1のステップ1で起動)、複雑なツールに対処する必要なく、データの分析に集中できます。
Jaegerは、ブラウザでhttp://localhost:16686というエンドポイントにアクセスできますが、今エンドポイントにアクセスしても、システムについて何も表示されません。それは、現在収集しているトレースがコンソールに送信されているためです。Jaegerでトレースデータを表示するには、OpenTelemetryプロトコル (OTLP)フォーマットを使用してトレースをエクスポートする必要があります。
この課題では、以下の計測に利用するコンポーネントを設定して、このチュートリアルで中心となるユーザーフローを計測します。
前述のように、OTelの自動計測では、トレーシング設定のためにmessengerのコードベースの何かを変更する必要はありません。代わりに、すべてのトレーシング設定は、実行時にNode.jsプロセスにインポートされるスクリプトで定義されます。ここでは、messengerサービスによって生成されるトレースの送信先をコンソールから外部コレクター(このチュートリアルではJaeger)に変更します。
課題1と同じターミナルのまま、messengerリポジトリのappディレクトリにOTLPエクスポーターNode.jsパッケージをインストールします。
npm install @opentelemetry/exporter-trace-otlp-http@0.36.0
@opentelemetry/exporter-trace-otlp-http
ライブラリは、HTTPを介してOTLPフォーマットでトレース情報をエクスポートします。これはOTelの外部コレクターにテレメトリを送信するときに使用されます。
tracing.mjs(課題1で作成および編集したもの)を開き、以下のように変更します。
この行をファイルの先頭にあるimport
文のセットに追加します。
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
OTel SDKに提供する「エクスポーター」を、課題1で使用したコンソールエクスポーターから、HTTP経由でOTLPデータをOTLP対応コレクターに送信できるものに変更します。以下の行を、
traceExporter:new opentelemetry.tracing.ConsoleSpanExporter(),
以下の行に置き換えます
traceExporter: new OTLPTraceExporter({ headers: {} }),
注:わかりやすいように、このチュートリアルでは、コレクターがデフォルトの場所、http://localhost:4318/v1/tracesにあると仮定しています。実際のシステムでは、場所を明示的に設定することをお勧めします。
Ctrl+c
を押して、messenger サービスを停止します。これは、OTel自動計測の構成でコンソールに計測結果を送信するために、ステップ4のターミナルで開始したものです。その後、再起動すると、ステップ2で構成した新しいエクスポーターが使用されます。
^cnode --import ./tracing.mjs index.mjs
2つ目の別のターミナセッションを開始します(以降の説明では、これをクライアントターミナルと呼び、ステップ1および3で使用した元のターミナルをmessengerターミナルと呼びます)。10秒ほど待ち、messengerサービスにヘルスチェックとなるHTTPリクエストを送信します(複数のトレースを確認したい場合は、コマンドを数回実行できます)。
curl -X GET http://localhost:4000/health
リクエストを送信する前に10秒待つと、サービス開始時に自動計測が生成する多くのトレースの後に表示されるため、トレースを見つけやすくなります。
ブラウザで、Jaeger UI(http://localhost:16686)にアクセスして、OTLPエクスポーターが期待通りに動作していることを確認します。タイトルバーのSearchをクリックして、Serviceフィールドのドロップダウンメニューから、名前がunknown_serviceで始まるサービスを選択します。Find Tracesボタンをクリックします。
ウィンドウの右側でトレースをクリックすると、そのトレース内のスパンのリストが表示されます。各スパンは、トレースの一部として実行された操作(複数のサービスを含む場合もありまます)を記述しています。スクリーンショットのjsonParserスパンは、messengerサービスのリクエスト処理コードのjsonParser
部分の実行に要した時間を示しています。
ステップ5で述べたように、OTel SDKによりエクスポートされるサービス名(unknown_service)が意味をなさないことがあります。これを修正するには、messengerターミナルでCtrl+c
を押して、messengerサービスを停止します。次に、いくつかのNode.jsパッケージをインストールします。
^cnpm install @opentelemetry/semantic-conventions@1.10.0 \
@opentelemetry/resources@1.10.0
これら2つのライブラリは、以下の機能を提供します。
@opentelemetry/semantic-conventions
– OTel仕様で定義されているトレースの標準属性を定義します。@opentelemetry/resources
– OTelデータを生成するソース(このチュートリアルでは、messengerサービス)を表すオブジェクト(resource)を定義します。
テキストエディタでtracing.mjsを開き、以下のように変更します。
以下の行を、ファイルの先頭にあるimport
文のセットに追加します。
import { Resource } from "@opentelemetry/resources";import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
最後のimport
文の後に以下の行を追加して、OTel仕様の正しいキーの下にmessenger
というresource
を作成します。
const resource = new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: "messenger",
});
以下の黒字の行の間にオレンジ色の行を追加して、resource
オブジェクトをNodeSDKコンストラクタに渡します。
const sdk = new opentelemetry.NodeSDK({ resource,
traceExporter: new OTLPTraceExporter({ headers: {} }),
instrumentations: [getNodeAutoInstrumentations()],
});
messengerサービスを再起動します。
node --import ./tracing.mjs index.mjs
10秒ほど待ち、ステップ4で開いたクライアントターミナルで、もう一度サーバーにヘルスチェックリクエストを送信します(複数のトレースを確認したい場合は、コマンドを数回実行できます)。
curl -X GET http://localhost:4000/health
注:クライアントターミナルは次のセクションで再利用し、messengerターミナルは課題3で再利用するため、開いたままにしておきます。
ブラウザで、Jaeger UIにmessengerという新しいサービスが表示されていることを確認します(数秒かかる場合があり、Jaeger UIの更新が必要な場合があります)。
Serviceドロップダウンメニューからmessengerを選択し、Find Tracesボタンをクリックすると、messengerサービスから発信された最近のすべてのトレースが表示されます(スクリーンショットは、20のうち最新の2つを示しています)。
トレースをクリックすると、そのトレース内のスパンが表示されます。各スパンは、messengerサービスから発信されたものとして適切にタグ付けされています。
ここでは、notifierサービスで利用する自動計測の起動および構成を行います。基本的に、前の2つのセクションでmessengerサービスに関する手順で実行したコマンドと同じコマンドを実行します。
新しいターミナルセッションを開きます(以降のステップでは、これをnotifierターミナルと呼びます)。notifierリポジトリのappディレクトリに移動して、Node.jsをインストールします(必要に応じて別の方法で代用可能です)。
cd ~/microservices-march/notifier/appasdf install
依存関係をインストールします。
npm install
notifierサービス用のPostgreSQLデータベースを起動します。
docker compose up -d
データベーススキーマとテーブルを作成して、いくつかのシードデータを挿入します。
npm run refresh-db
OTel Node.jsパッケージをインストールします(パッケージについては、コンソールに送信されるOTel自動計測の構成のステップ1および3をご覧ください)。
npm install @opentelemetry/auto-instrumentations-node@0.36.4 \ @opentelemetry/exporter-trace-otlp-http@0.36.0 \
@opentelemetry/resources@1.10.0 \
@opentelemetry/sdk-node@0.36.0 \
@opentelemetry/semantic-conventions@1.10.0
tracing.mjsという新しいファイルを作成します。
touch tracing.mjs
お好みのテキストエディタで、tracing.mjsを開き、以下のスクリプトを追加して、OTel SDKを起動します。
import opentelemetry from "@opentelemetry/sdk-node";import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: "notifier",
});
const sdk = new opentelemetry.NodeSDK({
resource,
traceExporter: new OTLPTraceExporter({ headers: {} }),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
注:このスクリプトは、SemanticResourceAttributes.SERVICE_NAME
フィールドの値がnotifier
であることを除き、messengerサービスのものと同じです。
OTel自動計測でnotifierサービスを開始します。
node --import ./tracing.mjs index.mjs
10秒ほど待ち、クライアントターミナルで、notifierサービスにヘルスチェックのリクエストを送信します。ポート4000でリスニングしているmessengerサービスと競合をさけるため、このサービスはポート5000で通信を待ち受けています。
curl http://localhost:5000/health
注:課題3で再利用するため、クライアントおよびnotifierターミナルは開いたままにしておきます。
ブラウザで、Jaeger UIにnotifierという新しいサービスが表示されていることを確認します。
NGINXの場合、OTel自動計測方法を使わず、手動でトレーシングを設定します。現在、OTelを使用してNGINXを計測する最も一般的な方法は、C言語で書かれたモジュールを使用することです。サードパーティモジュールは、NGINXエコシステムの重要な部分ですが、いくつかの設定作業が必要になります。このチュートリアルでは、設定について紹介します。このモジュールの背景となる情報については、当社ブログのCompiling Third‑Party Dynamic Modules for NGINX and NGINX Plus(NGINXおよびNGINX Plus用のサードパーティ動的モジュールのコンパイル)をご覧ください。
新しいターミナルセッション(NGINX ターミナル)を起動し、ディレクトリをmessengerリポジトリのルートに変更して、load-balancerという新しいディレクトリと、Dockerfile、nginx.conf、opentelemetry_module.confという新しいファイルを作成します。
cd ~/microservices-march/messenger/mkdir load-balancer
cd load-balancer
touch Dockerfile
touch nginx.conf
touch opentelemetry_module.conf
お好みのテキストエディタでDockerfileを開き、以下の行を追加します(コメントで各行の機能について説明していますが、すべてを理解しなくてもDockerコンテナを構築および実行できます)。
FROM --platform=amd64 nginx:1.23.1
# Replace the nginx.conf file with our own
COPY nginx.conf /etc/nginx/nginx.conf
# Define the version of the NGINX OTel module
ARG OPENTELEMETRY_CPP_VERSION=1.0.3
# Define the search path for shared libraries used when compiling and running NGINX
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/opentelemetry-webserver-sdk/sdk_lib/lib
# 1. Download the latest version of Consul template and the OTel C++ web server module, otel-webserver-module
ADD https://github.com/open-telemetry/opentelemetry-cpp-contrib/releases/download/webserver%2Fv${OPENTELEMETRY_CPP_VERSION}/opentelemetry-webserver-sdk-x64-linux.tgz /tmp
RUN apt-get update \
&& apt-get install -y --no-install-recommends dumb-init unzip \
# 2. Extract the module files
&& tar xvfz /tmp/opentelemetry-webserver-sdk-x64-linux.tgz -C /opt \
&& rm -rf /tmp/opentelemetry-webserver-sdk-x64-linux.tgz \
# 3. Install and add the 'load_module' directive at the top of the main NGINX configuration file
&& /opt/opentelemetry-webserver-sdk/install.sh \
&& echo "load_module /opt/opentelemetry-webserver-sdk/WebServerModule/Nginx/1.23.1/ngx_http_opentelemetry_module.so;\n$(cat /etc/nginx/nginx.conf)" > /etc/nginx/nginx.conf
# 4. Copy in the configuration file for the NGINX OTel module
COPY opentelemetry_module.conf /etc/nginx/conf.d/opentelemetry_module.conf
EXPOSE 8085
STOPSIGNAL SIGQUIT
nginx.confを開き、以下の行を追加します。
events {}http {
include /etc/nginx/conf.d/opentelemetry_module.conf;
upstream messenger {
server localhost:4000;
}
server {
listen 8085;
location / {
proxy_pass http://messenger;
}
}
}
この極めて基本的なNGINX構成ファイルは、NGINXに次のことを指示します。
注:これは、本番環境におけるリバースプロキシおよびロードバランサーとしてのNGINXの実際の構成にかなり近いものです。唯一の大きな違いは、upstream
ブロックのserver
の引数が、通常localhost
ではなくドメイン名またはIPアドレスであることです。
opentelemetry_module.confを開き、以下の行を追加します。
NginxModuleEnabled ON;NginxModuleOtelSpanExporter otlp;
NginxModuleOtelExporterEndpoint localhost:4317;
NginxModuleServiceName messenger-lb;
NginxModuleServiceNamespace MicroservicesMarchDemoArchitecture;
NginxModuleServiceInstanceId DemoInstanceId;
NginxModuleResolveBackends ON;
NginxModuleTraceAsError ON;
NGINXとNGINX OTelモジュールを含むDockerイメージをビルドします。
docker build -t messenger-lb .
NGINXのリバースプロキシとロードバランサーのDockerコンテナを起動します。
docker run --rm --name messenger-lb -p 8085:8085 --network="host" messenger-lb
クライアントターミナルで、NGINXのリバースプロキシとロードバランサーを介して、messengerサービスにヘルスチェックリクエストを送信します(このリクエストを送信する前に待つ必要はありません)。
curl http://localhost:8085/health
注:課題3で再利用するため、NGINXおよびクライアントターミナルは開いたままにしておきます。
ブラウザで、Jaeger UIに、起動したサービスとともに、新しいmessenger-lbサービスが表示されていることを確認します。ブラウザでJaeger UIの再読み込みが必要になる場合があります。
アーキテクチャとユーザーフローでは、ユーザーフローの処理について概説しましたが、改めて以下に示します。
テレメトリを導入する目的は、以下のとおりです。
この課題では、OTelの計測によって生成されるトレースが前述の目的を満たすかどうかを評価する方法を学びます。まず、システムを稼働させ、いくつかのトレースを作成します。次に、メッセージフローと、NGINX、messengerサービス、notifierサービスによって生成されたそのセクションのトレースを検査します。
クライアントターミナルで、2人のユーザー間で会話を設定し、いくつかのメッセージを送信します。
curl -X POST \ -H "Content-Type: application/json" \
-d '{"participant_ids": [1, 2]}' \
'http://localhost:8085/conversations'
curl -X POST \
-H "User-Id: 1" \
-H "Content-Type: application/json" \
-d '{"content": "This is the first message"}' \
'http://localhost:8085/conversations/1/messages'
curl -X POST \
-H "User-Id: 2" \
-H "Content-Type: application/json" \
-d '{"content": "This is the second message"}' \
'http://localhost:8085/conversations/1/messages'
以下のような出力がnotifierサービスによって生成され、notifierターミナルに表示されます。
Received new_message: {"type":"new_message","channel_id":1,"user_id":1,"index":1,"participant_ids":[1,2]}Sending notification of new message via sms to 12027621401
Received new_message: {"type":"new_message","channel_id":1,"user_id":2,"index":2,"participant_ids":[1,2]}
Sending notification of new message via email to the_hotstepper@kamo.ze
Sending notification of new message via sms to 19147379938
ブラウザでJaeger UIを開き、Serviceドロップダウンメニューからmessenger-lbを選択して、Find Tracesボタンをクリックします。フローの始まりの瞬間からのトレースのリストが表示されます。このスクリーンショットのように、任意のトレースをクリックすると、そのトレースの詳細が表示されます。
いろいろなトレースをクリックして、少し調べてみてください。次に進む前に、課題3の冒頭に挙げた計測の目的にトレースの情報がどのように役立っているかを考えてみてください。適切な質問は以下のとおりです。
親スパンの中に11の子スパンのあるNGINXスパンから始めます。現在のNGINXの構成は非常にシンプルなので、子スパンはそれほど興味深いものではなく、NGINXリクエスト処理のライフサイクルの各ステップにかかる時間を示しているだけです。しかし、親スパン(一番最初のスパン)には興味深いインサイトが含まれています。
Tagsの下の以下の属性に注目してください。
http.method
フィールド – POST
(RESTの定義では、作成を意味します)http.status_code
フィールド – 201
(作成に成功したことを示します)http.target
フィールド – conversations/1/messages
(メッセージエンドポイント)
これら3つの情報を組み合わると、次のような意味になります。「POST
リクエストが、/conversations/1/messages
に送信され、レスポンスは201
(作成に成功)でした。」これは、アーキテクチャとユーザーフローのステップ1および4aに相当します。
webengine.name
フィールドは、これがリクエストのNGINX部分であることを示しています。さらに、messengerとnotifierのスパンは、messenger-lb conversations/1
のスパンの中に入れ子になっているので(トレースを読み取る準備のスクリーンショットに示すように)、NGINXのリバースプロキシを介してmessengerサービスに送信されたリクエストが、フロー内の期待されるすべてのコンポーネントにヒットしたことがわかります。
この情報は、NGINXのリバースプロキシがフローの一部であったことがわかるので、目的を満たしています。
messenger-lbと表示されたスパンのリストで、最新(リストの一番下)のスパンを見て、リクエストのNGINX部分にかかった時間を確認します。スクリーンショットでは、スパンは589マイクロ秒(µs)から始まり、24µs続いています。つまり、完全なリバースプロキシ操作には613µs(約0.6ミリ秒(ms))しかかかっていません(正確な値は、チュートリアルを実際に実行したときには異なります)。
このような設定では、ほとんどの場合、値は他の測定値との相対的な関係でしか役に立ちませんし、システム間でも異なります。しかしこの場合、この操作が5秒に近づく危険性は明らかにありません。
この情報は、NGINXによる操作が5秒近くはかかっていないことがわかるので、目的を満たしています。フロー内で非常に遅い操作があれば、それは後で起こっているはずです。
NGINXのリバースプロキシ層には、これに関する情報は含まれていないので、messengerスパンに移動してください。
トレースのmessengerサービスセクションには、さらに11のスパンが含まれています。ここでも子スパンのほとんどは、リクエストを処理するときにExpressフレームワークが使用する基本的なステップに関するもので、あまり興味深いものではありません。しかし親スパン(一番最初のスパン)には、興味深いインサイトが含まれています。
Tagsの下の以下の属性に注目してください。
http.method
フィールド – POST
(RESTの定義では、作成を意味します)http.route
フィールド – /conversations/:conversationId/messages
(メッセージルート)http.target
フィールド – /conversations/1/messages
(メッセージエンドポイント)この情報は、messengerサービスがフローの一部であり、ヒットしたエンドポイントが新しいメッセージのエンドポイントであることを示すので、目的を満たしています。
以下のスクリーンショットに示すように、トレースのmessenger部分は、1.28msで始まり、36.28msで終わっていて、全体の時間は35msです。この時間のほとんどは、JSONの解析(middleware
-
jsonParser
)と、それ以上にデータベースへの接続(pg-pool.connect
とtcp.connect
)に費やされています。
メッセージを書く過程でいくつかのSQLクエリが作成されることを考えると、これは理にかなっています。つまり、これらのクエリのタイミングをとらえるためにはOTelの自動計測の構成を強化することが必要であると示唆されます(チュートリアルでは、この追加の計測について示しませんが、課題4では、データベースクエリをラップするために使用できるスパンを手動で作成します)。
この情報は、messenger操作が5秒近くはかかっていないことがわかるので、目的を満たしています。フロー内で非常に遅い操作があれば、それは後で起こっているはずです。
NGINXスパン同様、messengerスパンにはこの情報は含まれていないので、notifierスパンに移動します。
トレースのnotifierセクションには、2つのスパンしか含まれていません。
chat_queue
process
スパン – notifierサービスがchat_queueメッセージキューのイベントを処理したことを確認します。pg-pool.connect
スパン – イベントを処理した後、notifierサービスがそのデータベースに何らかの接続を行ったことを示します。これらのスパンから得られる情報は、すべてのステップを理解するという目的を部分的に満たしているだけです。notifierサービスがキューのイベントを処理するところまで到達したことはわかりますが、以下のことはわかりません。
つまり、notifierサービスフローを完全に理解するためには、次のことを行う必要があります。
notifierサービスのスパンの全体的なタイミングを見ると、リクエストは、フローのnotifierセクションで30.77msを費やしたことがわかります。しかし、フロー全体の「終わり」(受信者への通知の送信)を示すスパンがないため、このセクションのフローの合計タイミング、または操作の全体完了時間を判断することはできません。
しかし、messengerサービスのchat_queue
send
スパンが4.12msで開始された2ms後に、notifierサービスのchat_queue
process
スパンが6.12msで開始されていることを確認できます。
messengerサービスからイベントがディスパッチされた2ms後に、notifierがイベントを処理したことがわかるので、この目的は満たされています。目的2とは異なり、この目的を達成するためにイベントが完全に処理されたかどうか、またはそれに要した時間を知る必要はありません。
現在のOTel自動計測により生成されるトレースを分析した結果、以下のことが明らかになりました。
現在の形では以下に関連するスパンの多くは役に立つとは言えません
request
handler
、すべてのデータベース操作のスパンは、目的に関連していると思われます。middlewareスパン(expressInit
やcorsMiddleware
など)の中には、関連性がないと思われるものもあり、これらは削除できます。
以下のような重要なスパンが記録できていません
つまり基本の計測構成では、以下の最後の目的を満たすことがわかります。
しかし、以下の最初の2つの目的を満たすための情報は十分にはありません。
この課題では、課題3で行ったトレース分析に基づいて、OTelの計測を最適化します。これには、不要なスパンの削除、新しいカスタムスパンの作成およびnotifierサービスによって消費されるイベントがmessengerサービスによって生成されるものであることの確認が含まれます。
お好みのテキストエディタで、messengerリポジトリのappディレクトリにあるtracing.mjsファイルを開き、先頭部分にあるimport
文のリストの最後に以下の行を追加します。
const IGNORED_EXPRESS_SPANS = new Set([ "middleware - expressInit",
"middleware - corsMiddleware",
]);
これは、Jaeger UIの以下のスクリーンショットに示されたスパンのリストから、このフローに有用な情報を提供しないためトレースから省略されるスパン名のセットを定義します。スクリーンショットに表示されている他のスパンも不要だと判断した場合、IGNORED_EXPRESS_SPANS
のリストに追加できます。
自動計測の構成にフィルターを追加し、以下のオレンジ色でハイライトされている行をその下のように変更して、不要なスパンを省略します。
const sdk = new opentelemetry.NodeSDK({ resource,
traceExporter: new OTLPTraceExporter({ headers: {} }),
instrumentations: [getNodeAutoInstrumentations()],
});
変更後:
const sdk = new opentelemetry.NodeSDK({ resource,
traceExporter: new OTLPTraceExporter({ headers: {} }),
instrumentations: [
getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-express": {
ignoreLayers: [
(name) => {
return IGNORED_EXPRESS_SPANS.has(name);
},
],
},
}),
],
});
getNodeAutoInstrumentations
関数は、ステップ1で定義したスパンのセットを参照し、@opentelemetry/instrumentation-express
が生成するトレースからスパンを除外します。つまり、IGNORED_EXPRESS_SPANS
に属するスパンに対してreturn
文がtrue
と判定して、ignoreLayers
文がトレースからそのスパンを削除します。
messengerターミナルで、Ctrl+c
を押して、messengerサービスを停止します。
^cnode --import ./tracing.mjs index.mjs
10秒ほど待って、クライアントターミナルで新しいメッセージを送信します。
curl -X POST \ -H "User-Id: 2" \
-H "Content-Type: application/json" \
-d '{"content": "This is the second message"}' \
'http://localhost:8085/conversations/1/messages'
Jaeger UIでmessengerスパンを再確認します。expressInit
とcorsMiddleware
という2つのmiddleware
スパンが表示されなくなりました(課題3トレースのmessengerセクションの検証の目的2のスクリーンショットと比較してみてください)
このセクションで、初めてアプリケーションコードに触れます。自動計測は、アプリケーションを変更することなく大量の情報を生成しますが、ビジネスロジックの特定の部分を計測して初めてわかるインサイトもあります。
計測する新しいメッセージフローでは、メッセージの受信者に対する通知の送信をトレースすることがその例です。
notifierリポジトリのappディレクトリにあるindex.mjs開きます。このファイルには、サービスのすべてのビジネスロジックが含まれています。ファイルの先頭部分にあるimport
文のリストの最後に、次の行を追加します
import { trace } from "@opentelemetry/api";
このコード(ファイルの91行目あたり)を、
for (let pref of preferences) { console.log(
`Sending notification of new message via ${pref.address_type} to ${pref.address}`
);
}
から以下の内容に変更します
const tracer = trace.getTracer("notifier"); // 1tracer.startActiveSpan( // 2
"notification.send_all",
{
attributes: {
user_id: msg.user_id,
},
},
(parentSpan) => {
for (let pref of preferences) {
tracer.startActiveSpan( // 3
"notification.send",
{
attributes: { // 4
notification_type: pref.address_type,
user_id: pref.user_id,
},
},
(span) => {
console.log(
`Sending notification of new message via ${pref.address_type} to ${pref.address}`
);
span.end(); // 5
}
);
}
parentSpan.end(); // 6
}
);
この新しいコードは以下のことを実行します。
tracer
を取得する。notification.send_all
という新しい親スパンを開始し、メッセージの送信者を特定するためにuser_id
属性を設定する。notification.send_all
の下にnotification.send
という新しい子スパンが作成される。すべての通知は、新しいスパンを生成します。
子スパンの属性をさらに設定する。
notification_type
– sms
またはemail
のいずれかuser_id
– 通知を受け取るユーザーのIDnotification.send
スパンを順番に閉じる。notification.send_all
スパンを閉じる。
親スパンを持つことで、ユーザーの通知設定が検出されない場合でも、各「通知送信」操作が報告されることが保証されます。
notifierターミナルで、Ctrl+c
を押して、notifierサービスを停止します。その後、再起動します。
^cnode --import ./tracing.mjs index.mjs
10秒ほど待って、クライアントターミナルで新しいメッセージを送信します。
curl -X POST \ -H "User-Id: 2" \
-H "Content-Type: application/json" \
-d '{"content": "This is the second message"}' \
'http://localhost:8085/conversations/1/messages'
Jaeger UIでnotifierスパンを再確認します。親スパンと2つの子スパンが表示され、それぞれ「通知送信」操作を行っていることがわかります。
新しいメッセージフローにおいてリクエストが通過するすべてのステップを確認できるので、これで最初と2番目の目的を完全に満たすことができます。各スパンのタイミングにより、これらのステップ間のラグが明らかになります。
フローを完全に把握するために必要なことがもう1つあります。それは、notifierサービスによって処理されているイベントが、実際にmessengerサービスによってディスパッチされたものかどうか、ということです。
2つのトレースを接続するために明示的な変更は必要ありませんが、自動計測の魔法をそのまま信じるわけにもいきません。
これを踏まえ、NGINXサービスで開始されるトレースが、本当にnotifierサービスで消費されるトレースと同じである(同じトレースIDを持つ)ことを確認するために、いくつかの簡単なデバッグコードを追加します。
messengerリポジトリのappディレクトリにあるindex.mjsファイルを開き、以下のように変更します。
先頭部分にあるimport文のリストの最後に、次の行を追加します。
import { trace } from "@opentelemetry/api";
オレンジ色でハイライトされた行を、黒色の既存の行の下に追加します。
async function createMessageInConversation(req, res) { const tracer = trace.getActiveSpan();
console.log("TRACE_ID: ", tracer.spanContext().traceId);
新しい行は、新しいメッセージの作成を処理するmessengerの関数の内部からTRACE_ID
を出力します。
notifierリポジトリのappディレクトリにあるindex.mjsファイルを開き、オレンジ色でハイライトされた行を既存の黒色の行の下に追加します。
export async function handleMessageConsume(channel, msg, handlers) { console.log("RABBIT_MQ_MESSAGE: ", msg);
新しい行は、notifierサービスにより受信されるAMQP(RabbitMQが利用するプロトコル。Advanced Message Queuing Protocol)イベントのすべての内容を表示します。
messengerとnotifierの両方のターミナルで以下のコマンドを実行し、messengerとnotifierのサービスを停止して再起動します。
^cnode --import ./tracing.mjs index.mjs
10秒ほど待って、クライアントターミナルでメッセージを再送信します。
curl -X POST \ -H "User-Id: 2" \
-H "Content-Type: application/json" \
-d '{"content": "This is the second message"}' \
'http://localhost:8085/conversations/1/messages'
messengerとnotifierのサービスのログを確認します。messengerサービスのログには、メッセージのトレースIDを報告する以下のような行が含まれています(実際のIDは、チュートリアルを実行するときには異なります)。
TRACE_ID: 29377a9b546c50be629c8e64409bbfb5
同様に、notifierサービスのログでは、以下のような出力でトレースIDが報告されます。
_spanContext: { traceId: '29377a9b546c50be629c8e64409bbfb5',
spanId: 'a94e9462a39e6dbf',
traceFlags: 1,
traceState: undefined
},
コンソールではトレースIDが一致していますが、最後のステップとして、Jaeger UIでトレースIDと比較してみてください。関連するトレースIDのエンドポイント(実際には異なりますが、この例ではhttp://localhost:16686/trace/29377a9b546c50be629c8e64409bbfb5)でUIを開き、トレース全体を確認します。Jaegerのトレースで以下のことを確認できます。
注:実際の本番システムでは、フローが期待通りに動作していることを確認したら、このセクションで追加したコードを削除することになります。
このチュートリアルでは、いくつかのコンテナやイメージを作成しました。これらを以下の手順で削除します。
実行中のDockerコンテナを削除する
docker rm $(docker stop messenger-lb)
platformサービス、およびmessengerとnotifierのデータベースサービスを削除する。
cd ~/microservices-march/platform && docker compose downcd ~/microservices-march/notifier && docker compose down
cd ~/microservices-march/messenger && docker compose down
おめでとうございます。これでチュートリアルは終了です。
しかしまだ、理想的なトレーシング構成を深くは掘り下げていません。本番環境では、各データベースクエリのカスタムスパンや、各サービスのコンテナIDなどの実行時の詳細を記述するすべてのスパンの追加メタデータのようなものを追加する必要があるかもしれません。また、システムの健全性を完全に把握するために、他の2種類のOTelデータ(メトリクスとロギング)を実装することもできます。
マイクロサービスの学習を続けるには、「Microservices July 2023」をチェックしてください。Unit 4: マイクロサービスの複雑性をObservabilityで管理するでは、可観測性データの3つの主要クラス、インフラストラクチャとアプリの整合性の重要性、深いデータの分析を開始する方法について学ぶことができます。
"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."