LESSON 40分

ストーリー

高橋アーキテクト
ログで”何が起きたか”がわかる。メトリクスで”どれだけ起きているか”がわかる。でも”どこで、どの順番で起きているか”はわからない

高橋アーキテクトがマイクロサービスの構成図を指した。

高橋アーキテクト
1つのリクエストが5つのサービスを横断するとき、どこで時間がかかっているのか。それを可視化するのが分散トレーシングだ。これがないと、分散システムのパフォーマンス問題は解決できない

分散トレーシングとは

分散トレーシングは、1つのリクエストがシステム内の複数サービスを横断する経路を記録・可視化する技術です。

ユーザーのリクエスト: POST /api/orders

[API Gateway]  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  350ms

  ├─[Auth Service]  ━━━━━━━  25ms

  └─[Order Service]  ━━━━━━━━━━━━━━━━━━━━━━━━━━━  310ms

      ├─[DB: SELECT order]  ━━━  5ms

      ├─[Inventory Service]  ━━━━━━━━━  40ms
      │    └─[DB: UPDATE stock]  ━━  8ms

      ├─[Payment Service]  ━━━━━━━━━━━━━━━━━━━  230ms  ← ボトルネック!
      │    └─[External: Stripe API]  ━━━━━━━━━━━━━━  220ms

      └─[Notification Service]  ━━━━  15ms
           └─[Email API]  ━━  10ms

基本概念

Trace(トレース)

1つのリクエストの全体的な経路です。一意のTraceIDで識別されます。

Span(スパン)

Trace内の1つの処理区間です。サービス間の呼び出しや、サービス内の重要な処理ごとにSpanが作成されます。

Context Propagation(コンテキスト伝搬)

TraceIDやSpanIDを、サービス間の呼び出し時にHTTPヘッダー等で伝搬する仕組みです。

// Spanの構造
interface Span {
  traceId: string;          // リクエスト全体のID
  spanId: string;           // このSpanのID
  parentSpanId?: string;    // 親SpanのID(ルートSpanはなし)
  operationName: string;    // 操作名 (e.g., "POST /api/orders")
  serviceName: string;      // サービス名
  startTime: number;        // 開始時刻(マイクロ秒)
  duration: number;         // 所要時間(マイクロ秒)
  status: {
    code: 'OK' | 'ERROR' | 'UNSET';
    message?: string;
  };
  attributes: Record<string, string | number | boolean>;
  events: SpanEvent[];      // Span内のイベント(ログ的な情報)
}

interface SpanEvent {
  name: string;
  timestamp: number;
  attributes: Record<string, string | number | boolean>;
}

Context Propagationの仕組み

// HTTPヘッダーによるコンテキスト伝搬
// W3C Trace Context 標準

// リクエスト送信側(Service A → Service B)
const headers = {
  'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
  //              version-traceId-parentSpanId-flags
};

// リクエスト受信側(Service B)
function extractContext(req: IncomingMessage): TraceContext {
  const traceparent = req.headers['traceparent'] as string;
  const [version, traceId, parentSpanId, flags] = traceparent.split('-');

  return {
    traceId,           // 同じTraceIDを引き継ぐ
    parentSpanId,      // 送信側のSpanIDが親になる
    sampled: flags === '01',
  };
}

OpenTelemetryでの実装

import { trace, context, propagation } from '@opentelemetry/api';

const tracer = trace.getTracer('order-service');

// サービス内でSpanを作成
async function createOrder(orderData: OrderInput): Promise<Order> {
  return tracer.startActiveSpan('createOrder', async (span) => {
    span.setAttribute('order.items_count', orderData.items.length);

    // DBアクセス(子Span)
    const order = await tracer.startActiveSpan('db.insertOrder', async (dbSpan) => {
      const result = await db.orders.create(orderData);
      dbSpan.setAttribute('db.statement', 'INSERT INTO orders');
      dbSpan.end();
      return result;
    });

    // 他サービス呼び出し(子Span + コンテキスト伝搬)
    await tracer.startActiveSpan('call.paymentService', async (callSpan) => {
      const headers: Record<string, string> = {};
      propagation.inject(context.active(), headers);

      await fetch('http://payment-service/charge', {
        method: 'POST',
        headers: {
          ...headers,           // traceparentヘッダーを付与
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ orderId: order.id, amount: order.total }),
      });
      callSpan.end();
    });

    span.end();
    return order;
  });
}

サンプリング戦略

全リクエストのトレースを保存するとコストが膨大になるため、サンプリングが重要です。

// サンプリング戦略
interface SamplingStrategy {
  // 確率的サンプリング: 一定割合をランダムに記録
  probabilistic: {
    rate: 0.1;  // 10%のリクエストを記録
  };

  // Rate Limiting: 1秒あたりの上限を設定
  rateLimiting: {
    maxTracesPerSecond: 100;
  };

  // Tail-based: レスポンス後にサンプリング判定
  tailBased: {
    rules: [
      { condition: 'error', sampleRate: 1.0 },      // エラーは100%記録
      { condition: 'duration > 2s', sampleRate: 1.0 }, // 遅いリクエストは100%
      { condition: 'default', sampleRate: 0.05 },    // それ以外は5%
    ];
  };
}
戦略利点欠点
確率的実装が簡単重要なトレースを見逃す可能性
Rate Limitingコスト予測が容易高負荷時にサンプル率が下がる
Tail-basedエラーや遅延を確実に記録実装が複雑、一時バッファが必要

Span属性のベストプラクティス

// セマンティック属性(OpenTelemetry Semantic Conventions)
const semanticAttributes = {
  // HTTP
  'http.method': 'POST',
  'http.url': '/api/orders',
  'http.status_code': 201,
  'http.request_content_length': 1234,

  // データベース
  'db.system': 'postgresql',
  'db.statement': 'SELECT * FROM orders WHERE id = $1',
  'db.operation': 'SELECT',

  // メッセージング
  'messaging.system': 'kafka',
  'messaging.destination': 'order-events',
  'messaging.operation': 'publish',

  // カスタム(ビジネス固有)
  'order.id': 'ORD-001',
  'order.total': 5000,
  'payment.provider': 'stripe',
};

まとめ

ポイント内容
Trace1リクエストの全体経路(TraceIDで識別)
SpanTrace内の1処理区間(SpanIDで識別)
Context PropagationTraceIDをサービス間でHTTPヘッダーで伝搬
サンプリング全件記録はコスト大、Tail-basedが推奨

チェックリスト

  • Trace、Span、Context Propagationの概念を説明できる
  • W3C Trace Contextの仕組みを理解した
  • OpenTelemetryでのSpan作成とコンテキスト伝搬を実装できる
  • サンプリング戦略の種類と使い分けを把握した

次のステップへ

次は「Jaeger/Zipkinの活用」を学びます。収集したトレースを可視化・分析するツールの使い方を見ていきましょう。


推定読了時間: 40分