LESSON 25分

ストーリー

佐藤CTO
Istioの設定は理解できたか。でも設定を入れただけでは不十分だ
佐藤CTO
設定を入れた”結果”がどうなっているか、常に見えていなければならない。マイクロサービスの世界では、可観測性(Observability)が命綱だ
あなた
モノリスなら1つのプロセスのログを見ればよかったですが、数十のサービスに分散すると…
佐藤CTO
そう、1つのリクエストがどのサービスを通過し、どこでどれだけ時間がかかり、どこでエラーが発生したのか。それを追跡できなければ、トラブルシューティングは不可能だ。今日は可観測性の3本柱を学ぼう

可観測性の3本柱

マイクロサービスの可観測性はメトリクスログトレースの3つで構成されます。

graph TD
    subgraph OBS["可観測性(Observability)"]
        M["メトリクス(Metrics)<br/>「今何が起きているか」<br/>Prometheus + Grafana"]
        L["ログ(Logs)<br/>「何が起きたのか」<br/>Fluentd / Loki"]
        T["トレース(Traces)<br/>「リクエストはどう流れたか」<br/>Jaeger / Zipkin"]
    end

    classDef metrics fill:#d4edda,stroke:#28a745,color:#333
    classDef logs fill:#e8f4fd,stroke:#2196f3,color:#333
    classDef traces fill:#fff3cd,stroke:#f0ad4e,color:#333
    class M metrics
    class L logs
    class T traces
目的データの性質代表ツール
メトリクスシステムの状態を数値で把握集約データ(時系列)Prometheus, Grafana
ログイベントの詳細を記録個別イベント(構造化/非構造化)Fluentd, Loki, ELK
トレースリクエストの経路を追跡因果関係のあるスパン群Jaeger, Zipkin, Tempo

分散トレーシング

なぜ分散トレーシングが必要か

モノリスでは1つのスタックトレースで問題箇所を特定できますが、マイクロサービスではリクエストが複数のサービスを横断するため、分散トレーシングが必要です。

graph LR
    REQ["ユーザーリクエスト<br/>POST /api/orders"] --> GW["API Gateway<br/>50ms"]
    GW --> ORD["Order Svc"]
    ORD -->|200ms| PAY["Payment Svc<br/>(遅い!)"]
    ORD -->|30ms| INV["Inventory Svc"]
    INV -->|20ms| NOT["Notification Svc"]

    classDef slow fill:#f8d7da,stroke:#dc3545,color:#333
    classDef normal fill:#d4edda,stroke:#28a745,color:#333
    classDef req fill:#e8f4fd,stroke:#2196f3,color:#333
    class PAY slow
    class GW,ORD,INV,NOT normal
    class REQ req

トレースの構造

graph TD
    Trace["Trace: abc123\n1つのリクエスト全体"]
    GW["API Gateway\nspan-001 / 310ms"]
    Order["Order Service\nspan-002 / 260ms"]
    Pay["Payment Service\nspan-003 / 200ms\nボトルネック"]
    Inv["Inventory Service\nspan-004 / 50ms"]
    Notif["Notification Service\nspan-005 / 20ms"]

    Trace --> GW
    GW --> Order
    Order --> Pay
    Order --> Inv
    Inv --> Notif

    style Trace fill:#1e293b,stroke:#475569,color:#f8fafc
    style GW fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style Order fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style Pay fill:#fee2e2,stroke:#dc2626,stroke-width:3px,color:#991b1b
    style Inv fill:#d1fae5,stroke:#059669,color:#065f46
    style Notif fill:#d1fae5,stroke:#059669,color:#065f46

Jaeger/Zipkinの比較

特性JaegerZipkin
開発元Uber(現CNCF)Twitter
言語GoJava
ストレージCassandra, Elasticsearch, KafkaCassandra, Elasticsearch, MySQL
UI高機能(依存関係グラフ)シンプル
サンプリングアダプティブサンプリング対応固定レート
Istio統合ネイティブ対応ネイティブ対応

トレーシングヘッダーの伝播

