Docker & OpenTelemetry: コードを一行も書かずにマイクロサービスを完全可視化

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

マイクロサービスの運用:ログだけでは不十分な理由

エラーチケットを受け取った場面を想像してみてください。「ユーザーが決済できず、システムが10秒間読み込み中になった後、500エラーが発生する」。Dockerで動作する マイクロサービス 環境では、このリクエストは5〜7つの異なるサービスを経由する可能性があります。もし docker logs だけで原因を探そうとすると、タイムスタンプを繋ぎ合わせるだけで数時間を費やすことになります。どのサービスが「ボトルネック」になっているかを特定するのは、まさに苦行です。

OpenTelemetry (OTel) は、この痛みを解決するために誕生しました。トレース(リクエストの軌跡)とメトリクス(システム指標)を中央集約的に収集できます。OTelの最大の強みは Auto-instrumentation(自動計装) です。Java、Python、Node.jsなどのアプリケーションを、ロジックのコードを一行も変更することなく監視できます。すべてはコンテナの外側から設定可能です。

オブザーバビリティへの3つのアプローチ:あなたはどこにいますか?

プロジェクトの各開発段階に応じて、適切な実装方法は異なります。以下は、私がこれまで適用してきた3つの方法です:

  • Manual Instrumentation(手動計装): ライブラリをインストールし、スパン(Span)を作成するコードを記述します。最も詳細なデータが得られますが、サービス数が数十に及ぶと膨大な手間がかかります。
  • Sidecar Pattern: 各アプリケーションコンテナにコレクターコンテナを付随させます。Kubernetesでは一般的ですが、純粋な Docker Compose環境 ではRAMやCPUリソースの無駄が生じる可能性があります。
  • Auto-Instrumentation via Docker Env(推奨): エージェント(.jarファイルやパッケージ)を読み込み、環境変数で設定するだけです。これは、展開の速さ、コードの非侵襲性、メンテナンス性のバランスが最も優れた解決策です。

私の経験上、15分で結果を出したいのであれば、3番目の方法を選択すべきです。

導入アーキテクチャ:データを直接送信しない

アプリケーションからJaegerやPrometheusに直接データを送信するように設定しがちですが、この方法は柔軟性に欠けます。代わりに、OTel Collector をハブアンドスポーク型の仲介役として利用しましょう。

標準的なデータフロー: App Container (Agent) -> OTel Collector -> Storage/Visualization (Jaeger/Grafana)

Collectorを使用することで、バックエンドの変更が容易になります。例えば、JaegerからDatadogに切り替えたい場合、Collectorの設定を5行修正するだけで済みます。フロントエンドの全アプリコンテナには一切影響を与えません。

実践的な導入ガイド

以下は、app.py を修正せずに、Dockerで動作するFlask(Python)アプリケーションにOTelを統合する方法です。

ステップ 1: OTel Collectorの設定

データの受信と送信方法を定義する otel-config.yaml ファイルを作成します。ここでデータの出力先を決定します:

receivers:
  otlp:
    protocols:
      grpc: # デフォルトの4317ポート
      http: # デフォルトの4318ポート

exporters:
  jaeger:
    endpoint: "jaeger:14250"
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [jaeger]

ステップ 2: OTel AgentによるDockerfileの最適化

各ライブラリを手動でインストールする代わりに、opentelemetry-bootstrap を使用します。このコマンドは、Flask、Django、Redisなどのフレームワークを自動的に認識し、適切な計装用パッケージをインストールします。

FROM python:3.9-slim
WORKDIR /app
COPY . .

RUN pip install flask opentelemetry-distro opentelemetry-exporter-otlp
# 必要なプラグインを自動インストール
RUN opentelemetry-bootstrap -a install

# OTelのラッパーを介してアプリケーションを実行
CMD ["opentelemetry-instrument", "python", "app.py"]

ステップ 3: Docker Composeによる連携

補足:ここでは Docker Compose V2(ハイフンなしの docker compose コマンド)を使用します。環境変数がアプリケーションとCollectorを接続する役割を果たします。

services:
  app:
    build: .
    environment:
      - OTEL_SERVICE_NAME=order-service
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
      - OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
    depends_on:
      - otel-collector

  otel-collector:
    image: otel/opentelemetry-collector-contrib
    volumes:
      - ./otel-config.yaml:/etc/otel-collector-config.yaml
    command: ["--config=/etc/otel-collector-config.yaml"]

  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"

実践のコツとハマりやすいポイント

実際の導入で苦労した経験から、3つの重要な注意点をまとめました:

  • Docker内のDNS問題: アプリの環境変数で localhost:4317 を絶対に使用しないでください。コンテナはCollectorではなく、自分自身を参照してしまいます。Composeファイルで設定したサービス名を使用してください。
  • サンプリングレートの制御: デフォルトではOTelは100%のデータを送信します。秒間1,000リクエストを超えるシステムでは、帯域を使い果たしてしまいます。OTEL_TRACES_SAMPLER=traceidratio を使用し、値を 0.1 に設定してデータの10%のみを取得するようにしましょう。
  • Collector의 デバッグ: Jaegerに何も表示されない場合は、docker logs otel-collector でCollectorのログを確認してください。多くの場合、gRPCとHTTPプロトコルの不一致が原因です。

おわりに

DockerへのOpenTelemetryの導入は、マイクロサービスをマスターするための第一歩です。この方法を使えば、ソースコードを汚すことなく、各リクエストを詳細に把握できます。データが一箇所に集まれば、パフォーマンスの最適化は、障害発生時の恐怖ではなく、エキサイティングな課題へと変わるはずです。

Share: