Jaeger

Jaeger 分散トレーシング 完全ガイド


目次

  1. はじめに
  2. コアコンセプト
  3. Jaeger アーキテクチャ
  4. デプロイメントパターン
  5. Jaeger UI と可視化機能
  6. セキュリティ
  7. 運用とベストプラクティス
  8. 高度なトピック
  9. 実践的なユースケース
  10. 他のトレーシングツールとの比較
  11. まとめと今後の展望
  12. 付録

1. はじめに

1.1 分散トレーシングとは何か

現代のソフトウェアアーキテクチャは、モノリシックなアプリケーションからマイクロサービスベースのアーキテクチャへと急速に移行している。この移行により、数十から数百のサービスが互いに連携してひとつのリクエストを処理する構成が一般的になった。このような環境では、ひとつのユーザーリクエストが複数のサービスを横断して処理されるため、問題の特定やパフォーマンスのボトルネックの発見が極めて困難になる。

分散トレーシング(Distributed Tracing)は、この課題を解決するための観測可能性(Observability)技術のひとつである。分散トレーシングでは、リクエストがシステム内のサービスを通過する際の経路を追跡し、各サービスでの処理時間、呼び出し関係、エラー情報などを記録する。これにより、以下のことが可能になる。

  • レイテンシのボトルネック特定: どのサービスがリクエスト全体の遅延の原因になっているかを可視化する
  • 障害の根本原因分析: エラーがどのサービスで発生し、どのように伝播したかを追跡する
  • サービス間の依存関係の把握: 実際のトラフィックに基づいて、サービス間の呼び出し関係を明らかにする
  • パフォーマンス最適化: 各サービスの処理時間の分布を分析し、最適化の優先順位を決定する

1.2 Jaeger の概要

Jaeger(イェーガー、ドイツ語で「猟師」の意)は、Uber Technologies が開発したオープンソースの分散トレーシングプラットフォームである。2017年に Uber がオープンソースとして公開し、その後 Cloud Native Computing Foundation(CNCF)に寄贈された。2019年に CNCF の Graduated プロジェクトに昇格し、Kubernetes や Prometheus と同等の成熟度を持つプロジェクトとして認定されている。

Jaeger は Google の Dapper 論文と Twitter の Zipkin に着想を得て開発された。主な特徴は以下の通りである。

  • OpenTelemetry ネイティブ対応: OpenTelemetry SDK およびプロトコルとのシームレスな統合
  • 高いスケーラビリティ: 大規模環境での運用を前提とした水平スケーリング設計
  • 柔軟なストレージバックエンド: Cassandra、Elasticsearch、Kafka、Badger など複数のストレージに対応
  • 豊富な可視化機能: トレースの検索、比較、依存関係グラフの表示
  • 適応型サンプリング: トラフィック量に応じた動的なサンプリングレートの調整
  • サービスパフォーマンス監視(SPM): RED メトリクス(Rate、Error、Duration)の自動生成

1.3 Jaeger の歴史とエコシステムにおける位置づけ

Jaeger の発展は、分散トレーシングの標準化の歴史と密接に関連している。

出来事
2012Google が Dapper 論文を発表
2012Twitter が Zipkin をオープンソースとして公開
2015OpenTracing プロジェクトが発足(CNCF)
2016Uber が Jaeger を社内で開発・運用開始
2017Jaeger をオープンソースとして公開、CNCF に Incubation プロジェクトとして寄贈
2017W3C Trace Context 仕様の策定開始
2019Jaeger が CNCF Graduated プロジェクトに昇格
2019OpenTelemetry プロジェクトが発足(OpenTracing と OpenCensus の統合)
2021OpenTelemetry Tracing 仕様が GA(Generally Available)に到達
2022Jaeger が OpenTelemetry SDK を公式に推奨し始める
2023Jaeger v2 の開発が開始(OpenTelemetry Collector ベースのアーキテクチャ)
2024Jaeger v2 が正式リリース、アーキテクチャが大幅に刷新

現在の Observability エコシステムにおいて、Jaeger は以下のように位置づけられる。

┌─────────────────────────────────────────────────────────┐
│                 Observability の三本柱                    │
├──────────────┬──────────────────┬────────────────────────┤
│   Metrics    │     Logs         │      Traces            │
│ (Prometheus) │ (Loki/Fluentd)  │  (Jaeger/Tempo/Zipkin) │
└──────────────┴──────────────────┴────────────────────────┘
         │              │                    │
         └──────────────┴────────────────────┘
                        │
              ┌─────────────────┐
              │ OpenTelemetry   │
              │ (統一的な計装)    │
              └─────────────────┘

OpenTelemetry はテレメトリデータの生成・収集の標準を提供し、Jaeger はトレースデータの保存・クエリ・可視化を担当するバックエンドとして機能する。この役割分担により、アプリケーション開発者は OpenTelemetry SDK で計装を行い、バックエンドは Jaeger、Tempo、Zipkin などから自由に選択できる。


2. コアコンセプト

2.1 トレーシングの基本用語

Trace(トレース)

トレースは、分散システムを横断するひとつのリクエストの完全な実行パスを表す。トレースは複数のスパンで構成され、有向非巡回グラフ(DAG: Directed Acyclic Graph)の構造を持つ。

Trace ID: abc123

[Service A: HTTP GET /api/orders] ─────────────────────────────────
    │
    ├── [Service B: GetUserProfile] ──────────────
    │
    ├── [Service C: QueryOrders] ──────────────────────
    │       │
    │       └── [Service D: QueryDatabase] ────────
    │
    └── [Service E: CheckPermissions] ──────

Span(スパン)

スパンは、トレース内のひとつの論理的な作業単位を表す。各スパンは以下の情報を含む。

フィールド説明
Trace IDこのスパンが属するトレースの一意識別子(128ビット)
Span IDこのスパンの一意識別子(64ビット)
Parent Span ID親スパンの識別子(ルートスパンの場合はなし)
Operation Nameこのスパンが表す操作の名前(例: HTTP GET /api/orders
Start Timestamp操作の開始時刻(マイクロ秒精度)
Duration操作の所要時間
Tags / Attributesキーバリューペアのメタデータ(例: http.status_code=200
Logs / Eventsタイムスタンプ付きのイベント記録
Span Kindスパンの種類(Client、Server、Producer、Consumer、Internal)
Statusスパンのステータス(OK、Error、Unset)

スパンの種類(Span Kind):

  • Client: リモートサービスへのリクエストを送信する側
  • Server: リモートリクエストを受信して処理する側
  • Producer: 非同期メッセージを送信する側(Kafka プロデューサーなど)
  • Consumer: 非同期メッセージを受信する側(Kafka コンシューマーなど)
  • Internal: サービス内部の処理。リモート呼び出しを伴わない操作

SpanContext(スパンコンテキスト)

SpanContext は、プロセス間でトレースの因果関係を伝播させるための情報を保持する。

  • Trace ID: トレース全体の一意識別子
  • Span ID: 現在のスパンの一意識別子
  • Trace Flags: サンプリング判断などのフラグ
  • Trace State: ベンダー固有の追加情報

W3C Trace Context 仕様では、以下の HTTP ヘッダーを使用してスパンコンテキストを伝播する。

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: jaeger=value1,other=value2

traceparent ヘッダーのフォーマット:

version-trace_id-parent_id-trace_flags
00     -4bf92f...-00f067...-01

version:     00(現在のバージョン)
trace_id:    32文字の16進数(128ビット)
parent_id:   16文字の16進数(64ビット)
trace_flags: 2文字の16進数(01 = サンプリング対象)

Baggage(バゲージ)

バゲージは、トレースコンテキストとともにサービス間を伝播するキーバリューペアのメタデータである。スパンのタグとは異なり、バゲージはリクエストパス上のすべてのダウンストリームサービスに自動的に伝播される。

Client → Service A → Service B → Service C
  │         │            │           │
  └─ baggage: user_id=12345 ────────────→ (すべてのサービスで参照可能)

典型的な用途:ユーザー ID やテナント ID の伝播、A/B テストのバリアント情報、デバッグフラグ、リクエストの優先度情報。

2.2 サンプリング戦略

すべてのリクエストのトレースデータを記録するのは、多くの本番環境において現実的ではない。サンプリングは、収集するトレースの量を制御するメカニズムである。

Head-based サンプリング

トレースの開始時にサンプリング判断を行う方式。

1. 定数サンプリング(Const Sampler)

sampler:
  type: const
  param: 1  # 1 = すべて記録、0 = すべて無視

2. 確率的サンプリング(Probabilistic Sampler)

sampler:
  type: probabilistic
  param: 0.1  # 10% のトレースを記録

3. レートリミティングサンプリング(Rate Limiting Sampler)

sampler:
  type: ratelimiting
  param: 2.0  # 1秒あたり最大2トレース

4. リモートサンプリング(Remote Sampler)

sampler:
  type: remote
  param: 0.1  # サーバーから取得できない場合のデフォルト値
  sampling_server_url: http://jaeger-collector:14268/api/sampling

適応型サンプリング(Adaptive Sampling)

エンドポイントごとのトラフィック量に基づいて、自動的にサンプリングレートを調整する。

高トラフィックのエンドポイント:
  /api/healthcheck → サンプリング率: 0.1%

低トラフィックのエンドポイント:
  /api/admin/config → サンプリング率: 100%

目標: 各エンドポイントから最低 N 件/秒のトレースを確保する

設定例:

extensions:
  jaeger_storage:
    backends:
      some_storage:
        memory:
          max_traces: 100000

  remote_sampling:
    adaptive:
      sampling_store: some_storage
      target_samples_per_second: 1.0
      default_sampling_probability: 0.1
      calculation_interval: 30s
      aggregation_buckets: 10

Tail-based サンプリング

トレース全体が完了した後にサンプリング判断を行う方式。エラーが発生したトレースや異常に遅いトレースを確実に記録できる。

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    expected_new_traces_per_sec: 1000
    policies:
      - name: errors-policy
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: latency-policy
        type: latency
        latency:
          threshold_ms: 5000
      - name: probabilistic-policy
        type: probabilistic
        probabilistic:
          sampling_percentage: 10
      - name: specific-endpoint
        type: string_attribute
        string_attribute:
          key: http.route
          values: [/api/checkout, /api/payment]

2.3 コンテキスト伝播(Context Propagation)

W3C Trace Context(推奨)

traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
tracestate: jaeger=abc123

B3 Propagation(Zipkin 互換)

b3: 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1

Jaeger Propagation(レガシー)

uber-trace-id: 4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:01

OpenTelemetry SDK での伝播設定例:

from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.b3 import B3MultiFormat

set_global_textmap(CompositePropagator([
    TraceContextTextMapPropagator(),  # W3C Trace Context
    B3MultiFormat(),                   # B3(Zipkin互換)
]))

2.4 OpenTelemetry との統合

Jaeger v2 では、OpenTelemetry との統合が大幅に強化された。公式推奨はアプリケーションの計装に OpenTelemetry SDK を使用することである。

┌─────────────────────────────────────────────────────┐
│                   Application                        │
│  ┌──────────────┐  ┌──────────────────┐             │
│  │ OTel SDK     │  │ Auto-            │             │
│  │ (Manual)     │  │ Instrumentation  │             │
│  └──────┬───────┘  └──────┬───────────┘             │
│         └────────┬─────────┘                         │
│         ┌────────▼────────┐                          │
│         │  TracerProvider  │                          │
│         │  + SpanProcessor │                          │
│         │  + Sampler       │                          │
│         └────────┬────────┘                          │
│         ┌────────▼────────┐                          │
│         │  SpanExporter   │                          │
│         │  (OTLP)         │                          │
│         └────────┬────────┘                          │
└──────────────────┼──────────────────────────────────┘
                   │ OTLP (gRPC/HTTP)
                   ▼
          ┌────────────────┐
          │ Jaeger Backend │
          └────────────────┘

Python での計装例

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from flask import Flask

resource = Resource.create({
    ResourceAttributes.SERVICE_NAME: "order-service",
    ResourceAttributes.SERVICE_VERSION: "1.2.0",
    ResourceAttributes.DEPLOYMENT_ENVIRONMENT: "production",
})

provider = TracerProvider(resource=resource)
otlp_exporter = OTLPSpanExporter(
    endpoint="http://jaeger-collector:4317",
    insecure=True,
)
provider.add_span_processor(
    BatchSpanProcessor(otlp_exporter, max_queue_size=2048, max_export_batch_size=512)
)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)

app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()

@app.route("/api/orders/<order_id>")
def get_order(order_id):
    with tracer.start_as_current_span("get_order_details",
        attributes={"order.id": order_id, "order.type": "standard"}
    ) as span:
        with tracer.start_as_current_span("query_database") as db_span:
            db_span.set_attribute("db.system", "postgresql")
            db_span.set_attribute("db.statement", "SELECT * FROM orders WHERE id = ?")
            order = query_order_from_db(order_id)

        with tracer.start_as_current_span("call_inventory_service") as inv_span:
            inv_span.set_attribute("peer.service", "inventory-service")
            inventory = check_inventory(order)

        if order is None:
            span.set_status(trace.StatusCode.ERROR, "Order not found")
            span.record_exception(ValueError(f"Order {order_id} not found"))
            return {"error": "Not found"}, 404
        return {"order": order, "inventory": inventory}

Go での計装例

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
    "go.opentelemetry.io/otel/trace"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func initTracer() (*sdktrace.TracerProvider, error) {
    ctx := context.Background()
    exporter, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithEndpoint("jaeger-collector:4317"),
        otlptracegrpc.WithInsecure(),
    )
    if err != nil {
        return nil, fmt.Errorf("failed to create exporter: %w", err)
    }

    res, _ := resource.Merge(
        resource.Default(),
        resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceName("payment-service"),
            semconv.ServiceVersion("2.1.0"),
            semconv.DeploymentEnvironment("production"),
        ),
    )

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter,
            sdktrace.WithBatchTimeout(5*time.Second),
            sdktrace.WithMaxQueueSize(2048),
        ),
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)),
    )

    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
        propagation.TraceContext{}, propagation.Baggage{},
    ))
    return tp, nil
}