Istioのサイドカーは自動でトレーシングヘッダーを注入しますが、アプリケーション内でヘッダーを伝播する責任はアプリケーション側にあります。

// アプリケーションコードでのヘッダー伝播
// Istioが認識するヘッダーを受信リクエストから送信リクエストに転送する
const TRACE_HEADERS = [
  'x-request-id',
  'x-b3-traceid',
  'x-b3-spanid',
  'x-b3-parentspanid',
  'x-b3-sampled',
  'x-b3-flags',
  'b3',
  'traceparent',  // W3C Trace Context
  'tracestate',
];

function propagateTraceHeaders(
  incomingHeaders: Record<string, string>,
  outgoingRequest: Record<string, string>
): void {
  for (const header of TRACE_HEADERS) {
    if (incomingHeaders[header]) {
      outgoingRequest[header] = incomingHeaders[header];
    }
  }
}

// Express.js ミドルウェアの例
app.use((req, res, next) => {
  // コンテキストにトレーシングヘッダーを保存
  req.traceContext = {};
  for (const header of TRACE_HEADERS) {
    if (req.headers[header]) {
      req.traceContext[header] = req.headers[header] as string;
    }
  }
  next();
});

// 他サービスへのリクエスト時にヘッダーを伝播
async function callPaymentService(req: Request, amount: number) {
  return axios.post('http://payment-service/api/charge', { amount }, {
    headers: req.traceContext,  // トレーシングヘッダーを転送
  });
}

メトリクス収集(Prometheus)

Istioが自動収集するメトリクス

Istioのサイドカーは、追加コードなしで以下のメトリクスを収集します。

メトリクス説明タイプ
istio_requests_totalリクエスト総数Counter
istio_request_duration_millisecondsレスポンスタイムHistogram
istio_request_bytesリクエストサイズHistogram
istio_response_bytesレスポンスサイズHistogram
istio_tcp_connections_opened_totalTCP接続開始数Counter
istio_tcp_connections_closed_totalTCP接続終了数Counter

PromQLクエリ例

# サービスごとのリクエスト成功率(過去5分間)
sum(rate(istio_requests_total{
  response_code!~"5.*",
  destination_service_name="order-service"
}[5m])) /
sum(rate(istio_requests_total{
  destination_service_name="order-service"
}[5m]))

# P99レイテンシ(99パーセンタイル)
histogram_quantile(0.99,
  sum(rate(istio_request_duration_milliseconds_bucket{
    destination_service_name="order-service"
  }[5m])) by (le)
)

# エラー率が5%を超えているサービスを検出
sum(rate(istio_requests_total{response_code=~"5.*"}[5m])) by (destination_service_name)
/
sum(rate(istio_requests_total{}[5m])) by (destination_service_name)
> 0.05

GoldenSignals(4つの黄金シグナル)

Googleが提唱するサービス監視の4つの黄金シグナルです。

graph TD
    GS["4つの黄金シグナル<br/>(Golden Signals)"]
    GS --> LAT["レイテンシ(Latency)<br/>リクエスト処理にかかる時間<br/>→ P50, P95, P99 で監視"]
    GS --> TRF["トラフィック(Traffic)<br/>システムへのリクエスト量<br/>→ RPS(秒間リクエスト数)で監視"]
    GS --> ERR["エラー率(Errors)<br/>失敗したリクエストの割合<br/>→ 5xx率、タイムアウト率で監視"]
    GS --> SAT["飽和度(Saturation)<br/>リソースの使用度合い<br/>→ CPU、メモリ、接続数で監視"]

    classDef title fill:#e8f4fd,stroke:#2196f3,color:#333
    classDef signal fill:#fff3cd,stroke:#f0ad4e,color:#333
    class GS title
    class LAT,TRF,ERR,SAT signal

ログ集約

構造化ログの重要性

マイクロサービスでは、構造化ログ(JSON形式)を採用し、集約・検索を容易にすることが不可欠です。

// 悪い例: 非構造化ログ
console.log(`Order created: ${orderId} for user ${userId} total=${total}`);

