EXERCISE 90分

ストーリー

高橋アーキテクト
いよいよトレーシングを実際に導入してみよう

高橋アーキテクトがECサイトの構成図を広げた。

高橋アーキテクト
OpenTelemetryでトレーシングを計装し、Jaegerで可視化する。そしてボトルネックを見つけて、改善提案まで出してくれ

演習の概要

ECサイトのマイクロサービスに分散トレーシングを導入し、パフォーマンス分析を行ってください。

ミッション一覧

#ミッション難易度
1OpenTelemetryのセットアップ基本
2手動計装の実装基本
3コンテキスト伝搬の実装応用
4サンプリング戦略の設計応用
5ボトルネック分析と改善提案応用
6サービスマップの設計応用

ミッション1: OpenTelemetryのセットアップ

Node.jsアプリケーションにOpenTelemetryの自動計装をセットアップしてください。

要件

  • OTel SDKの初期化コードを作成
  • Jaegerへのエクスポート設定
  • HTTP、Express、PostgreSQLの自動計装を有効化
解答例(自分で実装してから確認しよう)
// tracing.ts - アプリケーション起動前にインポート
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';

const traceExporter = new OTLPTraceExporter({
  url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
});

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'order-service',
    [ATTR_SERVICE_VERSION]: process.env.APP_VERSION || '1.0.0',
    'deployment.environment': process.env.NODE_ENV || 'development',
  }),
  spanProcessors: [new BatchSpanProcessor(traceExporter)],
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-http': { enabled: true },
      '@opentelemetry/instrumentation-express': { enabled: true },
      '@opentelemetry/instrumentation-pg': { enabled: true },
    }),
  ],
});

sdk.start();

process.on('SIGTERM', () => {
  sdk.shutdown().then(() => process.exit(0));
});

export { sdk };
// index.ts
import './tracing'; // 最初にインポート
import express from 'express';
// ... アプリケーションコード

ミッション2: 手動計装の実装

注文作成のビジネスロジックに手動計装を追加してください。

要件

  • 注文作成処理のカスタムSpanを作成
  • ビジネス属性(orderId, amount, items_count)を付与
  • エラー時のステータスとイベントを記録
解答例(自分で実装してから確認しよう)
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';

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

async function createOrder(input: OrderInput): Promise<Order> {
  return tracer.startActiveSpan('order.create', {
    kind: SpanKind.INTERNAL,
    attributes: {
      'order.items_count': input.items.length,
      'order.customer_id': input.customerId,
    },
  }, async (span) => {
    try {
      // バリデーション
      await tracer.startActiveSpan('order.validate', async (validateSpan) => {
        validateOrderInput(input);
        validateSpan.end();
      });

      // 在庫確認
      const stockAvailable = await tracer.startActiveSpan('order.checkInventory', async (invSpan) => {
        const result = await inventoryService.checkStock(input.items);
        invSpan.setAttribute('inventory.all_available', result.allAvailable);
        invSpan.end();
        return result;
      });

      // 注文保存
      const order = await tracer.startActiveSpan('order.save', async (saveSpan) => {
        const result = await orderRepository.create(input);
        saveSpan.setAttribute('order.id', result.id);
        saveSpan.setAttribute('order.total', result.total);
        saveSpan.end();
        return result;
      });

      // 決済処理
      await tracer.startActiveSpan('order.processPayment', async (paySpan) => {
        paySpan.setAttribute('payment.amount', order.total);
        await paymentService.charge(order);
        paySpan.end();
      });

      span.setAttribute('order.id', order.id);
      span.setAttribute('order.total', order.total);
      span.setStatus({ code: SpanStatusCode.OK });
      return order;

    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      throw error;
    } finally {
      span.end();
    }
  });
}

ミッション3: コンテキスト伝搬の実装

サービス間の呼び出しでトレースコンテキストを伝搬する実装を作成してください。

要件

  • HTTPクライアントでのコンテキスト注入
  • HTTPサーバーでのコンテキスト抽出
  • W3C Trace Context形式を使用
