NGINXチュートリアル:OpenTelemetryトレーシングでマイクロサービスを把握する方法

NGINX | 2023年3月28日

この記事は、「Microservices March 2023: マイクロサービスの提供を始める」の概念を実践するための4つのチュートリアルのうちの1つです:

マイクロサービスアーキテクチャには、チームの自律性の向上や、スケーリングとデプロイの柔軟性の向上など、多くのメリットがあります。一方で、システム内のサービスが増えるほど(マイクロサービスアプリには数十、場合によっては数百のサービスが含まれます)、システム全体の稼働状況を明確に把握し続けることが難しくなります。複雑なソフトウェアシステムの開発と保守に携わる者として、全体像を明確に把握することが極めて重要であると認識しています。オブザーバビリティツールを使用すると、多数のサービスとそれを支えるインフラストラクチャ全体の状況を可視化できます。

このチュートリアルでは、マイクロサービスアプリにとって非常に重要なオブザーバビリティの一種であるトレーシングについて取り上げます。まず、オブザーバビリティについて説明する際によく使われる用語を定義します。

  • オブザーバビリティ – 複雑なシステム(マイクロサービスアプリなど)の外部出力(トレース、ログ、メトリクスなど)に関する情報だけに基づいて、その内部状態や状況を把握できる能力。
  • モニタリング – 対象の進捗や状態を一定期間にわたって観察し、確認する機能です。たとえば、ピーク時にアプリに流入するトラフィックを監視し、その情報に基づいてアプリを適切にスケーリングできます。
  • テレメトリ – メトリクス、トレース、ログを収集し、発生元から別のシステムへ転送して保存および分析すること。また、そのデータ自体も指します。
  • トレーシング/トレース – 分散システムのすべてのノードを通過するリクエストまたはアクションの経路を記録したもの。
  • スパン – トレース内に記録される、オペレーションとそれに関連するメタデータです。トレースは、入れ子になった多数のスパンで構成されます。
  • イベントロギング/ログ – メタデータを含む、タイムスタンプ付きのテキストレコード。
  • メトリック – 実行時に取得される測定値。たとえば、ある時点でアプリケーションが使用しているメモリ量などです。

これらの概念をすべて活用することで、マイクロサービスのパフォーマンスに関する知見を得られます。トレースを使用すると、リクエストが行われた際に、疎結合であることも多い複数のコンポーネントにまたがって何が起きているかを全体的に把握できるため、トレーシングはオブザーバビリティ戦略において特に有用です。また、パフォーマンスのボトルネックを特定するうえでも特に効果的です。

このチュートリアルでは、OpenTelemetry(OTel)のトレーシングツールキットを使用します。OTelは、テレメトリの収集、処理、エクスポートに使用される、急速に普及しているオープンソースのベンダーニュートラルな標準です。OTelでは、トレースによって、複数のサービスにまたがる場合もあるデータフローを、時系列に並べた一連の「チャンク」に分割します。これにより、以下の点を容易に把握できます。

  • 「チャンク」内で実行されたすべてのステップ
  • これらすべてのステップにかかった時間
  • 各ステップのメタデータ

OTelについて詳しくない場合は、標準の概要と実装時の考慮事項を詳しく説明した「OpenTelemetryとは」をご覧ください。

チュートリアルの概要

このチュートリアルでは、OTelを使用してマイクロサービスアプリの動作をトレースする方法を取り上げます。4つの課題を通じて、システム内でリクエストを追跡し、マイクロサービスに関する疑問を解決する方法を学びます。

これらの課題を踏まえ、トレーシングを初めて設定する際は、次の手順を推奨します。

  1. システム全体と、計測対象となる個々の処理を把握します。
  2. 稼働中のシステムから把握する必要がある情報を決定します。
  3. システムを「素朴に」計装します。つまり、不要な情報を除外したり、カスタムデータポイントを収集したりせずにデフォルト設定を使用し、その計装が疑問の解決に役立つかどうかを評価します。
  4. 報告される情報を調整し、それらの疑問により迅速に答えられるようにします。

注:このチュートリアルの目的は、テレメトリに関するいくつかの基本概念を説明することであり、本番環境にマイクロサービスをデプロイする適切な方法を示すことではありません。実際の「マイクロサービス」アーキテクチャを使用していますが、いくつかの重要な注意点があります。

  • このチュートリアルでは、KubernetesやNomadなどのコンテナオーケストレーションフレームワークは使用しません。これは、特定のフレームワークの詳細にとらわれずに、マイクロサービスの概念を学べるようにするためです。ここで紹介するパターンは、これらのフレームワークのいずれかを実行するシステムにも適用できます。
  • これらのサービスは、ソフトウェアエンジニアリング上の厳密さよりも、理解しやすさを重視して最適化されています。ここで重要なのは、コードの詳細ではなく、システム内で各サービスが果たす役割と通信パターンを確認することです。詳細については、各サービスの「README」ファイルを参照してください。

チュートリアルのアーキテクチャとテレメトリの目標

アーキテクチャとユーザーフロー

この図は、チュートリアルで使用するマイクロサービスやその他の要素の全体的なアーキテクチャと、それらの間のデータフローを示しています。

チュートリアルで使用するトポロジーを示す図。2つのマイクロサービス、NGINX、RabbitMQで構成されるメッセージングシステムをOpenTelemetryでトレースしています

2つのマイクロサービスは次のとおりです。

  • The messengerサービス – メッセージ保存機能を備えたシンプルなチャットAPI
  • The notifierサービス – ユーザーの設定に基づいてイベントをトリガーし、ユーザーに通知するリスナー

サポートインフラストラクチャは、次の3つで構成されます。

  • NGINX Open Source – messengerサービスおよびシステム全体へのエントリポイント
  • RabbitMQ – サービス間の非同期通信を可能にする、広く利用されているオープンソースのメッセージブローカー
  • Jaeger – システム内の各コンポーネントが生成するテレメトリを収集して可視化する、オープンソースのエンドツーエンド分散トレーシングシステム

ここではいったんOTelを考慮せず、トレース対象となる一連のイベント、つまりユーザーが新しいチャットメッセージを送信してから受信者に通知されるまでの流れに注目します。

チュートリアルで使用するメッセージングシステム内の情報の流れを示す図

フローの内訳は次のとおりです。

  1. ユーザーがmessengerサービスにメッセージを送信します。NGINリバースプロキシがそのメッセージをインターセプトし、複数あるmessengerサービスのインスタンスのいずれかに転送します。
  2. The messengerサービスは、新しいメッセージをデータベースに書き込みます。
  3. The messengerサービスは、メッセージが送信されたことを示すイベントを、RabbitMQのメッセージキュー「chat_queue」に生成します。このイベントは汎用的なもので、特定の送信先はありません。
  4. 同時に:
    • 4a. messengerサービスは、メッセージが正常に送信されたことを示す応答を送信者に返します。
    • 4b. notifierサービスは、chat_queue上の新しいイベントを検知して処理します。
  5. The notifierサービスは、新しいメッセージの受信者の通知設定をデータベースで確認します。
  6. The notifierサービスは、受信者が希望する方法を使用して、1件以上の通知を送信します(このチュートリアルで選択できる方法はSMSとメールです)。

テレメトリの目標

テレメトリ計測を設定する際は、「すべてを送信して、何らかの知見が得られることを期待する」という漠然とした方針ではなく、より明確な計測目標を定めることから始めるのが最善です。このチュートリアルでは、テレメトリに関する3つの主要な目標を設定します。

  1. 新しいメッセージフローでリクエストがたどるすべてのステップを把握する
  2. 通常の条件下で、フローがエンドツーエンドで5秒以内に実行されていることを確信できる
  3. See how long it takes the notifierサービスが、messengerサービスから送信されたイベントの処理を開始するまでにかかる時間を確認します(遅延が長すぎる場合、notifierサービスでイベントキューの読み取りに問題が発生し、イベントが滞留している可能性があります)

これらの目標は、システムの技術的な運用とユーザーエクスペリエンスの両方に関係している点に注目してください。

チュートリアルの前提条件とセットアップ

前提条件

ご自身の環境でチュートリアルを完了するには、以下が必要です。

  • Linux/Unix互換環境注:このチュートリアルでNGINXのトレースを行う演習は、NGINX用OpenTelemetryモジュールが対応していないため、ARMベースのプロセッサでは動作しません。(Linux aarch64アーキテクチャ、およびM1またはM2チップを搭載したApple製マシンが該当します。)messengerおよびnotifierサービスを使用する演習は、すべてのアーキテクチャで動作します。
  • Linuxコマンドライン、JavaScript、bashに関する基本的な知識(ただし、すべてのコードとコマンドが提示され、説明も付いているため、知識が限られていても進められます)
  • DockerとDocker Compose
  • Node.js 19.x以降
    • バージョン19.xでテスト済みですが、それより新しいバージョンのNode.jsでも動作すると見込まれます。
    • Node.jsのインストールに関する詳細は、README(messengerサービスのリポジトリ内)を参照してください。また、asdfをインストールすると、チュートリアルで使用しているものとまったく同じバージョンのNode.jsを利用できます。
  • curl(ほとんどのシステムにインストール済み)
  • 以下の技術が記載されています:アーキテクチャとユーザーフロー:messenger、notifier(次のセクションでダウンロードします)、NGINX Open Source、Jaeger、RabbitMQ。

注: このチュートリアルでは、messengerサービスとnotifierサービスがNode.jsで記述されているため、JavaScript SDKを使用します。また、OTelから取得できる情報の種類を把握できるように、OTelの自動インストルメンテーション機能(auto‑instrumentationとも呼ばれます)も設定します。このチュートリアルでは、OTel Node.js SDKについて必要な情報をすべて説明します。詳細については、OTelドキュメントを参照してください。

セットアップ

  1. ターミナルセッションを開始します。
  2. ホームディレクトリに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

課題1:基本的なOTel計装を設定する

このチャレンジでは、start the messengerサービスを起動し、OTel自動インストルメンテーションを構成して、テレメトリをコンソールに送信します。

messengerサービスの起動

  1. Change to the platformリポジトリに移動し、Docker Composeを起動します:
cd ~/microservices-march/platformdocker compose up -d --build
  1. これにより、以降の課題で使用するRabbitMQとJaegerが起動します。
    • The ‑dフラグを指定すると、コンテナの起動後にDocker Composeがコンテナからデタッチします(指定しない場合、コンテナはターミナルにアタッチされたままになります)。
    • The --buildフラグを指定すると、Docker Composeは起動時にすべてのイメージを再ビルドします。これにより、ファイルに変更が加えられた場合でも、実行するイメージを最新の状態に保つことができます。
  2. Change to the messengerリポジトリのappディレクトリに移動し、Node.jsをインストールします(必要に応じて別の方法を使用しても構いません):
cd ~/microservices-march/messenger/appasdf install
  1. 依存関係をインストールします:
npm install
  1. Start the PostgreSQL database for the messengerサービス用のPostgreSQLデータベースを起動します:
docker compose up -d
  1. データベースのスキーマとテーブルを作成し、初期データを挿入します:
npm run refresh-db

コンソールに送信するOTel自動インストルメンテーションの設定

OTel自動インストルメンテーションを使用すると、トレーシングを設定するためにmessengerのコードベースを変更する必要はありません。トレーシングの設定はアプリケーションコード自体にインポートするのではなく、実行時にNode.jsプロセスへインポートされるスクリプト内ですべて定義します。

ここでは、messengerサービスの自動インストルメンテーションを設定し、トレースの最も基本的な送信先としてコンソールを指定します。Challenge 2では、外部コレクターとしてJaegerにトレースを送信するように設定を変更します。

  1. 引き続きappディレクトリ(messengerリポジトリ内)で、OTel Node.jsのコアパッケージをインストールします:
npm install @opentelemetry/sdk-node@0.36.0 \ @opentelemetry/auto-instrumentations-node@0.36.4
  1. これらのライブラリには、次の機能があります。注: OTelのJavaScript SDKは非常に細かく分割されているため、このチュートリアルの基本的な例だけでも、いくつか追加のパッケージをインストールする必要があります。このチュートリアルで扱う範囲を超える計装タスクに必要なパッケージを確認するには、OTelの充実した「スタートガイド」を参照し、OTelの「GitHubリポジトリ」をご覧ください。
    • @opentelemetry/sdk-node – OTelデータの生成とエクスポート
    • @opentelemetry/auto-instrumentations-node – 最も一般的なすべてのNode.js計装をデフォルト構成で自動セットアップ
  2. OTelトレーシングのセットアップおよび構成コードを格納するために、tracing.mjsという名前の新しいファイルを作成します:
touch tracing.mjs
  1. 任意のテキストエディターでtracing.mjsを開き、次のコードを追加します。
//1
import 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();
  1. このコードは次の処理を行います:
    1. OTel SDKから必要な関数とオブジェクトをインポートします。
    2. NodeSDKの新しいインスタンスを作成し、次のように構成します。
      • スパンをコンソールに送信します(ConsoleSpanExporter)。
      • 計装の基本セットとしてauto‑instrumenterを使用します。この計装により、最も一般的な自動計装ライブラリがすべて読み込まれます。このチュートリアルに関連するライブラリは次のとおりです。
        • @opentelemetry/instrumentation-pg(Postgresデータベースライブラリ(pg)用)
        • @opentelemetry/instrumentation-express(Node.jsのExpressフレームワーク用)
        • @opentelemetry/instrumentation-amqplib(RabbitMQライブラリ「amqplib」用)
    3. SDKを起動します。
  2. ステップ3で作成した自動計装スクリプトをインポートして、messengerサービスを起動します。
node --import ./tracing.mjs index.mjs
  1. しばらくすると、トレースに関連する大量の出力がコンソール(ターミナル)に表示され始めます:
...
{
  traceId: '9c1801593a9d3b773e5cbd314a8ea89c',
  parentId: undefined,
  traceState: undefined,
  name: 'fs statSync',
  id: '2ddf082c1d609fbe',
  kind: 0,
  timestamp: 1676076410782000,
  duration: 3,
  attributes: {},
  status: { code: 0 },
  events: [],
  links: []
}
...

注:Challenge 2で再利用するため、ターミナルセッションは開いたままにしておいてください。

チャレンジ2:すべてのサービスにOTelインストルメンテーションとトレース可視化を設定

トレースの表示と分析にはさまざまなツールを使用できますが、このチュートリアルではJaegerを使用します。Jaegerは、スパンやその他のトレースデータを表示するためのWebベースのユーザーインターフェースを備えた、シンプルなオープンソースのエンドツーエンド分散トレーシングフレームワークです。platformリポジトリで提供されるインフラストラクチャにはJaegerが含まれているため(チャレンジ1のステップ1で起動済み)、複雑なツールの扱いに煩わされることなく、データ分析に集中できます。

ブラウザーからJaegerにアクセスするには、エンドポイント「http://localhost:16686」を使用します。ただし、現時点でこのエンドポイントにアクセスしても、システムに関する情報は何も表示されません。現在収集しているトレースがコンソールに送信されているためです。Jaegerでトレースデータを表示するには、OpenTelemetryプロトコル(OTLP)形式を使用してトレースをエクスポートする必要があります。

この課題では、以下の計装を構成して、主要なユーザーフローを計装します。

外部コレクターに送信するOTel自動インストルメンテーションの設定

改めて説明すると、OTel自動インストルメンテーションを使用する場合、トレーシングを設定するためにmessengerコードベースを変更する必要はありません。代わりに、すべてのトレーシング設定をスクリプトに記述し、実行時にNode.jsプロセスへインポートします。ここでは、messengerサービスが生成するトレースの送信先を、コンソールから外部コレクター(このチュートリアルではJaeger)に変更します。

  1. 引き続きチャレンジ1と同じターミナルを使用し、appディレクトリ(messengerリポジトリ内)で、OTLPエクスポーターのNode.jsパッケージをインストールします:
npm install @opentelemetry/exporter-trace-otlp-http@0.36.0
  1. The @opentelemetry/exporter-trace-otlp-httpライブラリは、トレース情報をOTLP形式でHTTP経由でエクスポートします。OTel外部コレクターにテレメトリを送信する際に使用します。
  2. Challenge 1で作成して編集した「tracing.mjs」を開き、次の変更を加えます。注:簡略化のため、このチュートリアルではコレクターがデフォルトの場所(http://localhost:4318/v1/traces)にあることを前提としています。実際のシステムでは、場所を明示的に設定することを推奨します。
    • ファイルの先頭にある一連のimport文に、次の行を追加します:
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
  • OTel SDKに指定する「exporter」を、Challenge 1で使用したコンソールexporterから、HTTP経由でOTLPデータをOTLP互換コレクターに送信できるものに変更します。次のコードを置き換えます。
traceExporter:new opentelemetry.tracing.ConsoleSpanExporter(),
  • を次のように置き換えます:
traceExporter: new OTLPTraceExporter({ headers: {} }),
  1. Press Ctrl+cを押して、messengerサービスを停止します。このサービスは、「コンソールに送信するOTel自動インストルメンテーションの設定」のステップ4で、このターミナルから起動したものです。次に、ステップ2で設定した新しいエクスポーターを使用するため、サービスを再起動します:
^cnode --import ./tracing.mjs index.mjs
  1. 別の2つ目のターミナルセッションを開始します。(以降の手順では、これを「クライアントターミナル」、ステップ1と3で使用した元のターミナルを「メッセンジャーターミナル」と呼びます。)約10秒待ってから、messengerサービスにヘルスチェックリクエストを送信します(複数のトレースを確認する場合は、数回実行できます)。
curl -X GET http://localhost:4000/health
  1. リクエストを送信する前に10秒待つと、サービスの起動時に自動インストルメンテーションによって生成される多数のトレースよりも後に対象のトレースが記録されるため、見つけやすくなります。
  2. ブラウザーでJaeger UI(http://localhost:16686)にアクセスし、OTLPエクスポーターが想定どおりに動作していることを確認します。タイトルバーの[Search]をクリックし、[Service]フィールドのドロップダウンメニューから、名前が「unknown_service」で始まるサービスを選択します。[Find Traces]ボタンをクリックします。
  3. ウィンドウの右側にあるトレースをクリックすると、そのトレースに含まれるスパンの一覧が表示されます。各スパンには、トレースの一部として実行された処理が記述されています。処理が複数のサービスにまたがる場合もあります。スクリーンショットの「jsonParser」スパンは、「jsonParser」部分の処理にかかった時間を示しています。この部分は、「messenger」サービスのリクエスト処理コードに含まれます。
    自動インストルメンテーションを変更して正しいサービス名を表示する前の、unknown_serviceのスパン一覧を示すJaeger GUIのスクリーンショット
  4. ステップ5で説明したとおり、OTel SDKによってエクスポートされるサービス名(unknown_service)では、サービスの内容がわかりません。これを修正するには、messengerのターミナルでCtrl+cを押して、messengerサービスを停止します。次に、Node.jsパッケージをさらに2つインストールします。
^c npm install @opentelemetry/semantic-conventions@1.10.0 \ @opentelemetry/resources@1.10.0
  1. この2つのライブラリは、次の機能を提供します。
    • @opentelemetry/semantic-conventions – OTel仕様で定義されているトレースの標準属性を定義します。
    • @opentelemetry/resources – OTelデータを生成するソース(このチュートリアルではmessengerサービス)を表すオブジェクト(リソース)を定義します。
  2. テキストエディターで「tracing.mjs」を開き、次の変更を加えます。
    • ファイルの先頭にあるimport文の一連の記述に、次の行を追加します:
import { Resource } from "@opentelemetry/resources";import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
  • OTel仕様の適切なキーの下に、resource「messenger」を作成するため、最後のimportステートメントの後に次の行を追加します:
const resource = new Resource({  [SemanticResourceAttributes.SERVICE_NAME]: "messenger",
});
  • 黒色の行の間にオレンジ色で強調表示された行を追加し、resourceオブジェクトをNodeSDKコンストラクターに渡します。
const sdk = new opentelemetry.NodeSDK({  resource,
  traceExporter: new OTLPTraceExporter({ headers: {} }),
  instrumentations: [getNodeAutoInstrumentations()],
});
  1. Restart the messengerサービスを再起動します:
node --import ./tracing.mjs index.mjs
  1. 約10秒待ってから、手順4で開いたクライアントターミナルで、サーバーに別のヘルスチェックリクエストを送信します(複数のトレースを確認する場合は、コマンドを数回実行できます)。
curl -X GET http://localhost:4000/health
  1. 注:クライアントターミナルは次のセクションで再利用するため、メッセンジャーターミナルはチャレンジ3で再利用するため、開いたままにしておいてください。
  2. ブラウザーのJaeger UIに「messenger」という新しいサービスが表示されることを確認します(表示には数秒かかる場合があり、Jaeger UIの更新が必要になることがあります)。
    Jaeger GUIのスクリーンショット。psansを詳細に調査できるサービスの一覧にmessengerが表示されています
  3. Select messengerをServiceドロップダウンメニューから選択し、Find Tracesボタンをクリックすると、messengerサービスから生成された最近のトレースがすべて表示されます(スクリーンショットには、最新の20件のうち2件が表示されています):
    messengerサービスの最新トレース2件を表示するJaeger GUIのスクリーンショット
  4. トレースをクリックすると、そのトレースに含まれるスパンが表示されます。各スパンには、発生元が「messenger」サービスであることを示すタグが適切に付けられています。
    単一のmessengerスパンの詳細を表示するJaeger GUIのスクリーンショット

notifierサービスのOTel自動インストルメンテーションを構成する

次に、notifierサービスの自動インストルメンテーションを起動して構成します。基本的には、前の2つのセクションでmessengerサービスに対して実行したものと同じコマンドを実行します。

  1. 新しいターミナルセッションを開きます(以降の手順では「notifierターミナル」と呼びます)。notifierリポジトリのappディレクトリに移動し、Node.jsをインストールします(必要に応じて別の方法を使用できます)。
cd ~/microservices-march/notifier/appasdf install
  1. 依存関係をインストールします:
npm install
  1. Start the PostgreSQL database for the notifierサービス用のPostgreSQLデータベースを起動します:
docker compose up -d
  1. データベースのスキーマとテーブルを作成し、初期データを挿入します:
npm run refresh-db
  1. 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
  1. 次の名前で新しいファイルを作成します:tracing.mjs:
touch tracing.mjs
  1. 任意のテキストエディターで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();
  1. 注:このスクリプトは、messengerサービス用のスクリプトとまったく同じですが、SemanticResourceAttributes.SERVICE_NAMEフィールドの値はnotifierです。
  2. OTel自動計装を使用してnotifierサービスを起動します:
node --import ./tracing.mjs index.mjs
  1. 約10秒待ってから、クライアントのターミナルでヘルスチェックリクエストをnotifierサービスに送信します。このサービスは、ポート4000でリッスンしているmessengerサービスとの競合を避けるため、ポート5000でリッスンしています:
curl http://localhost:5000/health
  1. 注:Challenge 3で再利用するため、clientとnotifierのターミナルは開いたままにしてください。
  2. ブラウザーのJaeger UIに「notifier」という新しいサービスが表示されていることを確認します。
    スパンを詳細に調査できるサービスの一覧にnotifierが表示されたJaeger GUIのスクリーンショット

NGINXのOTelインストルメンテーションを構成

NGINXでは、OTelの自動インストルメンテーション方式を使用せず、トレーシングを手動で設定します。現在、OTelを使用してNGINXをインストルメント化する最も一般的な方法は、Cで記述されたモジュールを使用することです。サードパーティ製モジュールはNGINXエコシステムの重要な要素ですが、設定には多少の作業が必要です。このチュートリアルでは、その設定手順を説明します。背景情報については、F5のブログ記事「NGINXおよびNGINX Plus向けサードパーティ製ダイナミックモジュールのコンパイル」をご覧ください。

  1. 新しいターミナルセッション(「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
  1. 任意のテキストエディターで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
  1. Open 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;
        }
    }
}
  1. この非常に基本的なNGINX設定ファイルでは、NGINXに次の処理を指示します。 注: これは、本番環境でリバースプロキシおよびロードバランサーとして使用するNGINXの実際の設定とほぼ同じです。主な違いは、serverディレクティブの引数として、upstreamブロックでは通常、localhostではなくドメイン名またはIPアドレスを指定する点です。
    • 一連のmessengerサービスインスタンスを表す、messengerという名前のアップストリームグループを設定する
    • ポート8085でHTTPリクエストを待ち受ける
    • パスが次で始まるすべての受信リクエストを/(つまり、すべての受信リクエスト)messengerアップストリームに転送する
  2. Open opentelemetry_module.confを開き、以下を追加します。
