概要
この記事は、OpenTelemetryについて全く知らない人に向けた入門記事です。前半でOpenTelemetryとは何か解説し、後半でデモアプリケーションを動かしながら、理解を深めていきます。
この記事の執筆時点のOpenTelemetryの最新バージョンは、1.8.0です。参照する公式ドキュメントや動作確認するデモアプリケーションのバージョンも1.8.0です。
OpenTelemetryとは
OpenTelemetryは、マイクロサービスアーキテクチャーで分散されたサービス(アプリケーション)の健全性や性能を示す「テレメトリーデータ」を生成、収集、管理、エクスポートするためのOSSです。
大規模な分散システムでは、健全性や性能を把握することが極めて困難なため、迅速なトラブルシューティングのためにはテレメトリーデータの収集が求められます。分散されたサービス(アプリケーション)からテレメトリーデータを収集するには、それらのアプリケーションが異なるプログラミング言語で実装していたとしても、それらに組み込むことができる、標準化された(ベンダーに依存しない)APIやSDKなどが必要です。それを提供するのがOpenTelemetryです。
OpenTelemetryの目的は、あくまでもテレメトリーデータを簡単に生成、収集、管理、エクスポートできるようにすることであり、テレメトリーデータの保存と可視化はJaegerやPrometheusのようなOSSまたは商用製品に任せるように、役割分担されています。
ちなみに、OpenTelemetryの公式サイトでは、以下のようにOpenTelemetryとは「Observability framework」(オブザーバビリティーフレームワーク)である説明しています。
OpenTelemetry is an Observability framework and toolkit designed to create and manage telemetry data such as traces, metrics, and logs.
オブザーバビリティーとは
オブザーバビリティー(可観測性)とは、システムやアプリケーションの内部の状態や挙動を把握し、問題(例えば、リクエストに対する処理の遅延など)を追跡および解決するための能力を指します。オブザーバビリティーを向上させる目的は、システム全体の健全性を維持し、問題を早期に検出して迅速に対処することです。これにより、システムの信頼性や可用性を向上させることができます。オブザーバビリティーを実現するためには、次に説明するテレメトリーデータが必要になります。
テレメトリーデータとは
OpenTelemetryにおけるテレメトリーデータとは、以下の3つを意味します。
- ログ:主にトラブルシューティングを容易にすることを目的に記録するメッセージ(テキスト形式の情報)です。ログには、このメッセージとともに、ログ生成処理が実行された時刻やログレベルなどが含まれます。ログの形式には、プレーンテキスト、構造化、非構造化の3種類がありますが、プレーンテキストが最も一般的です。
- メトリクス:一定の期間にわたって測定された何らかの数値です。日時、イベント名、イベント値などの属性が記録されます。ログとは異なり、形式は構造化が基本であるため、照会が容易です。保存にも適しており、大量のメトリクスを長期間保存して、システムの過去の傾向を把握することもできます。メトリクスを監視して、問題の兆候を検出した場合にアラートするようなこともできます。
- トレース:分散システムにおけるリクエストの処理経路を表すデータです。分散システムでリクエストが処理される際には、さまざまな処理(外部APIの呼び出し、DBからのデータの取得、など)が実行されます。これらの各処理を表すデータは「スパン」と呼ばれ、操作の過程でトレースID、スパンID、操作名、開始/終了の日時、イベントなどが含まれます。このスパンにより構成されるトレースを監視・追跡すること(「分散トレーシング」と呼ばれる)により、エラー、遅延、リソースの可用性などの問題の原因となっているコードブロックを特定できます。
OpenTelemetryのアーキテクチャーと構成要素
以下の図は、後述するデモアプリケーションにおけるテレメトリーデータのフローを表した図ですが、OpenTelemetryのアーキテクチャーと構成要素を大雑把に示していると考えられます。
最上部の「OpenTelemetry Demo」の「Microservice」は、様々のプログラミング言語で実装された複数のマイクロサービスです。これらのサービスに、実装するプログラミング言語に対応したOpenTelemetryのSDKが組み込まれています。そのSDKにより生成されたテレメトリーデータがOpenTelemetry Collectorの構成要素であるReceiverにHTTPやgRPCで送られ、それをProcessorが処理します。処理されたデータは、ExporterによりPrometeusやJaegerにHTTPやgRPCで送信されます。
動作検証
OpenTelemetryプロジェクトでは、デモアプリケーション「OpenTelemetry Demo」を公開しているので、これを動かしながら、OpenTelemetryの理解を深めていきましょう。
以下のように、GitHubからクローンして、opentelemetry-demo
ディレクトリに移動します。
$ git clone https://github.com/open-telemetry/opentelemetry-demo.git
$ cd opentelemetry-demo/
デモの前提条件
OpenTelemetry Demoを起動する前提条件は、以下になっています。
- Docker
- Docker Compose v2.0.0以上
- アプリケーション用に6GBのRAM
デモのアーキテクチャー
OpenTelemetry Demoは、gRPCとHTTPを介して互いに対話する、異なるプログラミング言語で書かれたマイクロサービスと、ユーザートラフィックを偽造するためにLocustを使用する負荷ジェネレータで構成されています。このデモのアーキテクチャーは以下のようになっています。
詳細は、ドキュメントを確認してください。
デモを動かしてみよう
では実際にOpenTelemetry Demoを動かしてみましょう。以下のように、docker compose up
コマンドで起動しますが、
$ docker compose up --force-recreate --remove-orphans --detach
私の環境ではKafkaが起動せず、エラーになります。
[+] Running 18/18
✔ Container grafana Running 0.0s
✔ Container ffs-postgres Healthy 0.0s
✔ Container jaeger Running 0.0s
✔ Container redis-cart Running 0.0s
✔ Container prometheus Running 0.0s
✘ Container kafka Error 0.0s
✔ Container opensearch Running 0.0s
✔ Container feature-flag-service Running 0.0s
✔ Container otel-col Running 0.0s
✔ Container ad-service Running 0.0s
✔ Container currency-service Running 0.0s
✔ Container email-service Running 0.0s
✔ Container product-catalog-service Running 0.0s
✔ Container shipping-service Running 0.0s
✔ Container cart-service Running 0.0s
✔ Container quote-service Running 0.0s
✔ Container payment-service Running 0.0s
✔ Container recommendation-service Running 0.0s
dependency failed to start: container kafka is unhealthy
原因は未調査ですが、とりあえず単独で起動させれば、よさそうです。
$ docker start kafka
ただ、上のログで分かるように起動するサービスも多すぎますし、メモリーも大量に消費するので、もう少し小さな構成にできると、ありがたいです。ということで、調べてみると、カレントディレクトリーにdocker-compose.yml
の他にdocker-compose.minimal.yml
というファイルがあることに気づきました。
差分を見ると、Feature Flagsとそれが使用するPostgreSQLなどのコンテナー、アカウントサービスやテスト用のコンテナーなどがdocker-compose.minimal.yml
には含まれていないことが分かります。また、各コンテナーのメモリーの使用量が大幅に制限されていることも読み取れます。
前述のデモのアーキテクチャーの図では、赤枠で囲まれたサービスが含まれていません。
前回起動したものをいったん終了させて、
$ docker compose down
このファイルを指定して、起動してみましょう。
$ docker compose -f docker-compose.minimal.yml up --remove-orphans --detach
[+] Running 19/19
✔ Container redis-cart Running 0.0s
✔ Container jaeger Running 0.0s
✔ Container opensearch Running 0.0s
✔ Container grafana Running 0.0s
✔ Container prometheus Running 0.0s
✔ Container otel-col Running 0.0s
✔ Container email-service Running 0.0s
✔ Container payment-service Running 0.0s
✔ Container ad-service Running 0.0s
✔ Container quote-service Running 0.0s
✔ Container cart-service Running 0.0s
✔ Container currency-service Running 0.0s
✔ Container shipping-service Running 0.0s
✔ Container product-catalog-service Running 0.0s
✔ Container recommendation-service Running 0.0s
✔ Container checkout-service Running 0.0s
✔ Container frontend Running 0.0s
✔ Container load-generator Running 0.0s
✔ Container frontend-proxy Started
今度は正常に起動したようです。これにより、以下のアプリケーションにアクセスできるようです。
アプリケーション | URL | アプリケーションの概要 | 最小構成(※) |
---|---|---|---|
Webストア | http://localhost:8080/ | オンラインストアのサンプルアプリ。様々なマイクロサービスと連携して、機能を実現している。 | 〇 |
Grafana | http://localhost:8080/grafana/ | OpenTelemetryがテレメトリーデータを視覚化し、システム状態の分析するためのアプリ。 | 〇 |
Feature Flags | http://localhost:8080/feature/ | 様々なマイクロサービスの特定の機能を意図的に異常動作させて、一定の確率でエラーを発生させるかように切り替えるためのUI。 | × |
Locust(ロードジェネレーター) | http://localhost:8080/loadgen/ | UIのある負荷テストツール。現実的なユーザーの買い物の流れを模したリクエストをフロントエンドに送り続けるサービス。 | 〇 |
Jaeger | http://localhost:8080/jaeger/ui/ | OpenTelemetryが生成したテレメトリーデータを収集し、それを可視化、分析するUI。 | 〇 |
※最小構成のdocker-compose.minimal.yml
に含まれるかどうか。
Webストア
まずはWebストアにアクセスしてみましょう。トップページは以下のようになっています。「Go Shopping」をクリックして、適当に買い物をしてみましょう。
適当に商品を選択し、
購入してみます。
購入が完了しました。非常に単純なオンラインストアアプリですね。
これで様々サービスへリクエストが送信されたことになります。
Jaeger
次にJaegerのUIにアクセスします。以下のような画面が表示されるので、「Service」に「frontend」を選択して、検索してみましょう。
リクエストの送信時刻と処理時間を可視化したものが表示されます。
この中の●の1つをクリックしてみると、以下のように内部でカートサービスなどのサービスにリクエストしていることが分かります。
System ArchitectureタブのDAGタブをクリックします。これを見ると、サービス間でどのようにリクエストが行われているかが分かります。矢印に付与されている数値は、各サービス間のトレース数を表しています。これは、あるサービスから別のサービスにトレースがどれだけの数で移動しているかを示しています。つまり、トレース数が多い矢印は、サービス間の通信がより頻繁に行われていると言えます。
Monitorタブをクリックすると、全体や各URLにおけるレイテンシーやエラー率が確認できます。
Grafana
Grafanaにもアクセスてみましょう。
「Create your first dashboard」でダッシュボードを作成すると、様々なメトリクスを可視化することができます。
Locust
ロードジェネレーターであるLocustのUIにもアクセスしてみましょう。
デフォルトでは10ユーザーがオンラインストアアプリにアクセスしているようなリクエストが自動的に転送されているようです。
STOPボタンをクリックして、このリクエストの転送をやめてみます(負荷を無くしてみます)。しばらく時間をおいてグラフを見てみると、いくつかのメトリクスが急降下してそのまま上昇しないことがわかります(※以下のグラフでは3時間ほど放置しています)。
では負荷を最初の10倍にしてみましょう。
これにより、いくつかのメトリクスが急上昇します。
Feature Flags
ちなみにFeature Flagsが起動している場合、アクセスすると以下のような画面が表示されます。
特定のサービスに意図的にエラーを発生させることで、どのような違いが出るか確認することができます。
ソースコード解析
各アプリケーションのソースコードにはどのようなOpenTelemetryのコードが追加されているのでしょうか?ソースコードを解析してみましょう。
デモのアーキテクチャーの図を見てわかるように、サービスは様々なプログラミング言語で実装されているので、自分が見やすい言語のサービスのソースコードを読むことができます。
ここでは、Pythonで実装されたレコメンデーションサービスのソースコードをチェックしてみます。このサービスは、ユーザーが閲覧した商品に基づいて、ユーザーに推奨される商品のリストを取得する役割を果たします。
まずはレコメンデーションサービスのディレクトリーに移動します。
$ cd src/recommendationservice/
tree
コマンドでディレクトリー構成を見てみましょう。
$ tree
.
├── Dockerfile
├── README.md
├── logger.py
├── metrics.py
├── recommendation_server.py
└── requirements.txt
6ファイルだけで、非常にシンプルですね。requirements.txt
を見ると、OpenTelemetryのライブラリーが依存関係にあるのが分かります。
opentelemetry-distro==0.43b0
opentelemetry-exporter-otlp-proto-grpc==1.22.0
どのような実装になっているのでしょうか。メインの処理が実装されていると思われるrecommendation_server.py
を覗いてみましょう。
まず、OpenTelemetryでテレメトリーデータ(トレース、メトリクス、ログ)を生成するためのモジュールをインポートしています。
from opentelemetry import trace, metrics
from opentelemetry._logs import set_logger_provider
これらのモジュールを使って、トレース、メトリクス、ログを生成するためのコードを見てみましょう。
トレースを生成するためのコード
トレースを生成するためのOpenTelemetryのTracer
オブジェクトは、以下のコードで初期化するようです。
tracer = trace.get_tracer_provider().get_tracer(service_name)
スパンは、以下のようにOpenTelemetryのTracer
オブジェクトのstart_as_current_span()
メソッドを使って生成します。with
文とともに生成すれば、with
文のコードブロックが終了する時点でスパンも終了します。
with tracer.start_as_current_span("get_product_list") as span:
get_product_list
は、推奨する商品リストを取得するためのスパンの名前です。
メトリクスを生成するためのコード
メトリクスを生成するオブジェクトは、以下のコードで初期化していますが、
meter = metrics.get_meter_provider().get_meter(service_name)
rec_svc_metrics = init_metrics(meter)
このinit_metrics()
は以下のような実装になっており、meter.create_counter()
でCounter
オブジェクトを生成し、
def init_metrics(meter):
# Recommendations counter
app_recommendations_counter = meter.create_counter(
'app_recommendations_counter', unit='recommendations',
description="Counts the total number of given recommendations"
)
rec_svc_metrics = {
"app_recommendations_counter": app_recommendations_counter,
}
return rec_svc_metrics
サービスの呼び出し1回当たりの推奨商品数の合計をメトリクスとして保存するようです。
rec_svc_metrics["app_recommendations_counter"].add(len(prod_list),
{'recommendation.type': 'catalog'})
なお、CPUやメモリーの消費量、GCの回数などのメトリクスは自動的に取得できるようになっています。そのためのコードをアプリケーションに追加する必要はありません。
ログを生成するためのコード
ログを生成するオブジェクトの初期化は、以下のようになっています。
# Initialize Logs
logger_provider = LoggerProvider(
resource=Resource.create(
{
'service.name': service_name,
}
),
)
set_logger_provider(logger_provider)
log_exporter = OTLPLogExporter(insecure=True)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
# Attach OTLP handler to logger
logger = logging.getLogger('main')
logger.addHandler(handler)
初期化が完了したら、以下のようにメッセージを含むログをレベルに応じて生成することができます。
logger.info(f"Receive ListRecommendations for product ids:{prod_list}")
今回はPythonで実装されたサービスを確認してみましたが、他のプログラミング言語のソースコードにも同様の実装になっていると思います。
感想
OpenTelemetryを利用することで、複雑な分散システムでもプログラミング言語に依存せず標準化されたテレメトリーデータを生成できる点は非常に有用だと思いました。テレメトリーデータは分散システムの健全性や性能を把握するだけでなく、機械学習を活用して障害の予兆の検出やリソースの削減などにも利用できそうです。ただし、実装を誤っていると、かえって混乱を招きトラブルシューティングの時間を遅くしたり、不要な作業を発生させてしまうかもしれません。