解答例(自分で実装してから確認しよう)
import { context, propagation, trace, SpanKind } from '@opentelemetry/api';

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

// HTTP呼び出し時のコンテキスト伝搬
async function callPaymentService(orderId: string, amount: number): Promise<PaymentResult> {
  return tracer.startActiveSpan('http.client.POST /charge', {
    kind: SpanKind.CLIENT,
    attributes: {
      'http.method': 'POST',
      'http.url': 'http://payment-service:8082/charge',
      'peer.service': 'payment-service',
    },
  }, async (span) => {
    // 現在のコンテキストからHTTPヘッダーに注入
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
    };
    propagation.inject(context.active(), headers);

    try {
      const response = await fetch('http://payment-service:8082/charge', {
        method: 'POST',
        headers,
        body: JSON.stringify({ orderId, amount }),
      });

      span.setAttribute('http.status_code', response.status);

      if (!response.ok) {
        throw new Error(`Payment failed: ${response.status}`);
      }

      span.setStatus({ code: SpanStatusCode.OK });
      return await response.json();
    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      throw error;
    } finally {
      span.end();
    }
  });
}

// 受信側: Expressミドルウェアでコンテキストを抽出
// (自動計装を使えば不要だが、仕組みの理解として)
function traceContextMiddleware(req: Request, res: Response, next: NextFunction) {
  const extractedContext = propagation.extract(context.active(), req.headers);
  context.with(extractedContext, () => {
    next();
  });
}

ミッション4: サンプリング戦略の設計

本番環境で使用するサンプリング戦略を設計してください。

要件

  • コスト効率を考慮
  • エラーと遅延リクエストは確実に記録
  • 見積もりリクエスト量: 1000 req/s
解答例(自分で実装してから確認しよう)
// サンプリング戦略設計書
const samplingStrategy = {
  approach: 'Tail-based sampling (OTel Collector)',

  rules: [
    { condition: 'エラーが発生したトレース', sampleRate: '100%', reason: '障害調査に必須' },
    { condition: 'レスポンス2秒以上のトレース', sampleRate: '100%', reason: 'ボトルネック分析に必須' },
    { condition: 'ヘルスチェック (/health)', sampleRate: '0%', reason: 'ノイズ排除' },
    { condition: 'その他の正常リクエスト', sampleRate: '5%', reason: 'コスト最適化' },
  ],

  estimatedVolume: {
    totalRequests: '1000 req/s',
    errorRate: '0.5% → 5 traces/s (100%)',
    slowRequests: '1% → 10 traces/s (100%)',
    normalSampled: '985 req/s × 5% → ~49 traces/s',
    totalTraces: '~64 traces/s',
    storageEstimate: '~5.5M traces/day → ~55GB/day',
  },

  collectorConfig: `
    processors:
      tail_sampling:
        decision_wait: 10s
        policies:
          - name: errors
            type: status_code
            status_code: { status_codes: [ERROR] }
          - name: slow-requests
            type: latency
            latency: { threshold_ms: 2000 }
          - name: health-check-drop
            type: string_attribute
            string_attribute:
              key: http.target
              values: ["/health", "/ready"]
              enabled_regex_matching: false
              invert_match: true
          - name: probabilistic
            type: probabilistic
            probabilistic: { sampling_percentage: 5 }
  `,
};

ミッション5: ボトルネック分析と改善提案

以下のトレースデータを分析し、ボトルネックと改善提案を作成してください。

トレースデータ

POST /api/orders (Total: 4200ms)
├─ auth.validate           ━━  30ms
├─ db.getUser              ━━  25ms
├─ db.getCart               ━━  20ms
├─ inventory.check (item1)  ━━  40ms   ┐
├─ inventory.check (item2)  ━━  35ms   │ N+1 + 直列
├─ inventory.check (item3)  ━━  42ms   │
├─ inventory.check (item4)  ━━  38ms   ┘
├─ pricing.calculate        ━━  15ms
├─ payment.charge           ━━━━━━━━━━━━━━━━  3800ms  ← 遅い!
│   └─ stripe.api           ━━━━━━━━━━━━━━  3750ms
├─ db.saveOrder             ━━  30ms
└─ notification.send        ━━  50ms
解答例(自分で実装してから確認しよう)
# ボトルネック分析レポート

