LESSON 40分

ストーリー

佐藤CTO
マイクロサービスで障害が起きたとき、どのサービスが原因かを特定するのが最も困難な課題だ
あなた
先日の決済遅延も、API Gateway、Payment Service、外部決済API、DBの4つのサービスが関わっていて、原因の特定に2時間かかりました
佐藤CTO
分散トレーシングがあれば、リクエストの経路を1本の線で追跡できる。2時間が5分になる可能性がある

分散トレーシングの基礎

基本概念

概念説明
Trace1つのリクエストの全体の旅路。複数のSpanで構成される
Span1つのサービスまたは処理単位での作業。開始時刻、期間、属性を持つ
Trace ID1つのトレースを識別するユニークID。全サービスで共有される
Span ID各Spanを識別するユニークID
Parent Span ID親Spanを参照するID。Span間の親子関係を表現
Context Propagationサービス間でTrace/Span IDを伝搬する仕組み

トレースの構造

Trace ID: abc-123-def-456

  API Gateway [Span A] ──────────────────────────────

    ├─→ Auth Service [Span B] ────────

    ├─→ Payment Service [Span C] ─────────────────────
    │     │
    │     ├─→ DB Query [Span D] ──────
    │     │
    │     └─→ External Payment API [Span E] ──────────

    └─→ Notification Service [Span F] ────

  時間 ──────────────────────────────────────────────→
  0ms        100ms       200ms       300ms      400ms

Context Propagation

// W3C Trace Context ヘッダー
// リクエストヘッダーに Trace Context を伝搬する

// 送信側
const headers = {
  'traceparent': '00-abc123def456-span789-01',
  // version-trace_id-parent_id-trace_flags
  'tracestate': 'vendor1=value1,vendor2=value2',
};

// OpenTelemetryによる自動Context伝搬
import { context, propagation } from '@opentelemetry/api';

async function callPaymentService(orderId: string): Promise<PaymentResult> {
  // 現在のSpanのContextを自動的にヘッダーに注入
  const headers: Record<string, string> = {};
  propagation.inject(context.active(), headers);

  const response = await fetch('http://payment-service/api/payments', {
    method: 'POST',
    headers: {
      ...headers,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ orderId }),
  });

  return response.json();
}

手動計装(Manual Instrumentation)

import { trace, SpanKind, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('payment-service', '1.0.0');

async function processPayment(
  orderId: string,
  amount: number
): Promise<PaymentResult> {
  // カスタムSpanの作成
  return tracer.startActiveSpan(
    'processPayment',
    {
      kind: SpanKind.INTERNAL,
      attributes: {
        'payment.order_id': orderId,
        'payment.amount': amount,
        'payment.currency': 'JPY',
      },
    },
    async (span) => {
      try {
        // 1. 注文の検証
        const order = await tracer.startActiveSpan(
          'validateOrder',
          async (validateSpan) => {
            const result = await orderRepository.findById(orderId);
            validateSpan.setAttribute('order.status', result.status);
            validateSpan.end();
            return result;
          }
        );

        // 2. 外部決済APIの呼び出し
        const paymentResult = await tracer.startActiveSpan(
          'callExternalPaymentAPI',
          { kind: SpanKind.CLIENT },
          async (apiSpan) => {
            apiSpan.setAttribute('payment.provider', 'stripe');
            try {
              const result = await stripeClient.charge({
                amount,
                currency: 'jpy',
                orderId,
              });
              apiSpan.setAttribute('payment.transaction_id', result.transactionId);
              apiSpan.setStatus({ code: SpanStatusCode.OK });
              apiSpan.end();
              return result;
            } catch (error) {
              apiSpan.setStatus({
                code: SpanStatusCode.ERROR,
                message: error instanceof Error ? error.message : 'Unknown error',
              });
              apiSpan.recordException(error as Error);
              apiSpan.end();
              throw error;
            }
          }
        );

        // 3. 決済結果の保存
        await tracer.startActiveSpan(
          'savePaymentResult',
          async (saveSpan) => {
            await paymentRepository.save({
              orderId,
              transactionId: paymentResult.transactionId,
              status: 'completed',
            });
            saveSpan.end();
          }
        );

        span.setStatus({ code: SpanStatusCode.OK });
        span.end();
        return paymentResult;
      } catch (error) {
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: error instanceof Error ? error.message : 'Payment failed',
        });
        span.recordException(error as Error);
        span.end();
        throw error;
      }
    }
  );
}

トレースバックエンド

Jaeger vs Tempo

観点JaegerGrafana Tempo
ストレージElasticsearch, CassandraObject Storage (S3等)
コストストレージコスト高めObject Storageで低コスト
検索柔軟な検索クエリTraceQL、Trace ID検索
Grafana統合プラグインで連携ネイティブ統合
スケール水平スケール対応高い水平スケーラビリティ
推奨ケース既存のES環境がある場合Grafanaスタックを使う場合

Tempo設定例

# tempo-config.yaml
stream_over_http_enabled: true
server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318

storage:
  trace:
    backend: s3
    s3:
      bucket: tempo-traces
      endpoint: s3.amazonaws.com
      region: ap-northeast-1
    wal:
      path: /var/tempo/wal
    local:
      path: /var/tempo/blocks

# サンプリング設定
overrides:
  defaults:
    ingestion:
      rate_strategy: local
      rate_limit_bytes: 15000000
      burst_size_bytes: 20000000

サンプリング戦略

# サンプリング戦略: 全トレースを保存するとコストが膨大
sampling_strategies:
  # 1. Head-based Sampling(リクエスト開始時に決定)
  head_based:
    type: probabilistic
    rate: 0.1  # 10%をサンプリング
    per_service:
      payment-service: 1.0   # 決済は100%保存
      search-service: 0.01   # 検索は1%

  # 2. Tail-based Sampling(リクエスト完了後に決定)
  tail_based:
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
        # エラーのあるトレースは100%保存

      - name: slow_requests
        type: latency
        latency: { threshold_ms: 2000 }
        # 2秒以上のトレースは100%保存

      - name: default
        type: probabilistic
        probabilistic: { sampling_percentage: 5 }
        # その他は5%
トレーシングの落とし穴と対策
落とし穴問題対策
高カーディナリティ属性ストレージコスト爆発user_idはタグに入れるがインデックスしない
サンプリング漏れ重要なトレースが記録されないTail-basedでエラー・遅延を確実に記録
Context伝搬の断絶非同期処理でTraceが切れるメッセージキューにもTrace Contextを伝搬
パフォーマンスオーバーヘッド本番環境の性能低下サンプリング率を調整、バッチエクスポート

まとめ

ポイント内容
Trace/Spanリクエストの全体旅路と各処理単位
Context PropagationW3C Trace Context でサービス間伝搬
手動計装重要なビジネスロジックにカスタムSpanを追加
サンプリングHead-based(確率的)、Tail-based(条件付き)
バックエンドJaeger(ES環境向け)、Tempo(Grafanaスタック向け)

チェックリスト

  • Trace、Span、Context Propagationの概念を理解した
  • OpenTelemetryでの手動計装を実装できる
  • サンプリング戦略の使い分けを把握した
  • Jaeger/Tempoの特徴と選択基準を理解した
  • トレーシングの落とし穴と対策を知っている

次のステップへ

次は「構造化ログとログ集約」を学びます。3本柱の最後の柱であるログを、構造化して効率的に活用する方法を深掘りしましょう。


推定読了時間: 40分