ストーリー
高橋アーキテクトがECサイトの構成図を広げた。
演習の概要
ECサイトのマイクロサービスに分散トレーシングを導入し、パフォーマンス分析を行ってください。
ミッション一覧
| # | ミッション | 難易度 |
|---|---|---|
| 1 | OpenTelemetryのセットアップ | 基本 |
| 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障害時: 楽観的に注文受付、後で在庫確認',
],
},
};
達成度チェック
| ミッション | 内容 | 完了 |
|---|---|---|
| 1 | OpenTelemetryのセットアップ | [ ] |
| 2 | 手動計装の実装 | [ ] |
| 3 | コンテキスト伝搬の実装 | [ ] |
| 4 | サンプリング戦略の設計 | [ ] |
| 5 | ボトルネック分析と改善提案 | [ ] |
| 6 | サービスマップの設計 | [ ] |
まとめ
| ポイント | 内容 |
|---|---|
| セットアップ | OTel SDK + Jaeger + 自動計装 |
| 手動計装 | ビジネスロジックにカスタムSpanと属性を追加 |
| コンテキスト伝搬 | propagation.inject/extractでTraceIDを伝搬 |
| 分析 | ウォーターフォールからボトルネックパターンを特定 |
チェックリスト
- OpenTelemetryのセットアップとJaegerへのエクスポートができた
- ビジネスロジックに手動計装を追加できた
- コンテキスト伝搬を実装できた
- トレースからボトルネックを特定し改善提案ができた
次のステップへ
演習が完了したら、Step 4 のチェックポイントクイズに挑戦しましょう。
推定所要時間: 90分