## 問題1: 外部API遅延(critical)
- **箇所**: payment.charge → stripe.api (3750ms / 4200ms = 89%)
- **原因**: Stripe APIのレスポンスが極端に遅い
- **改善案**:
  1. タイムアウトを3秒に設定
  2. サーキットブレーカーを導入
  3. Stripe APIの代替プロバイダをフォールバックとして用意

## 問題2: N+1 + 直列呼び出し(warning)
- **箇所**: inventory.check が4回個別に呼ばれている (合計155ms)
- **原因**: カート内アイテムごとに在庫チェックを直列実行
- **改善案**:
  1. バッチAPI (POST /inventory/check-batch) を作成
  2. 1回のリクエストで全アイテムの在庫を確認 → 40ms程度に短縮

## 問題3: 通知の同期実行(info)
- **箇所**: notification.send (50ms)
- **原因**: 注文作成のレスポンスに通知完了を含めている
- **改善案**:
  1. メッセージキューに非同期化
  2. レスポンスタイムを50ms短縮

## 改善後の予想レスポンスタイム
- 現在: 4200ms
- payment改善後: ~450ms (タイムアウト3s設定、正常時)
- inventory改善後: -115ms
- notification改善後: -50ms
- 目標: ~300ms (正常時)

ミッション6: サービスマップの設計

ECサイトのサービスマップを設計し、リスク分析を行ってください。

解答例(自分で実装してから確認しよう)
const serviceMap = {
  services: [
    { name: 'api-gateway', type: 'internal', criticality: 'critical' },
    { name: 'auth-service', type: 'internal', criticality: 'critical' },
    { name: 'order-service', type: 'internal', criticality: 'critical' },
    { name: 'payment-service', type: 'internal', criticality: 'critical' },
    { name: 'inventory-service', type: 'internal', criticality: 'high' },
    { name: 'notification-service', type: 'internal', criticality: 'low' },
    { name: 'stripe-api', type: 'external', criticality: 'critical' },
  ],

  riskAnalysis: {
    singlePointOfFailure: [
      'auth-service: 全リクエストが通過 → キャッシュ+レプリカで対策',
      'stripe-api: 外部依存 → フォールバック決済プロバイダを用意',
    ],
    cascadeFailure: [
      'stripe障害 → payment障害 → order障害 → 全注文停止',
      '対策: サーキットブレーカー + タイムアウト + フォールバック',
    ],
    degradation: [
      'notification障害時: 注文は成功、通知は後で再送',
      'inventory障害時: 楽観的に注文受付、後で在庫確認',
    ],
  },
};

達成度チェック

ミッション内容完了
1OpenTelemetryのセットアップ[ ]
2手動計装の実装[ ]
3コンテキスト伝搬の実装[ ]
4サンプリング戦略の設計[ ]
5ボトルネック分析と改善提案[ ]
6サービスマップの設計[ ]

まとめ

ポイント内容
セットアップOTel SDK + Jaeger + 自動計装
手動計装ビジネスロジックにカスタムSpanと属性を追加
コンテキスト伝搬propagation.inject/extractでTraceIDを伝搬
分析ウォーターフォールからボトルネックパターンを特定

チェックリスト

  • OpenTelemetryのセットアップとJaegerへのエクスポートができた
  • ビジネスロジックに手動計装を追加できた
  • コンテキスト伝搬を実装できた
  • トレースからボトルネックを特定し改善提案ができた

次のステップへ

演習が完了したら、Step 4 のチェックポイントクイズに挑戦しましょう。


推定所要時間: 90分