NginxModuleEnabled ON;NginxModuleOtelSpanExporter otlp;
NginxModuleOtelExporterEndpoint localhost:4317;
NginxModuleServiceName messenger-lb;
NginxModuleServiceNamespace MicroservicesMarchDemoArchitecture;
NginxModuleServiceInstanceId DemoInstanceId;
NginxModuleResolveBackends ON;
NginxModuleTraceAsError ON;
  1. NGINXとNGINX OTelモジュールを含むDockerイメージをビルドします:
docker build -t messenger-lb .
  1. NGINXリバースプロキシおよびロードバランサー用のDockerコンテナを起動します:
docker run --rm --name messenger-lb -p 8085:8085 --network="host" messenger-lb
  1. クライアントのターミナルで、NGINXリバースプロキシおよびロードバランサーを介して、messengerサービスにヘルスチェックリクエストを送信します(このリクエストを送信する前に待機する必要はありません):
curl http://localhost:8085/health
  1. 注:Challenge 3で再利用するため、NGINXとクライアントのターミナルは開いたままにしておいてください。
  2. ブラウザーで、新しい「messenger-lb」サービスが、先ほど起動したサービスとともにJaeger UIに表示されていることを確認します。ブラウザーでJaeger UIを再読み込みする必要がある場合があります。
    スパンを詳細に調査できるサービスの一覧に、新たにmessenger-lbが追加されたJaeger GUIのスクリーンショット

チャレンジ3:OTelトレースの読み方を学ぶ

「アーキテクチャとユーザーフロー」ではユーザーフローの各段階について説明しましたが、ここで振り返ってみましょう。

  1. ユーザーが別のユーザーにメッセージを送信すると、会話が始まります。
  2. NGINXリバースプロキシがメッセージをインターセプトし、messengerサービスに転送します。
  3. The messengerサービスは、メッセージを自身のデータベースに書き込んだ後、RabbitMQを介してイベントを送信します。
  4. The notifierサービスはそのイベントを受信し、受信者(2人目のユーザー)の通知設定を参照して、指定された方法で受信者に通知を送信します。

また、テレメトリを実装する目的は次のとおりです。

  1. 新しいメッセージフローを実現するために、リクエストが経るすべてのステップを把握する。
  2. 通常の条件下で、フローがエンドツーエンドで5秒以内に実行されていることを確信できる。
  3. See how long it takes the notifierサービスが、messengerサービスから送信されたイベントの処理を開始するまでにかかる時間を確認します。

この演習では、OTelインストルメンテーションによって生成されたトレースが前述の目標を満たしているかどうかを評価する方法を学びます。まず、システムを実際に動かして、トレースをいくつか作成します。次に、メッセージフローのトレースと、そのうちNGINX、messengerサービス、およびnotifierサービスによって生成されたセクションを確認します。

トレースデータの作成

クライアント端末で会話を設定し、2人のユーザー間でメッセージを2、3件送信します:

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を開き、messenger-lbをServiceドロップダウンメニューから選択して、Find Tracesボタンをクリックします。フローの最初からトレースの一覧が表示されます。次のスクリーンショットのように、任意のトレースをクリックすると詳細が表示されます。

フロー内のすべてのスパンを表示するJaeger GUIのスクリーンショット

いくつかの項目をクリックして、少し確認してみましょう。次に進む前に、トレース内の情報が、チャレンジ3の「概要」に記載されている計測の目標達成にどのように役立つかを検討してください。検討すべき主な質問は次のとおりです。

  • 目標の達成に役立つ情報は何ですか?
  • 不足している情報は何ですか?
  • 関連性のない情報は何ですか?

トレースのNGINX(messenger-lb)セクションを確認する

目標1:新しいメッセージフローでリクエストが経るすべてのステップを確認する

まず、親スパン内に11個の子スパンを持つNGINXスパンから確認します。現在のNGINX構成は非常にシンプルなため、子スパンには特筆すべき情報はなく、NGINXのリクエスト処理ライフサイクルにおける各ステップの所要時間が示されているだけです。一方、親スパン(最初のスパン)からは、いくつかの興味深い知見が得られます。

トレースのNGINX(messenger-lb)セクションにある親スパンを表示したJaeger GUIのスクリーンショット

  • [Tags]には、次の属性が表示されます。これら3つの情報を総合すると、「POSTリクエストが/conversations/1/messagesに送信され、レスポンスは201(作成成功)だった」ことを示しています。これは「アーキテクチャとユーザーフロー」のステップ1および4aに対応します。
    • http.methodフィールド – POST(RESTでは作成を意味します)
    • http.status_codeフィールド – 201(作成が成功したことを示します)
    • http.targetフィールド – conversations/1/messages(メッセージエンドポイント)
  • [Process]のwebengine.nameフィールドは、これがリクエストのNGINX部分であることを示します。

さらに、messengerとnotifierのスパンは、messenger-lbのconversations/1スパン内にネストされているため(「トレースを読み取るための準備」のスクリーンショットを参照)、NGINXリバースプロキシ経由でmessengerサービスに送信されたリクエストが、フロー内の想定されるすべてのコンポーネントに到達したことを確認できます。

この情報から、NGINXリバースプロキシがフローの一部であったことを確認できるため、目標を達成したと判断できます。

目標2:フローが5秒以内に実行されることを確認する

In the list of spans labeled messenger-lbというラベルが付いたスパンのリストで、最新のスパン(リストの一番下)を確認し、リクエストのNGINX部分にかかった時間を調べます。スクリーンショットでは、スパンは589マイクロ秒(µs)の時点で開始し、24 µs継続しています。つまり、リバースプロキシ処理全体にかかった時間はわずか613 µs(約0.6ミリ秒(ms))です。(実際の値は、チュートリアルを実行する環境によって異なります。)

トレースのNGINX(messenger-lb)セクションにあるスパンを表示したJaeger GUIのスクリーンショット

このような構成では、ほとんどの場合、値は他の測定値と比較する場合にのみ有用であり、システムによって異なります。ただし、このケースでは、この処理に5秒近くかかる可能性がないことは明らかです。