func main() {
    tp, err := initTracer()
    if err != nil { log.Fatal(err) }
    defer tp.Shutdown(context.Background())

    tracer := otel.Tracer("payment-service")
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx, span := tracer.Start(r.Context(), "process_payment",
            trace.WithAttributes(
                attribute.String("payment.method", "credit_card"),
                attribute.Float64("payment.amount", 99.99),
            ),
        )
        defer span.End()
        // ビジネスロジック
    })

    wrappedHandler := otelhttp.NewHandler(handler, "ProcessPayment")
    http.Handle("/api/payment", wrappedHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Java での計装例

import io.opentelemetry.api.trace.*;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.semconv.ResourceAttributes;
import java.time.Duration;

public class TracingConfig {
    public static OpenTelemetry initOpenTelemetry() {
        OtlpGrpcSpanExporter exporter = OtlpGrpcSpanExporter.builder()
            .setEndpoint("http://jaeger-collector:4317")
            .setTimeout(Duration.ofSeconds(10))
            .build();

        Resource resource = Resource.getDefault().merge(
            Resource.create(Attributes.of(
                ResourceAttributes.SERVICE_NAME, "user-service",
                ResourceAttributes.SERVICE_VERSION, "3.0.1",
                ResourceAttributes.DEPLOYMENT_ENVIRONMENT, "production"
            ))
        );

        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
            .addSpanProcessor(BatchSpanProcessor.builder(exporter)
                .setMaxQueueSize(2048).setMaxExportBatchSize(512)
                .setScheduleDelay(Duration.ofSeconds(5)).build())
            .setResource(resource).build();

        return OpenTelemetrySdk.builder()
            .setTracerProvider(tracerProvider).buildAndRegisterGlobal();
    }
}

3. Jaeger アーキテクチャ

3.1 Jaeger v2 アーキテクチャ概要

Jaeger v2 は、OpenTelemetry Collector をベースとした全面的に再設計されたアーキテクチャを採用している。v1 からの最大の変更点は、独自のコンポーネント(Agent、Collector、Query)を統合し、単一の jaeger バイナリで全機能を提供するようになったことである。

┌────────────────────────────────────────────────────────────────┐
│                    Jaeger v2 アーキテクチャ                     │
│                                                                │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐              │
│  │ Service A  │  │ Service B  │  │ Service C  │              │
│  │ (OTel SDK) │  │ (OTel SDK) │  │ (OTel SDK) │              │
│  └─────┬──────┘  └─────┬──────┘  └─────┬──────┘              │
│        │ OTLP          │ OTLP          │ OTLP                │
│        └───────────────┼───────────────┘                     │
│                        ▼                                      │
│        ┌───────────────────────────────┐                     │
│        │       Jaeger v2 Binary        │                     │
│        │  ┌─────────────────────────┐  │                     │
│        │  │  OTel Collector Core    │  │                     │
│        │  │  Receivers → Processors │  │                     │
│        │  │           → Exporters   │  │                     │
│        │  └────────────┬────────────┘  │                     │
│        │  ┌────────────▼────────────┐  │                     │
│        │  │  Jaeger Extensions      │  │                     │
│        │  │  (Storage, Query,       │  │                     │
│        │  │   Remote Sampling)      │  │                     │
│        │  └────────────┬────────────┘  │                     │
│        └───────────────┼───────────────┘                     │
│                        ▼                                      │
│        ┌───────────────────────────────┐                     │
│        │     Storage Backend           │                     │
│        │  (Cassandra / Elasticsearch   │                     │
│        │   / Badger / gRPC Remote)     │                     │
│        └───────────────────────────────┘                     │
│                                                                │
│        ┌───────────────────────────────┐                     │
│        │   Jaeger UI (React) + API    │◄── ブラウザ          │
│        └───────────────────────────────┘                     │
└────────────────────────────────────────────────────────────────┘

3.2 v1 と v2 のアーキテクチャ比較

特性Jaeger v1Jaeger v2
コアランタイム独自実装OpenTelemetry Collector
バイナリ複数(agent, collector, query, ingester)単一(jaeger)
Agent必須(UDP でデータ受信)不要(OTLP で直接送信)
受信プロトコルJaeger Thrift (UDP/HTTP), ZipkinOTLP (gRPC/HTTP), Jaeger, Zipkin
推奨 SDKJaeger Client LibrariesOpenTelemetry SDK
設定方式CLI フラグ + 環境変数YAML 設定ファイル
拡張性限定的OTel Collector のプラグインエコシステム
パイプライン固定柔軟(Receiver→Processor→Exporter)
SPM別コンポーネント統合済み

3.3 コンポーネント詳細

3.3.1 Receivers(レシーバー)

OTLP Receiver(推奨)

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: "0.0.0.0:4317"
        max_recv_msg_size_mib: 4
        max_concurrent_streams: 100
        tls:
          cert_file: /certs/server.crt
          key_file: /certs/server.key
          client_ca_file: /certs/ca.crt
      http:
        endpoint: "0.0.0.0:4318"

Jaeger Receiver(後方互換性)

receivers:
  jaeger:
    protocols:
      grpc:
        endpoint: "0.0.0.0:14250"
      thrift_http:
        endpoint: "0.0.0.0:14268"
      thrift_compact:
        endpoint: "0.0.0.0:6831"

3.3.2 Processors(プロセッサー)

processors:
  batch:
    send_batch_size: 10000
    send_batch_max_size: 11000
    timeout: 10s

  attributes:
    actions:
      - key: environment
        value: production
        action: upsert
      - key: db.password
        action: delete

  filter:
    error_mode: ignore
    traces:
      span:
        - 'attributes["http.route"] == "/healthz"'

  resource:
    attributes:
      - key: cloud.region
        value: ap-northeast-1
        action: upsert

3.3.3 Exporters(エクスポーター)

exporters:
  jaeger_storage_exporter:
    trace_storage: main_storage

  otlp:
    endpoint: "tempo.monitoring.svc:4317"
    tls:
      insecure: false
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s

3.3.4 Extensions(拡張機能)

extensions:
  jaeger_storage:
    backends:
      main_storage:
        elasticsearch:
          server_urls: http://elasticsearch:9200
          index_prefix: jaeger
          tags_as_fields:
            all: true
          num_shards: 5
          num_replicas: 1
      archive_storage:
        elasticsearch:
          server_urls: http://elasticsearch-archive:9200
          index_prefix: jaeger-archive

  jaeger_query:
    storage:
      traces: main_storage
      traces_archive: archive_storage
    grpc:
      endpoint: "0.0.0.0:16685"
    http:
      endpoint: "0.0.0.0:16686"

3.4 ストレージバックエンド

3.4.1 Elasticsearch / OpenSearch

extensions:
  jaeger_storage:
    backends:
      es_main:
        elasticsearch:
          server_urls: "http://es-node1:9200,http://es-node2:9200,http://es-node3:9200"
          index_prefix: "jaeger"
          num_shards: 5
          num_replicas: 1
          create_index_templates: true
          use_ilm: true
          ilm_policy_name: "jaeger-traces-policy"
          bulk_size: 5000000
          bulk_workers: 3
          bulk_actions: 1000
          bulk_flush_interval: 200ms
          tags_as_fields:
            all: true
          tls:
            enabled: true
            ca_file: /certs/ca.crt
          username: jaeger
          password: "${ES_PASSWORD}"

3.4.2 Apache Cassandra

extensions:
  jaeger_storage:
    backends:
      cassandra_main:
        cassandra:
          servers: "cassandra-node1,cassandra-node2,cassandra-node3"
          port: 9042
          keyspace: "jaeger_v1_production"
          connections_per_host: 3
          consistency: LOCAL_QUORUM
          schema:
            datacenter: dc1
            replication_factor: 3
          span_store_write_cache_ttl: 12h

3.4.3 Badger(組み込みストレージ)

extensions:
  jaeger_storage:
    backends:
      badger_storage:
        badger:
          directories:
            keys: /var/jaeger/badger/keys
            values: /var/jaeger/badger/values
          ephemeral: false
          span_store_ttl: 72h

3.4.4 Kafka を使用したアーキテクチャ

Services ──→ Jaeger Collector ──→ Kafka ──→ Jaeger Ingester ──→ Storage

Collector 側(Kafka への書き込み):

exporters:
  kafka:
    protocol_version: "3.0.0"
    brokers: [kafka-broker1:9092, kafka-broker2:9092, kafka-broker3:9092]
    topic: "jaeger-spans"
    encoding: otlp_proto
    producer:
      compression: zstd
      required_acks: all

Ingester 側(Kafka からの読み取り):

receivers:
  kafka:
    brokers: [kafka-broker1:9092, kafka-broker2:9092, kafka-broker3:9092]
    topic: "jaeger-spans"
    group_id: "jaeger-ingester"
    encoding: otlp_proto

3.4.5 ストレージバックエンドの選択ガイド

特性ElasticsearchCassandraBadgerKafka+ES
スケーラビリティ非常に高非常に高
検索機能全文検索対応制限的基本的ES 依存
書き込み性能中〜高非常に高非常に高
運用の複雑さ
推奨環境中〜大規模超大規模開発・テスト大規模・高信頼

4. デプロイメントパターン

4.1 All-in-One デプロイメント

# docker-compose.yml
services:
  jaeger:
    image: jaegertracing/jaeger:2.4
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
      - "16686:16686" # Jaeger UI
    volumes:
      - ./jaeger-config.yaml:/etc/jaeger/config.yaml
    command: ["--config", "/etc/jaeger/config.yaml"]
# jaeger-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: "0.0.0.0:4317"
      http:
        endpoint: "0.0.0.0:4318"

processors:
  batch:
    send_batch_size: 10000
    timeout: 5s

extensions:
  jaeger_storage:
    backends:
      some_storage:
        memory:
          max_traces: 100000
  jaeger_query:
    storage:
      traces: some_storage
    http:
      endpoint: "0.0.0.0:16686"

exporters:
  jaeger_storage_exporter:
    trace_storage: some_storage

service:
  extensions: [jaeger_storage, jaeger_query]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger_storage_exporter]

