目次を表示する

システム設計とCS概念

Observability(分散トレーシング)── 「遅いのはどこ」を追う

Observability(分散トレーシング)── 「遅いのはどこ」を追う

扱うCS概念:分散トレーシング(OpenTelemetry)、Context Propagation、サンプリング戦略、RED/USE メソッド、ログ・メトリクス・トレースの三本柱


この章で何ができるようになるか:マイクロサービス環境で「1つのリクエストがどのサービスをどう通過し、どこで遅くなったか」を追跡するシステムの設計を説明できるようになる。


問題設定

EC サイトで「注文画面が遅い」と報告があった。

モノリス時代:
  1つのプロセスのプロファイラーを見ればよい

マイクロサービス時代(20サービス):
  リクエスト → API Gateway → Order Service → User Service
                                            → Inventory Service → DB
                                            → Payment Service → Stripe API
                                            → Notification Service → FCM
  
  「どのサービスの、どの操作が遅いのか」が見えない
  ログを見ても、どのログが同じリクエストに属するかわからない

Observability の三本柱

graph TD
    subgraph 三本柱
        Metrics[メトリクス<br/>「何が起きているか」<br/>数値の集約]
        Logs[ログ<br/>「何が起きたか」<br/>個別イベントの記録]
        Traces[トレース<br/>「どう流れたか」<br/>リクエストの経路追跡]
    end

    Metrics --> Dashboard[ダッシュボード<br/>Grafana]
    Logs --> Search[ログ検索<br/>Elasticsearch]
    Traces --> Analysis[トレース分析<br/>Jaeger / Tempo]
メトリクスログトレース
データ型数値(カウンター、ゲージ)テキスト(構造化/非構造化)スパンの木構造
粒度集約済み個別イベントリクエスト単位
保存コスト中(サンプリング前提)
用途アラート・傾向分析デバッグ・監査レイテンシ分析
ツールPrometheus, DatadogELK, LokiJaeger, Zipkin, Tempo

分散トレーシング:Trace と Span

分散トレーシング — Span ウォーターフォールとボトルネック分析

1つのリクエストの処理フロー(Trace):

Trace ID: abc-123
├── Span: API Gateway (12ms)
│   ├── Span: Order Service (45ms)
│   │   ├── Span: User Service (8ms)
│   │   ├── Span: Inventory Service (15ms)
│   │   │   └── Span: PostgreSQL Query (10ms)  ← ここが遅い
│   │   └── Span: Payment Service (20ms)
│   │       └── Span: Stripe API Call (18ms)
│   └── Span: Notification Service (3ms)
└── Total: 48ms

Trace:1つのリクエストの全体像。一意な Trace ID を持つ。

Span:Trace を構成する個別の操作。開始時刻、終了時刻、親 Span ID を持つ。

# OpenTelemetry での計装(Instrumentation)
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

# トレーサーの初期化
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="otel-collector:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

tracer = trace.get_tracer("order-service")

# アプリケーションコード
async def create_order(request):
    with tracer.start_as_current_span("create_order") as span:
        span.set_attribute("order.user_id", request.user_id)
        span.set_attribute("order.total", request.total)

        # 子スパン:ユーザー情報取得
        with tracer.start_as_current_span("get_user"):
            user = await user_service.get(request.user_id)

        # 子スパン:在庫確認
        with tracer.start_as_current_span("check_inventory"):
            available = await inventory_service.check(request.items)

        # 子スパン:決済
        with tracer.start_as_current_span("process_payment") as payment_span:
            try:
                result = await payment_service.charge(request.total)
                payment_span.set_attribute("payment.status", "success")
            except Exception as e:
                payment_span.set_status(trace.StatusCode.ERROR, str(e))
                payment_span.record_exception(e)
                raise

        return OrderResponse(order_id=result.order_id)

Context Propagation:Trace ID を伝播させる

サービス A → サービス B への HTTP 呼び出しで Trace ID を引き継ぐ必要がある。

# サービス A(呼び出し側):Trace ID を HTTP ヘッダーに注入
import httpx
from opentelemetry.propagate import inject

async def call_user_service(user_id: str):
    headers = {}
    inject(headers)  # 現在の Trace Context をヘッダーに注入
    # → headers = {"traceparent": "00-abc123-def456-01"}
    
    async with httpx.AsyncClient() as client:
        return await client.get(
            f"http://user-service/users/{user_id}",
            headers=headers
        )

# サービス B(受信側):HTTP ヘッダーから Trace ID を抽出
from opentelemetry.propagate import extract

async def get_user(request):
    # リクエストヘッダーから Trace Context を復元
    context = extract(carrier=request.headers)
    
    with tracer.start_as_current_span("get_user_handler", context=context):
        user = await db.get_user(request.path_params["user_id"])
        return user

W3C Trace Context ヘッダーの形式:

traceparent: 00-{trace_id}-{span_id}-{trace_flags}
             00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
                 ↑ 32桁hex(128bit)                  ↑ 16桁hex(64bit)  ↑ サンプリング

サンプリング戦略

全リクエストのトレースを保存するとストレージが爆発する。

秒間10万リクエスト × 各トレース 5KB × 86400秒/日
= 43 TB/日(現実的でない)

1% サンプリング → 430 GB/日(管理可能)
from opentelemetry.sdk.trace.sampling import (
    TraceIdRatioBased,
    ParentBased,
)

# 方式1:確率サンプリング(全体の1%を記録)
sampler = TraceIdRatioBased(0.01)

# 方式2:親ベース(親 Span がサンプルされていれば子もサンプル)
# → 1つのトレースが途中で切れるのを防ぐ
sampler = ParentBased(root=TraceIdRatioBased(0.01))

# 方式3:Tail-based サンプリング(Collector 側で判断)
# → レイテンシが高い or エラーのトレースを優先的に保存
class TailBasedSampler:
    def should_sample(self, span) -> bool:
        # エラーは100%記録
        if span.status == StatusCode.ERROR:
            return True
        # レイテンシが P99 超過は100%記録
        if span.duration_ms > 500:
            return True
        # それ以外は 0.1%
        return random.random() < 0.001

Head-based vs Tail-based

Head-based(リクエスト開始時に決定):
  ✅ シンプル、各サービスが独立して判断
  ❌ 「遅かったリクエスト」を事前には知れない

Tail-based(リクエスト完了後に決定):
  ✅ 遅い/エラーのトレースを100%保存
  ❌ 一時的に全 Span をバッファする必要がある(Collector が重い)

RED / USE メソッド:何を計測するか

RED メソッド(サービスレベル)

Rate:     リクエストレート(req/sec)
Errors:   エラーレート(error_count / total_count)
Duration: レイテンシ分布(P50, P95, P99)

→ 「このサービスは今正常か」を3つの数値で判断

USE メソッド(リソースレベル)

Utilization: リソースの使用率(CPU 80%、メモリ 70%)
Saturation:  飽和度(待ち行列の長さ、リクエストキューの深さ)
Errors:      リソースレベルのエラー(ディスクI/Oエラー、ネットワークドロップ)

→ 「このサーバーのリソースは今余裕があるか」を3つの数値で判断
# Prometheus メトリクス定義例(FastAPI)
from prometheus_client import Counter, Histogram, Gauge

# RED メトリクス
request_count = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'path', 'status']
)
request_duration = Histogram(
    'http_request_duration_seconds',
    'HTTP request duration',
    ['method', 'path'],
    buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
)

# USE メトリクス
active_connections = Gauge(
    'active_connections',
    'Number of active WebSocket/HTTP connections'
)

# ミドルウェアで自動計測
@app.middleware("http")
async def metrics_middleware(request, call_next):
    start = time.time()
    response = await call_next(request)
    duration = time.time() - start

    request_count.labels(
        method=request.method,
        path=request.url.path,
        status=response.status_code
    ).inc()
    request_duration.labels(
        method=request.method,
        path=request.url.path
    ).observe(duration)

    return response

まとめ

課題解決策設計ポイント
リクエストの経路追跡分散トレーシング(Span の木構造)Trace ID の一貫した伝播
サービス間の ID 引き継ぎContext Propagation(W3C Trace Context)ヘッダー注入/抽出
ストレージコストサンプリング(Head/Tail-based)エラー・遅延は100%、他は確率
サービスの健全性判断RED メソッド(Rate/Errors/Duration)3つの数値でダッシュボード
リソースのボトルネック発見USE メソッド(Utilization/Saturation/Errors)インフラ層の監視
ログとトレースの紐付けTrace ID をログに埋め込み構造化ログ + 相関ID