この情報から、NGINXの処理に5秒近くもかかっていないことがわかるため、目的は達成されています。フロー内に非常に時間のかかる処理があるとすれば、それは後続の段階で発生しているはずです。

目標3:notifierサービスがmessengerサービスから送信されたイベントを読み取るまでの所要時間を確認する

NGINXリバースプロキシレイヤーにはこれに関する情報が含まれていないため、次にmessengerのスパンを確認します。

トレースのmessengerセクションを確認

目標1:新しいメッセージフローでリクエストが経るすべてのステップを確認する

トレースのmessengerサービスのセクションには、さらに11個のスパンがあります。ここでも、子スパンの大半は、Expressフレームワークがリクエストを処理する際の基本的なステップに関するもので、特に注目すべき点はありません。一方、親スパン(最初のスパン)からは、今回も興味深い情報が得られます。

トレースのmessengerセクションにある親スパンを表示したJaeger GUIのスクリーンショット

[Tags]には、次の属性が表示されます。

  • http.methodフィールド – POST(ここでも、RESTの用語では作成を意味します)
  • http.routeフィールド – /conversations/:conversationId/messages(メッセージのルート)
  • http.targetフィールド – /conversations/1/messages(メッセージエンドポイント)

この情報から、messengerサービスがフローに含まれ、アクセスされたエンドポイントが新しいメッセージエンドポイントであったことがわかるため、目的を達成できます。

目標2:フローが5秒以内に実行されることを確認する

次のスクリーンショットに示すように、トレースのmessenger部分は1.28 msで開始し、36.28 msで終了しており、全体の所要時間は35 msでした。その大半はJSONの解析(middleware - jsonParser)に費やされ、さらに多くの時間がデータベースへの接続(pg-pool.connectおよびtcp.connect)に費やされました。

メッセージの書き込み処理では複数のSQLクエリも実行されるため、これは妥当な結果です。そこで、これらのクエリの実行時間を取得できるように、自動インストルメンテーションの設定を拡張することも検討できます。(この追加のインストルメンテーションについてはチュートリアルでは説明しませんが、Challenge 4ではスパンを手動で作成します。このスパンを使用して、データベースクエリをラップできます。)

Jaeger GUIのスクリーンショット。トレースのmessengerセクションにあるスパンと、それぞれの所要時間を示しています

この情報から、messengerの処理時間は5秒には遠く及ばないことがわかるため、目的を達成できます。フロー内に非常に遅い処理があるとすれば、それは後続の処理で発生しているはずです。

目標3:notifierサービスがmessengerサービスから送信されたイベントを読み取るまでの所要時間を確認する

NGINXのスパンと同様に、messengerのスパンにもこの情報は含まれていないため、notifierのスパンに移ることができます。

トレースのnotifierセクションを確認する

目標1:新しいメッセージフローでリクエストが経るすべてのステップを確認する

トレースの「notifier」セクションに含まれるスパンは2つだけです。

トレースのnotifierセクションにある2つのスパンを示すJaeger GUIのスクリーンショット

  • The chat_queueのprocessスパン – notifierサービスがchat_queueメッセージキューのイベントを処理したことを確認します
  • The pg-pool.connectスパン – イベントの処理後、notifierサービスがデータベースに何らかの接続を確立したことを示します

これらのスパンから得られる情報だけでは、すべてのステップを把握するという目標を部分的にしか達成できません。notifierサービスがキューからイベントを取得する段階まで進んだことは確認できますが、次の点は不明です。

  • このサービスが送信したメッセージ通知が、messengerサービスによってディスパッチされたイベントに対応しているか
  • 該当するメッセージ通知が、メッセージの受信者に正しく送信された

これは、notifierサービスのフローを完全に理解するには、以下を行う必要があることを示しています。

  • 通知の送信を示すスパンを手動で計装する
  • 次のイベント間に、トレースIDによる明示的な関連付けがあることを確認します。messengerサービスが送信したイベントと、notifierサービスが受信したイベント

目標2:フローが5秒以内に実行されることを確認する

Looking at the overall timing of the notifierサービスのスパン全体のタイミングを見ると、リクエストがフローのnotifierセクションで30.77 msを費やしたことがわかります。ただし、フロー全体の「終了」(受信者への通知送信)を示すスパンがないため、このセクションの合計所要時間も、処理全体の完了時間も特定できません。

目標3:notifierサービスがmessengerサービスから送信されたイベントを読み取るまでの所要時間を確認する

ただし、chat_queueのprocessスパンがnotifierサービスで6.12 msに開始しており、これはmessengerサービスのchat_queueのsendスパンが4.12 msに開始してから2 ms後であることがわかります。

messengerサービスがディスパッチしたイベントをnotifierサービスが消費している様子を示すJaeger GUIのスクリーンショット

この目標を達成できたと判断できるのは、イベントがnotifierによって消費されたのが、messengerサービスによるディスパッチの2 ms後だと把握できるためです。目標2とは異なり、この目標を達成するうえで、イベントが完全に処理されたかどうかや、処理にかかった時間を把握する必要はありません。

結論

現在のOTel自動計装によって生成されたトレースを分析した結果、以下の点が明らかになりました。

  • これらのスパンの多くは、現状のままでは有用ではありません。
    • NGINXは、重視するリバースプロキシの役割には関係のない、認可チェックやファイル配信などの機能に関連するスパンを生成しています。しかし現時点では、NGINX向けのOTelインストルメンテーションで不要なスパンを除外できないため、対処することはできません。
    • Node.jsサービス(messengerサービスとnotifierサービス)のスパンには、目標に関連していると思われるものがあります。具体的には、JSON解析、request handler、およびすべてのデータベース操作のスパンです。一方、一部のミドルウェアのスパン(expressInitやcorsMiddlewareなど)は関連性がないと思われるため、削除できます。
  • 以下について、重要なスパンが欠落しています:
    • Notifications sent by the notifierサービスから送信される通知
    • A clear mapping between the RabbitMQ event dispatched from the messengerサービスから送信されたRabbitMQイベントと、notifierサービスで処理されるイベントとの明確な対応関係

つまり、基本的な計装によって最後の目標が達成されます。

  • 所要時間を確認する:notifierサービスが、messengerサービスから送信されたイベントの処理を開始するまで

