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, Datadog | ELK, Loki | Jaeger, Zipkin, Tempo |
分散トレーシング:Trace と 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 |