4.2 Kubernetes デプロイメント

helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm install jaeger jaegertracing/jaeger \
  --namespace observability \
  --create-namespace \
  --values values-production.yaml
# values-production.yaml
collector:
  replicaCount: 3
  resources:
    requests: { cpu: 500m, memory: 512Mi }
    limits: { cpu: 2000m, memory: 2Gi }
  autoscaling:
    enabled: true
    minReplicas: 3
    maxReplicas: 10

query:
  replicaCount: 2
  ingress:
    enabled: true
    hosts:
      - host: jaeger.example.com
        paths: [{ path: /, pathType: Prefix }]

storage:
  type: elasticsearch
  elasticsearch:
    host: elasticsearch.observability.svc
    port: 9200

Kubernetes マニフェスト(直接定義)

apiVersion: v1
kind: ConfigMap
metadata:
  name: jaeger-config
  namespace: observability
data:
  config.yaml: |
    receivers:
      otlp:
        protocols:
          grpc: { endpoint: "0.0.0.0:4317" }
          http: { endpoint: "0.0.0.0:4318" }
    processors:
      batch: { send_batch_size: 10000, timeout: 10s }
      memory_limiter: { check_interval: 1s, limit_mib: 1500, spike_limit_mib: 512 }
    extensions:
      jaeger_storage:
        backends:
          main_storage:
            elasticsearch:
              server_urls: "https://elasticsearch.observability.svc:9200"
              index_prefix: jaeger
              username: jaeger
              password: "${ES_PASSWORD}"
              num_shards: 5
              num_replicas: 1
              tags_as_fields: { all: true }
      jaeger_query:
        storage: { traces: main_storage }
        http: { endpoint: "0.0.0.0:16686" }
    exporters:
      jaeger_storage_exporter: { trace_storage: main_storage }
    service:
      extensions: [jaeger_storage, jaeger_query]
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [jaeger_storage_exporter]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jaeger
  namespace: observability
spec:
  replicas: 3
  selector:
    matchLabels: { app: jaeger }
  template:
    metadata:
      labels: { app: jaeger }
      annotations: { prometheus.io/scrape: "true", prometheus.io/port: "8888" }
    spec:
      containers:
        - name: jaeger
          image: jaegertracing/jaeger:2.4
          args: ["--config=/etc/jaeger/config.yaml"]
          ports:
            - { name: otlp-grpc, containerPort: 4317 }
            - { name: otlp-http, containerPort: 4318 }
            - { name: query-http, containerPort: 16686 }
            - { name: metrics, containerPort: 8888 }
          resources:
            requests: { cpu: 500m, memory: 512Mi }
            limits: { cpu: 2000m, memory: 2Gi }
          livenessProbe:
            httpGet: { path: /, port: 13133 }
          readinessProbe:
            httpGet: { path: /, port: 13133 }
          volumeMounts:
            - { name: config, mountPath: /etc/jaeger }
      volumes:
        - name: config
          configMap: { name: jaeger-config }
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: jaeger
  namespace: observability
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: jaeger
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }

4.3 サイドカーパターン

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  template:
    spec:
      containers:
        - name: order-service
          image: myregistry/order-service:v1.2.0
          env:
            - { name: OTEL_EXPORTER_OTLP_ENDPOINT, value: "http://localhost:4317" }
            - { name: OTEL_SERVICE_NAME, value: "order-service" }
        - name: otel-collector
          image: otel/opentelemetry-collector-contrib:0.96.0
          args: ["--config=/etc/otel/config.yaml"]
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits: { cpu: 500m, memory: 512Mi }

4.4 DaemonSet パターン

各ノードに OTel Collector を DaemonSet としてデプロイするパターン。サイドカーパターンよりもリソース効率が良い。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: otel-collector
  namespace: observability
spec:
  template:
    spec:
      containers:
        - name: otel-collector
          image: otel/opentelemetry-collector-contrib:0.96.0
          ports:
            - { containerPort: 4317, hostPort: 4317 }
            - { containerPort: 4318, hostPort: 4318 }

5. Jaeger UI と可視化機能

5.1 主要画面

  • Search: サービス名、オペレーション名、タグ/属性、時間範囲、レイテンシ範囲でトレースを検索
  • Trace Detail: タイムラインビュー(ガントチャート形式)、スパンの階層構造、各スパンの詳細情報
  • Trace Comparison: 2つのトレースの並列比較と差分の自動検出
  • System Architecture: DAG 形式のサービス依存関係グラフ
  • Monitor (SPM): RED メトリクス(Rate, Error, Duration)のダッシュボード

5.2 トレース検索 API

curl "http://jaeger:16686/api/traces?\
service=order-service&\
operation=HTTP%20POST%20/api/checkout&\
minDuration=5s&\
lookback=3600000000&\
tags=http.status_code%3D500&\
limit=20"

5.3 トレース詳細ビュー

Trace: abc123 | 12 Spans | 2.3s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

order-service: HTTP GET /api/orders/123     [2.3s]
  ├─ user-service: GetUserProfile           [450ms]
  ├─ inventory-service: CheckStock          [850ms]
  │   └─ redis: GET inventory:123           [120ms]
  └─ payment-service: ValidatePayment       [1.2s]  ← ボトルネック
      ├─ fraud-detection: AnalyzeTransaction [800ms]
      └─ bank-gateway: AuthorizePayment     [350ms]

5.4 Service Performance Monitoring(SPM)

SPM はトレースデータから RED メトリクスを自動生成する。

connectors:
  spanmetrics:
    histogram:
      explicit:
        buckets: [2ms, 4ms, 6ms, 8ms, 10ms, 50ms, 100ms, 200ms, 400ms, 800ms, 1s, 5s, 10s]
    dimensions:
      - name: http.method
      - name: http.status_code
      - name: http.route
    exemplars:
      enabled: true

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger_storage_exporter, spanmetrics]
    metrics/spanmetrics:
      receivers: [spanmetrics]
      exporters: [prometheus]

生成されるメトリクスの例:

# リクエストレート
rate(calls_total{service_name="order-service"}[5m])

# エラーレート
rate(calls_total{service_name="order-service", status_code="STATUS_CODE_ERROR"}[5m])
/ rate(calls_total{service_name="order-service"}[5m])

# P99 レイテンシ
histogram_quantile(0.99, rate(duration_milliseconds_bucket{service_name="order-service"}[5m]))

5.5 Grafana との統合

# Grafana データソース設定
datasources:
  - name: Jaeger
    type: jaeger
    url: http://jaeger-query:16686
    jsonData:
      tracesToLogsV2:
        datasourceUid: 'loki'
        filterByTraceID: true
      tracesToMetrics:
        datasourceUid: 'prometheus'
        queries:
          - name: 'Request Rate'
            query: 'rate(calls_total{service_name="${__tags.service_name}"}[5m])'
      nodeGraph:
        enabled: true