ただし、最初の2つの目標を達成するには情報が不足しています。

  • 新しいメッセージフローでリクエストが経るすべてのステップを把握する
  • 通常の状況では、フローがエンドツーエンドで5秒未満で実行されることを確認する

課題4:トレースの測定値に基づいて計装を最適化

この課題では、課題3で実施したトレース分析に基づいて、OTelインストルメンテーションを最適化します。具体的には、不要なスパンの削除、新しいカスタムスパンの作成、および確認(notifierサービスが使用しているイベントがmessengerサービスによって生成されたものであること)を行います。

不要なスパンの削除

  1. 任意のテキストエディターで、tracing.mjsリポジトリのappディレクトリにあるmessengerファイルを開き、ファイル上部のimport文のリストの末尾に以下を追加します:
const IGNORED_EXPRESS_SPANS = new Set([  "middleware - expressInit",
  "middleware - corsMiddleware",
]);
  1. これは、以下のJaeger UIのスクリーンショットに表示されているスパンの一覧を基に、このフローに有用な情報を提供しないためトレースから除外するスパン名のセットを定義します。スクリーンショットに表示されている他のスパンも不要と判断した場合は、IGNORED_EXPRESS_SPANSの一覧に追加できます。
    Jaeger GUIのスクリーンショット。messengerサービスの複数のスパンの一覧が表示されており、関連情報を提供しない可能性があるため、トレースから除外できます
  2. オレンジ色で強調表示されている行を変更し、自動インストルメンテーション設定にフィルターを追加して、不要なスパンを除外します:
const sdk = new opentelemetry.NodeSDK({  resource,
  traceExporter: new OTLPTraceExporter({ headers: {} }),
  instrumentations: [getNodeAutoInstrumentations()],
});
  1. 次のように変更します:
const sdk = new opentelemetry.NodeSDK({  resource,
  traceExporter: new OTLPTraceExporter({ headers: {} }),
  instrumentations: [ getNodeAutoInstrumentations({ "@opentelemetry/instrumentation-express": { ignoreLayers: [ (name) => { return IGNORED_EXPRESS_SPANS.has(name); }, ], }, }), ],
});
  1. The getNodeAutoInstrumentations関数は、ステップ1で定義したスパンのセットを参照し、@opentelemetry/instrumentation-expressによって生成されたトレースから該当するスパンを除外します。つまり、return文は、IGNORED_EXPRESS_SPANSに含まれるスパンに対してtrueと評価され、ignoreLayers文によってそのスパンがトレースから削除されます。
  2. messengerのターミナルで、Ctrl+cを押してmessengerサービスを停止します。その後、再起動します:
^cnode --import ./tracing.mjs index.mjs
  1. 約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'
  1. Jaeger UIでmessengerスパンを再度確認します。2つのmiddlewareスパンであるexpressInitとcorsMiddlewareは表示されなくなりました(Challenge 3の「Goal 2」にある「Examine the messenger Section of the Trace」のスクリーンショットと比較できます)。
    インストルメンテーションを変更して2つのスパンをトレースから除外した後、トレースにそれらのスパンが含まれなくなったことを示すJaeger GUIのスクリーンショット

カスタムスパンの設定

このセクションでは、初めてアプリケーションコードを変更します。自動インストルメンテーションを使用すると、アプリケーションを変更せずに多くの情報を生成できますが、特定のビジネスロジックをインストルメント化しなければ得られない知見もあります。

インストルメンテーションを行う新しいメッセージフローでは、その一例として、メッセージの受信者への通知送信をトレースします。

  1. Open index.mjsをappディレクトリ(notifierリポジトリ内)で開きます。このファイルには、サービスのすべてのビジネスロジックが含まれています。ファイルの先頭にあるimport文のリストの末尾に、次の行を追加します:
import { trace } from "@opentelemetry/api";
  1. 次のコードを置き換えます(ファイル内の91行目付近)。
for (let pref of preferences) {
  console.log(
    `Sending notification of new message via ${pref.address_type} to ${pref.address}`
  );
}
  1. を次の内容に置き換えます:
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
  }
);
  1. 新しいコードは次のように動作します。親スパンを設けることで、ユーザーの通知設定が見つからない場合でも、「通知を送信」する各処理が確実に報告されます。
    1. OTelトレースを操作するためのグローバルオブジェクトであるtracerを取得します。
    2. 「notification.send_all」という新しい親スパンを開始し、メッセージの送信者を識別するためにuser_id属性を設定します。
    3. 受信者の通知設定を列挙するループに入り、notification.sendという新しい子スパンがnotification.send_allの下に作成されます。通知ごとに新しいスパンが生成されます。
    4. 子スパンにさらに属性を設定します:
      • notification_type – One of sms or email
      • user_id – 通知を受信するユーザーのID
    5. 各子notification.sendスパンを順番に終了します。
    6. 親notification.send_allスパンを閉じます。
  2. notifierのターミナルで、Ctrl+cを押してnotifierサービスを停止します。その後、再起動します:
^cnode --import ./tracing.mjs index.mjs
  1. 約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'
  1. Jaeger UIでnotifierのスパンを再確認します。親スパンと2つの子スパンが表示され、それぞれに「通知を送信」操作があります。
    notifierサービスのコードで3つの新しいスパンを定義した結果を示すJaeger GUIのスクリーンショット

新しいメッセージフローでリクエストがたどるすべてのステップを確認できるようになったため、1つ目と2つ目の目標を完全に達成できます。各スパンのタイミングから、これらのステップ間に生じる遅延を特定できます。

messengerとnotifierが同じイベントを処理していることを確認する

フローの全体像を把握するには、もう1つ確認が必要です。処理中のイベントは、notifierサービスが実際にmessengerサービスから送信されたものでしょうか?

2つのトレースを関連付けるために明示的な変更を加える必要はありません。ただし、自動インストルメンテーションが魔法のように機能すると盲信するのも避けるべきです。

この点を踏まえ、簡単なデバッグコードを追加して、NGINXサービスで開始されたトレースが、notifierサービスで受信されたトレースと実際に同一である(同じトレースIDを持つ)ことを確認します。

  1. Open the index.mjsファイルをappディレクトリ(messengerリポジトリ内)で開き、次の変更を加えます。
    • 冒頭にあるimport文のリストの末尾に、次の行を追加します。