// 良い例: 構造化ログ(JSON)
const logger = createLogger({ format: 'json' });

logger.info({
  event: 'order.created',
  orderId: 'ORD-12345',
  userId: 'USR-67890',
  total: 15000,
  currency: 'JPY',
  traceId: req.traceContext['x-b3-traceid'],
  spanId: req.traceContext['x-b3-spanid'],
  service: 'order-service',
  timestamp: new Date().toISOString(),
});

// 出力(JSON):
// {
//   "event": "order.created",
//   "orderId": "ORD-12345",
//   "userId": "USR-67890",
//   "total": 15000,
//   "currency": "JPY",
//   "traceId": "abc123def456",
//   "spanId": "span-002",
//   "service": "order-service",
//   "timestamp": "2025-01-15T10:30:00.000Z"
// }

ログ集約アーキテクチャ

graph TD
    SA["Service A<br/>(stdout)"] --> FL["Fluentd / Fluent Bit(収集)<br/>DaemonSet(各ノードに配置)"]
    SB["Service B<br/>(stdout)"] --> FL
    SC["Service C<br/>(stdout)"] --> FL
    FL --> ES["Elasticsearch / Grafana Loki<br/>← 保存・インデックス"]
    ES --> KIB["Kibana / Grafana<br/>← 検索・可視化"]

    classDef svc fill:#d4edda,stroke:#28a745,color:#333
    classDef collect fill:#fff3cd,stroke:#f0ad4e,color:#333
    classDef store fill:#e8f4fd,stroke:#2196f3,color:#333
    classDef viz fill:#f3e8fd,stroke:#9c27b0,color:#333
    class SA,SB,SC svc
    class FL collect
    class ES store
    class KIB viz

サービス依存関係の可視化(Kiali)

Kialiは、Istioのメッシュをリアルタイムで可視化するダッシュボードツールです。

Kialiが提供する情報

機能説明
サービスグラフサービス間の通信経路と状態をリアルタイム表示
トラフィックフローリクエスト量、成功率、レイテンシの表示
ヘルスチェック各サービスの健全性状態
設定検証Istioリソースの設定エラー検出
ワークロード詳細Pod、レプリカの状態表示
graph TD
    GW["API Gateway"] -->|"200 RPS, 99.8%"| Order["Order Service"]
    GW -->|"100 RPS, 99.9%"| User["User Service"]
    Order -->|"150 RPS, 99.5%"| Inv["Inventory"]
    Order -->|"50 RPS, 98.0%"| Pay["Payment"]
    Pay -->|"50 RPS, 97.0%"| Notif["Notification"]

    style GW fill:#d1fae5,stroke:#059669,stroke-width:2px,color:#065f46
    style Order fill:#d1fae5,stroke:#059669,color:#065f46
    style User fill:#d1fae5,stroke:#059669,color:#065f46
    style Inv fill:#d1fae5,stroke:#059669,color:#065f46
    style Pay fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
    style Notif fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#991b1b

凡例: 緑=正常(>99%) / 黄=警告(95-99%) / 赤=異常(<95%)


レイテンシ分析

レイテンシバジェット

各サービスにレイテンシの「予算」を割り当て、SLOを管理します。

// レイテンシバジェットの設計
interface LatencyBudget {
  service: string;
  p50Target: number;  // ミリ秒
  p95Target: number;
  p99Target: number;
}

const budgets: LatencyBudget[] = [
  { service: 'api-gateway',      p50Target: 5,   p95Target: 10,  p99Target: 20  },
  { service: 'order-service',    p50Target: 20,  p95Target: 50,  p99Target: 100 },
  { service: 'payment-service',  p50Target: 100, p95Target: 200, p99Target: 500 },
  { service: 'inventory-service', p50Target: 10,  p95Target: 30,  p99Target: 50  },
  { service: 'notification-service', p50Target: 50, p95Target: 100, p99Target: 200 },
];

// 全体のP99目標: 870ms以下(各サービスの直列分の合算)
// ※並列呼び出しの場合は最遅のサービスに依存

ヘルスチェックパターン