6. セキュリティ

6.1 TLS / mTLS の設定

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: "0.0.0.0:4317"
        tls:
          cert_file: /certs/server.crt
          key_file: /certs/server.key
          client_ca_file: /certs/ca.crt  # mTLS
          min_version: "1.3"

6.2 データのサニタイゼーション

processors:
  attributes:
    actions:
      - key: db.statement
        action: delete
      - key: http.request.header.authorization
        action: delete
      - key: user.email
        pattern: '(.).*@(.).*\.(.*)'
        replacement: '$1***@$2***.$3'
        action: update

6.3 RBAC

Jaeger UI へのアクセス制御は、リバースプロキシや OAuth2 Proxy で実装する。Ingress の auth-url アノテーションを使用した認証設定が一般的である。


7. 運用とベストプラクティス

7.1 パフォーマンスチューニング

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 4096
    spike_limit_mib: 1024
    limit_percentage: 80
  batch:
    send_batch_size: 10000
    send_batch_max_size: 11000
    timeout: 10s

service:
  telemetry:
    logs:
      level: warn  # 本番環境ではログレベルを下げる

7.2 監視とアラート

# Prometheus アラートルール
groups:
  - name: jaeger-collector
    rules:
      - alert: JaegerCollectorSpansDropped
        expr: rate(otelcol_receiver_refused_spans_total[5m]) > 0
        for: 5m
        labels: { severity: warning }

      - alert: JaegerCollectorQueueFull
        expr: otelcol_exporter_queue_size / otelcol_exporter_queue_capacity > 0.8
        for: 5m
        labels: { severity: warning }

      - alert: JaegerStorageWriteErrors
        expr: rate(otelcol_exporter_send_failed_spans_total[5m]) > 0
        for: 5m
        labels: { severity: critical }

      - alert: JaegerQueryHighLatency
        expr: histogram_quantile(0.99, rate(jaeger_query_latency_bucket[5m])) > 10
        for: 10m
        labels: { severity: warning }

7.3 トラブルシューティング

スパンが表示されない場合:

# Collector がスパンを受信しているか確認
curl http://jaeger-collector:8888/metrics | grep otelcol_receiver_accepted_spans_total

# ストレージへの書き込みエラーを確認
curl http://jaeger-collector:8888/metrics | grep otelcol_exporter_send_failed_spans_total

メモリ不足の場合:

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 2048       # Pod のメモリ制限の 80% に設定
    spike_limit_mib: 512

7.4 計装のベストプラクティス

スパンの命名規約:

良い例: HTTP GET /api/orders/{id}
悪い例: HTTP GET /api/orders/12345  (高カーディナリティ)

エラーの記録:

with tracer.start_as_current_span("process_order") as span:
    try:
        result = process_order(order_id)
    except ValidationError as e:
        span.set_status(StatusCode.ERROR, str(e))
        span.record_exception(e)
        raise

8. 高度なトピック

8.1 マルチテナンシー

テナントごとのパイプライン分離:

processors:
  filter/tenant_a:
    traces:
      span:
        - 'resource.attributes["tenant.id"] == "tenant-a"'

exporters:
  jaeger_storage_exporter/tenant_a:
    trace_storage: storage_tenant_a
  jaeger_storage_exporter/tenant_b:
    trace_storage: storage_tenant_b

service:
  pipelines:
    traces/tenant_a:
      receivers: [otlp]
      processors: [filter/tenant_a, batch]
      exporters: [jaeger_storage_exporter/tenant_a]

8.2 クロスクラスタートレーシング

┌──────────────────┐     ┌──────────────────┐
│  Cluster A       │     │  Cluster B       │
│  Service A ────────────→ Service B       │
│  OTel Collector  │     │  OTel Collector  │
└────────┬─────────┘     └────────┬─────────┘
         └──────────┬─────────────┘
                    ▼
           Central Jaeger Collector
                    ▼
              Elasticsearch

各クラスターの Collector でリソース属性を付加:

processors:
  resource:
    attributes:
      - key: k8s.cluster.name
        value: cluster-a
        action: upsert

8.3 ClickHouse バックエンド

CREATE TABLE jaeger_spans (
    timestamp DateTime64(6),
    traceID String,
    spanID String,
    parentSpanID String,
    operationName LowCardinality(String),
    serviceName LowCardinality(String),
    duration UInt64,
    tags Map(LowCardinality(String), String)
) ENGINE = MergeTree()
PARTITION BY toDate(timestamp)
ORDER BY (serviceName, operationName, timestamp)
TTL toDateTime(timestamp) + INTERVAL 30 DAY;

8.4 カスタムコンポーネント開発

OpenTelemetry Collector のカスタムプロセッサを Go で開発できる。processor.Factory インターフェースを実装し、ConsumeTraces メソッドでスパンデータを処理する。


9. 実践的なユースケース

9.1 レイテンシ分析

チェックアウト API の P99 レイテンシが 5 秒を超えている場合の分析手順:

  1. 遅いトレースの検索: minDuration=5s で検索
  2. ボトルネックの特定: タイムラインビューで各スパンの所要時間を確認
  3. 根本原因の調査: 最も遅いスパンの属性を確認
  4. 対策の実施と効果測定: 改善後に同じ条件でトレースを比較