import { trace } from "@opentelemetry/api";
  • 黒色で示された既存の行の下に、オレンジ色で強調表示された行を追加します:
async function createMessageInConversation(req, res) {
  const tracer = trace.getActiveSpan(); console.log("TRACE_ID: ", tracer.spanContext().traceId);
  • 新しく追加した行では、TRACE_IDを、messenger内で新規メッセージの作成を処理する関数から出力します。
  1. Open the index.mjsファイルをappディレクトリ(notifierリポジトリ内)で開き、黒色で示された既存の行の下に、オレンジ色で強調表示された行を追加します。
export async function handleMessageConsume(channel, msg, handlers) {  console.log("RABBIT_MQ_MESSAGE: ", msg);
  1. この新しい行では、notifierサービスが受信したAMQPイベントの内容全体を出力します。
  2. 次のコマンドをmessengerとnotifierの両方のターミナルで実行し、messengerサービスとnotifierサービスを停止して再起動します:
^cnode --import ./tracing.mjs index.mjs
  1. 約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'
  1. Look at the logs for the messengerサービスとnotifierサービスのログを確認します。messengerサービスのログには、メッセージのトレースIDを示す次のような行が含まれています(チュートリアルを実行した際の実際のIDは異なります):
TRACE_ID:  29377a9b546c50be629c8e64409bbfb5
  1. 同様に、notifierサービスのログでは、次のような出力にトレースIDが記録されます。
_spanContext: {
  traceId: '29377a9b546c50be629c8e64409bbfb5',
  spanId: 'a94e9462a39e6dbf',
  traceFlags: 1,
  traceState: undefined
},
  1. コンソール上のトレースIDは一致していますが、最後にJaeger UIのトレースIDと比較できます。該当するトレースIDのエンドポイント(実際のエンドポイントは異なりますが、この例ではhttp://localhost:16686/trace/29377a9b546c50be629c8e64409bbfb5)でUIを開き、トレース全体を確認します。Jaegerのトレースから、次のことを確認できます。
    • The AMQP auto‑instrumentation in the messengerサービスのAMQP自動インストルメンテーションは、イベントのディスパッチ時に、このトレースIDをメタデータの一部として追加します。
    • The AMQP auto‑instrumentation in the notifierサービスのAMQP自動インストルメンテーションは、そのメタデータを受け取り、トレースコンテキストを適切に設定します。

注:実際の本番システムでは、フローが想定どおりに動作することを確認した後、このセクションで追加したコードを削除します。

リソースのクリーンアップ

このチュートリアルでは、いくつかのコンテナとイメージを作成しました。以下の手順に従って削除してください。

  • 実行中のDockerコンテナを削除するには:
docker rm $(docker stop messenger-lb)
  • To remove the platformサービスと、messengerおよびnotifierデータベースサービスを削除するには:
cd ~/microservices-march/platform && docker compose down cd ~/microservices-march/notifier && docker compose down cd ~/microservices-march/messenger && docker compose down

次のステップ

おめでとうございます。チュートリアルは完了です!

  • NGINXリバースプロキシと2つのNode.jsサービスにOTelインストルメンテーションを設定しました。
  • OTelラボの目標を達成するため、OTelの自動計装によって提供されたデータを批判的な視点で確認し、不足していたテレメトリを追加しました。
    • アプリケーションコードを直接変更することなく、メッセージングシステムを通過する特定のリクエストの流れを適切に把握できました。
    • 通常の状況では、フローがエンドツーエンドで5秒未満で実行されていることを確認しました。

とはいえ、理想的なトレーシング構成の可能性は、まだほんの一部しか試していません。本番環境では、データベースクエリごとのカスタムスパンや、各サービスのコンテナIDなどのランタイム情報を記述する追加メタデータをすべてのスパンに追加することも考えられます。また、OTelデータの残り2種類(メトリクスとログ)も実装すれば、システムの健全性を包括的に把握できます。

マイクロサービスについてさらに学ぶには、Microservices March 2023をご覧ください。ユニット 4:オブザーバビリティでマイクロサービスの混沌と複雑さを管理では、オブザーバビリティデータの主要な3つの種類、インフラストラクチャとアプリケーションを整合させることの重要性、詳細なデータの分析を始める方法について学びます。


共有

関連するブログ記事

チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する
NGINX | 2024/01/18

チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する

OpenTelemetryを使用してNGINXサーバーの状態と健全性をより明確に把握することで、問題のトラブルシューティングを行い、Webアプリケーションのパフォーマンスを向上できます。

AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正
NGINX | 2023/05/31

AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正

Akita SoftwareのAPIオブザーバビリティ向け新しいNGINX Plus Certified Moduleを活用して、APIエンドポイントの問題をより迅速に特定し、修正できます。

JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法
NGINX | 2023/02/09

JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法

Red HatのシニアプリンシパルデベロッパーアドボケイトであるDaniel Oh氏が、Javaプロジェクトをゼロから構築するためのひな形を作成し、Kubernetesネイティブアプリケーションとしてビルドする方法を解説します。

F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング
NGINX | 2022/05/24

F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング

NGINXaaS for Azureは、NGINX PlusをAzureネイティブのSaaSとして利用できるサービスです。NGINXの設定を容易にリフト&シフトできるため、クラウド導入とモダンアプリの展開を加速できます。

SELinuxでNGINXおよびNGINX Plusを使用する
NGINX | 2018/08/17

SELinuxでNGINXおよびNGINX Plusを使用する

Red Hat Enterprise Linux(RHEL)および関連ディストリビューションでSecurity-Enhanced Linux(SELinux)を有効にすると、デフォルト設定によりNGINXとNGINX Plusの一部の操作が制限されます。この記事では、すべての機能を利用できるようにSELinuxの設定を変更する方法について説明します。

NGINX PlusによるAWS Auto Scalingグループの負荷分散
NGINX | 2017/03/06

NGINX PlusによるAWS Auto Scalingグループの負荷分散

新しいnginx-asg-sync統合ソフトウェアにより、NGINX PlusはAWS Auto Scalingグループの負荷を直接分散できるため、クラウドでの負荷分散がさらに柔軟になります