// サービスのヘルスチェックエンドポイント
interface HealthCheckResponse {
  status: 'healthy' | 'degraded' | 'unhealthy';
  version: string;
  uptime: number;
  checks: {
    name: string;
    status: 'pass' | 'warn' | 'fail';
    responseTime?: number;
    message?: string;
  }[];
}

// /health エンドポイントの実装例
app.get('/health', async (req, res) => {
  const checks = await Promise.all([
    checkDatabase(),
    checkRedis(),
    checkExternalAPI(),
  ]);

  const status = checks.every(c => c.status === 'pass')
    ? 'healthy'
    : checks.some(c => c.status === 'fail')
      ? 'unhealthy'
      : 'degraded';

  res.status(status === 'unhealthy' ? 503 : 200).json({
    status,
    version: process.env.APP_VERSION || 'unknown',
    uptime: process.uptime(),
    checks,
  });
});

ダッシュボード設計の原則

REDメソッド

マイクロサービスのダッシュボードにはREDメソッド(Rate, Errors, Duration)を適用します。

指標説明PromQLクエリ例
Rate秒間リクエスト数sum(rate(istio_requests_total[5m]))
Errorsエラーリクエスト率sum(rate(istio_requests_total{response_code=~"5.*"}[5m]))
Durationレスポンスタイム分布histogram_quantile(0.99, ...)

ダッシュボードの階層構造

graph TD
    L1["Level 1: 全体概要"] --> L2["Level 2: サービス別"]
    L2 --> L3["Level 3: 詳細調査"]

    L1 -.- L1a["全サービスの健全性サマリー\n全体のエラー率・レイテンシ\nアラート一覧"]
    L2 -.- L2a["個別サービスのRED指標\nサービス間通信の状態\nリソース使用率(CPU, Memory)"]
    L3 -.- L3a["個別リクエストのトレース\nログ詳細\nPod/コンテナレベルのメトリクス"]

    style L1 fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af
    style L2 fill:#fef3c7,stroke:#d97706,color:#92400e
    style L3 fill:#fee2e2,stroke:#dc2626,color:#991b1b
    style L1a fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style L2a fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style L3a fill:#f3f4f6,stroke:#9ca3af,color:#374151

アラート設計

# Prometheusアラートルール例
groups:
  - name: microservice-alerts
    rules:
      # エラー率が5%を超えたら警告
      - alert: HighErrorRate
        expr: |
          sum(rate(istio_requests_total{response_code=~"5.*"}[5m])) by (destination_service_name)
          /
          sum(rate(istio_requests_total{}[5m])) by (destination_service_name)
          > 0.05
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.destination_service_name }} のエラー率が5%超過"

      # P99レイテンシが1秒を超えたら重大
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            sum(rate(istio_request_duration_milliseconds_bucket[5m]))
            by (le, destination_service_name)
          ) > 1000
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.destination_service_name }} のP99レイテンシが1秒超過"

まとめ

ポイント内容
可観測性の3本柱メトリクス、ログ、トレースを組み合わせて運用する
分散トレーシングTraceID/SpanIDでリクエストの全経路を追跡する
ヘッダー伝播アプリケーションがトレーシングヘッダーを転送する責任を持つ
Golden Signalsレイテンシ、トラフィック、エラー率、飽和度の4指標で監視
ダッシュボード設計REDメソッドを基本に、3階層で構成する
アラート設計SLOに基づいた閾値でアラートを設定する

チェックリスト

  • 可観測性の3本柱とそれぞれの目的を説明できる
  • 分散トレーシングのTrace/Spanの構造を理解した
  • トレーシングヘッダーの伝播が必要な理由を理解した
  • Golden Signalsの4つの指標を列挙できる
  • 構造化ログの利点を説明できる
  • REDメソッドに基づくダッシュボード設計を理解した

次のステップへ

可観測性の全体像を把握したところで、次は「サービスディスカバリとロードバランシング」を学びます。サービスがお互いをどうやって見つけ、トラフィックをどう分散するのか、その仕組みを深掘りしましょう。


推定読了時間: 25分