9.2 障害の根本原因分析(RCA)

import requests
from collections import Counter

def analyze_errors(service, lookback_hours=1):
    resp = requests.get(f"{JAEGER_URL}/api/traces", params={
        "service": service,
        "tags": "error=true",
        "limit": 200,
    })
    traces = resp.json()["data"]
    error_services = Counter()

    for trace_data in traces:
        for span in trace_data["spans"]:
            is_error = any(
                tag["key"] == "error" and tag["value"] is True
                for tag in span.get("tags", [])
            )
            if is_error:
                process = trace_data["processes"][span["processID"]]
                error_services[process["serviceName"]] += 1

    for svc, count in error_services.most_common(10):
        print(f"  {svc}: {count}")

9.3 CI/CD パイプラインでのトレーシング

デプロイ前後のパフォーマンス比較を自動化し、リグレッション検出に活用する。P50/P95/P99 レイテンシとエラーレートを比較し、20% 以上の悪化があればアラートを発行する。


10. 他のトレーシングツールとの比較

10.1 Jaeger vs Zipkin

特性JaegerZipkin
開発元Uber → CNCFTwitter
言語GoJava
CNCF ステータスGraduatedなし
OTel 統合ネイティブ(v2 は OTel Collector ベース)OTel SDK 経由
適応型サンプリングありなし

10.2 Jaeger vs Grafana Tempo

特性JaegerGrafana Tempo
ストレージES、Cassandra 等オブジェクトストレージ(S3、GCS)
コストストレージに依存非常に低い
検索機能全文検索(ES 使用時)TraceQL
UI自前の UI ありGrafana に依存

10.3 選定ガイドライン

Jaeger を選ぶべき場合:
  - オンプレミスまたはマルチクラウド環境
  - 完全なカスタマイズが必要
  - CNCF エコシステムとの統合
  - 適応型サンプリングが必要

Tempo を選ぶべき場合:
  - Grafana スタックを使用中
  - ストレージコストを最小化したい
  - TraceQL による高度なクエリが必要

Zipkin を選ぶべき場合:
  - シンプルなセットアップを重視
  - 小〜中規模のシステム

11. まとめと今後の展望

11.1 Jaeger v2 のまとめ

  1. 統一されたアーキテクチャ: 単一バイナリへの統合により運用が簡素化
  2. OpenTelemetry ネイティブ: 業界標準との完全な統合
  3. 拡張可能なパイプライン: Receiver→Processor→Exporter モデル
  4. 豊富なストレージ選択肢: ワークロードに応じた最適なバックエンド選択
  5. SPM の統合: RED メトリクスの自動生成

11.2 今後の展望

  • OpenTelemetry Profiling: トレースとプロファイルの相関分析
  • eBPF ベースの自動計装: コード変更なしのトレーシング
  • AI/ML によるトレース分析: 異常検出と根本原因分析の自動化
  • コスト最適化: 列指向ストレージ、適応型サンプリングの高度化

11.3 導入のロードマップ

Phase 1: 評価・PoC(1-2週間)
  - All-in-One デプロイで評価
  - 1-2 サービスに OTel SDK を導入

Phase 2: パイロット運用(2-4週間)
  - ES/Cassandra のセットアップ
  - 主要サービスの計装

Phase 3: 本番展開(4-8週間)
  - 全サービスへの計装展開
  - 適応型サンプリング、SPM、Grafana 統合

Phase 4: 最適化・高度化(継続的)
  - Tail-based サンプリング
  - カスタムプロセッサ開発
  - CI/CD パイプライン統合

付録

A. ポート番号一覧

ポートプロトコル用途
4317gRPCOTLP Receiver(推奨)
4318HTTPOTLP Receiver(推奨)
14250gRPCJaeger gRPC Receiver(レガシー)
14268HTTPJaeger HTTP Receiver(レガシー)
6831UDPJaeger Compact Thrift(レガシー)
6832UDPJaeger Binary Thrift(レガシー)
9411HTTPZipkin Receiver
16686HTTPJaeger Query UI
16685gRPCJaeger Query gRPC
8888HTTPCollector メトリクス
8889HTTPSPM メトリクス(Prometheus)
13133HTTPヘルスチェック

B. 環境変数一覧(OpenTelemetry SDK 共通)

環境変数説明デフォルト値
OTEL_SERVICE_NAMEサービス名unknown_service
OTEL_EXPORTER_OTLP_ENDPOINTOTLP エンドポイントhttp://localhost:4317
OTEL_EXPORTER_OTLP_PROTOCOLプロトコルgrpc
OTEL_TRACES_SAMPLERサンプラータイプparentbased_always_on
OTEL_TRACES_SAMPLER_ARGサンプラーパラメータなし
OTEL_RESOURCE_ATTRIBUTESリソース属性なし
OTEL_PROPAGATORSコンテキスト伝播形式tracecontext,baggage
OTEL_BSP_SCHEDULE_DELAYバッチエクスポート間隔5000ms
OTEL_BSP_MAX_QUEUE_SIZEキューの最大サイズ2048
OTEL_BSP_MAX_EXPORT_BATCH_SIZEバッチの最大サイズ512

C. 参考リンク