JaegerとDockerによる分散トレーシング:マイクロサービスのための実践ガイド

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

クイックスタート:5分でJaegerを起動する

本番環境の障害発生から3時間、数百万件のログをスクロールしながら気づくはずです。ログは「症状」を示しますが、トレースは「原因」を示します。1回のユーザーのクリックが5つのAPI呼び出しと2つのデータベースクエリをトリガーする場合, 必要なのはリストではなくマップです。JaegerとDockerを使って、そのマップを5分以内に作成しましょう。

docker-compose.ymlファイルを使用して、Jaegerの「all-in-one」イメージを起動します。このバージョンは開発者にとっての万能ナイフのようなもので、UI、コレクター、クエリエンジンを1つの軽量なコンテナにまとめています。

version: '3.8'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686" # Jaeger UI
      - "4317:4317"   # OTLP gRPC通信用
      - "4318:4318"   # OTLP HTTP通信用
    environment:
      - COLLECTOR_OTLP_ENABLED=true

  service-a:
    image: node:18
    working_dir: /app
    volumes:
      - ./service-a:/app
    command: npm start
    environment:
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
    depends_on:
      - jaeger

docker-compose up -dを実行し、http://localhost:16686にアクセスしてください。私の経験では、このセットアップが多くのチームにとっての転換点となります。デバッグを「推測」から「視覚的な科学」へと変えてくれるからです。

ディープダイブ:トレースの構造

Jaegerを使いこなすには、「スパン(Span)」と「トレース(Trace)」という2つの概念を理解する必要があります。トレースをリクエストの「物語全体」と考えるなら、スパンは「個別の章」です。例えば、特定のデータベースクエリや、ダウンストリームサービスへの200ミリ秒のAPI呼び出しなどがスパンにあたります。

オブザーバビリティの論理

コンテキスト伝播(Context propagation)が鍵となります。サービスAがサービスBを呼び出す際、「トレースID」を渡す必要があります。Jaegerは魔法を使ってサービスをリンクしているわけではありません。代わりに、コードがHTTPヘッダーにこのIDを挿入します。通常はW3Cのtraceparent標準が使用されます。

レガシーなJaeger SDKのことは忘れてください。現在はOpenTelemetry(OTel)が業界標準です。Jaegerはこれをネイティブにサポートしており、スタックの将来性を保証します。以下は、Docker化された Jaegerインスタンスと通信するためにNode.jsサービスを計測(インストルメンテーション)する方法です。

// instrumentation.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
  }),
  serviceName: 'order-service',
});

sdk.start();

all-in-one Dockerイメージが優れているのは、CassandraやElasticsearchのセットアップの手間を省ける点です。ただし、すべてをRAMに保存することを忘れないでください。コンテナを再起動すると、データは消失します。ローカルでの開発には最適ですが、本番環境には決して使用しないでください。

高度な利用:本番環境のトラフィック処理

スケールが変わればすべてが変わります。トレーシングは膨大なデータを生成します。システムが毎秒1,000リクエストを処理し、各リクエストが10個のスパンを作成する場合、毎秒10,000個のスパンが発生することになります。これは基本的なセットアップを数分でクラッシュさせるでしょう。

スマートサンプリング

すべてのリクエストを記録する必要はありません。本番環境では、確率的サンプリング(Probabilistic Sampling)を使用します。トラフィックのわずか1%を記録するだけでも、ストレージ予算を使い果たすことなくパフォーマンスの傾向を把握するには十分な場合がほとんどです。

# 環境変数による1%サンプリングの設定
OTEL_TRACES_SAMPLER=parentbased_always_off
OTEL_TRACES_SAMPLER_ARG=0.01

永続ストレージへの移行

本格的なデプロイメントでは、Jaegerのコレクター(Collector)サービスとクエリ(Query)サービスを分割する必要があります。コレクターがアプリからデータを取得してElasticsearchに書き込み、クエリサービスがそのデータをUI用に読み込みます。この分離により、トラフィックの増加に合わせてコンポーネントを個別にスケールさせることが可能になります。

# 本番環境向けのストレージ設定
jaeger-collector:
  image: jaegertracing/jaeger-collector
  environment:
    - SPAN_STORAGE_TYPE=elasticsearch
    - ES_SERVER_URLS=http://elasticsearch:9200

jaeger-query:
  image: jaegertracing/jaeger-query
  environment:
    - SPAN_STORAGE_TYPE=elasticsearch
    - ES_SERVER_URLS=http://elasticsearch:9200

現場からの実践的な教訓

私はこれまで数百のマイクロサービスを管理してきました。分散トレーシングを始める前に知っておきたかったことを共有します。

1. カスタムタグが窮地を救う

標準的なトレースも役立ちますが、ビジネスドメインのタグはさらに有用です。customer_idorder_idなどのIDを常にスパンに付与しましょう。重要なお客様から午前2時に「決済が遅い」というクレームが来たとしても、特定のトレースを数秒で見つけ出すことができます。

const span = tracer.startSpan('process-payment');
span.setAttribute('order.id', '12345');
span.setAttribute('payment.provider', 'stripe');
span.end();

2. パフォーマンスへの影響(税金)に注意

トレーシングは無料(リソース消費ゼロ)ではありませんが、賢く使えば低コストです。gRPC(ポート4317)を使用することは、HTTPよりも大幅に効率的です。私たちのベンチマークでは、OTelの計測によるCPUオーバーヘッドは通常2%未満です。ただし、エクスポーターが非ブロッキングであることを確認してください。そうすれば、万が一Jaegerがダウンしてもアプリケーションが停止することはありません。

3. 依存関係グラフを活用する

Jaeger UIの「Dependencies(依存関係)」タブは隠れた逸品です。実際のトラフィックに基づいて、アーキテクチャのライブマップを構築してくれます。これは、チームが半年間更新していないREADMEや構成図よりもはるかに正確です。

4. 「ゴースト」スパンの追跡

サービスAがサービスBを呼び出しているのに、トレースにBが現れない場合は、ミドルウェアを確認してください。プロキシやロードバランサーがヘッダーを削除している可能性があります。パブリックゲートウェイからプライベートネットワークに渡る際など、すべてのホップでトレースコンテキストが維持されていることを必ず確認してください。

分散トレーシングはもはや贅沢品ではなく、モダンなオブザーバビリティのバックボーンです。まずはDockerでその感覚を掴み、ワークフローに深く組み込むことで、これまで「不可能」だったバグの修正に終止符を打ちましょう。

Share: