T-CREATOR

WebSocket SLO/SLI 設計:接続維持率・遅延・ドロップ率の目標値と計測方法

WebSocket SLO/SLI 設計:接続維持率・遅延・ドロップ率の目標値と計測方法

WebSocket を利用したリアルタイム通信システムを本番環境で運用する際、サービスレベルの目標設定と計測は欠かせません。しかし、HTTP とは異なる常時接続という特性から、従来の SLO/SLI 設計をそのまま適用するのは困難です。

この記事では、WebSocket に特化した SLO(Service Level Objectives)と SLI(Service Level Indicators)の設計方法について解説します。接続維持率、遅延、メッセージドロップ率という 3 つの重要な指標に焦点を当て、それぞれの目標値設定と計測手法を具体的なコード例とともに紹介しましょう。

背景

SLO と SLI の基本概念

SLI(Service Level Indicator)は、サービスの品質を数値化した指標です。一方、SLO(Service Level Objective)は、その指標に対する目標値を定めたものになります。

たとえば HTTP API では「レスポンスタイム 95 パーセンタイルが 500ms 以内」という SLO を設定し、実際のレスポンスタイムを SLI として計測することが一般的ですね。

WebSocket の特性と課題

WebSocket は HTTP と異なり、以下のような独自の特性を持っています。

#特性説明
1常時接続クライアントとサーバーの接続が長時間維持される
2双方向通信サーバーからクライアントへのプッシュ通信が可能
3低レイテンシリクエスト・レスポンスモデルのオーバーヘッドがない
4ステートフル接続状態を維持する必要がある

これらの特性により、従来の HTTP ベースの SLO/SLI をそのまま適用できません。接続の安定性、メッセージ配信の信頼性、リアルタイム性など、WebSocket 固有の観点から指標を設計する必要があるのです。

以下の図は、WebSocket における主要な計測ポイントを示しています。

mermaidflowchart TB
  client["クライアント"] -->|"1. 接続確立"| server["WebSocket サーバー"]
  server -->|"2. メッセージ送信"| client
  client -->|"3. メッセージ受信"| server
  server -->|"4. 接続維持"| monitoring["モニタリング<br/>システム"]
  monitoring -->|"計測"| metrics["SLI メトリクス"]

  metrics --> conn_rate["接続維持率"]
  metrics --> latency["メッセージ遅延"]
  metrics --> drop_rate["ドロップ率"]

この図から、接続確立から維持、メッセージ送受信の各段階で、異なる指標を計測する必要があることが理解できます。

課題

WebSocket 固有の SLO/SLI 設計の難しさ

WebSocket のサービスレベルを定義する際、以下のような課題に直面します。

接続の安定性をどう測るか

HTTP では単発のリクエストが成功したかどうかで判断できますが、WebSocket では接続が数時間、数日と続きます。接続断が発生した場合、それが許容範囲内なのか、サービス品質の低下なのかを判断する基準が必要です。

リアルタイム性の保証

チャットアプリやオンラインゲームでは、メッセージの遅延が直接ユーザー体験に影響します。しかし、どの程度の遅延なら許容されるのか、その目標値をどう設定するかは、アプリケーションの性質によって大きく異なるでしょう。

メッセージ配信の信頼性

ネットワークの不安定性やサーバー負荷により、メッセージが失われる可能性があります。この「メッセージドロップ」をどう計測し、どの程度まで許容するかを明確にする必要があります。

以下の図は、WebSocket における主要な課題とその関係性を示しています。

mermaidflowchart LR
  challenge["WebSocket SLO/SLI<br/>設計の課題"] --> stability["接続安定性<br/>の計測"]
  challenge --> realtime["リアルタイム性<br/>の保証"]
  challenge --> reliability["配信信頼性<br/>の測定"]

  stability --> reconnect["再接続頻度"]
  stability --> duration["接続持続時間"]

  realtime --> e2e["エンドツーエンド<br/>遅延"]
  realtime --> processing["処理時間"]

  reliability --> loss["メッセージ<br/>ロス"]
  reliability --> order["順序保証"]

これらの課題を解決するには、WebSocket に特化した指標の設計と計測手法が不可欠です。

解決策

WebSocket における 3 つの重要な SLI

WebSocket のサービスレベルを適切に管理するため、以下の 3 つの SLI を中心に設計します。

#SLI説明重要度
1接続維持率一定期間内に正常に接続を維持できているクライアントの割合★★★
2メッセージ遅延メッセージ送信から受信までのエンドツーエンド遅延★★★
3メッセージドロップ率送信したメッセージのうち、配信に失敗した割合★★☆

これらの指標は相互に関連しており、総合的に監視することでサービス品質を多角的に評価できます。

SLO の目標値設定方針

各 SLI に対する SLO は、以下の方針で設定するとよいでしょう。

接続維持率の目標値

  • 標準レベル: 99.0%(月間約 7.2 時間の接続断を許容)
  • 高レベル: 99.5%(月間約 3.6 時間の接続断を許容)
  • クリティカル: 99.9%(月間約 43 分の接続断を許容)

金融取引や医療など、接続断が重大な影響を与えるシステムではクリティカルレベル、一般的なチャットアプリでは標準レベルが適切です。

メッセージ遅延の目標値

  • リアルタイム要求低: P95 < 1000ms(ダッシュボード更新など)
  • リアルタイム要求中: P95 < 500ms(チャットアプリなど)
  • リアルタイム要求高: P95 < 100ms(オンラインゲーム、トレーディングなど)

P95(95 パーセンタイル)を使用することで、一時的なネットワーク遅延の影響を受けにくくなります。

メッセージドロップ率の目標値

  • 標準レベル: < 0.1%(1000 件中 1 件以下)
  • 高レベル: < 0.01%(10000 件中 1 件以下)
  • クリティカル: < 0.001%(100000 件中 1 件以下)

重要な通知やトランザクションを扱うシステムでは、高レベル以上が求められるでしょう。

以下の図は、各 SLI とその目標値の関係を示しています。

mermaidflowchart TB
  sli["WebSocket SLI"] --> conn["接続維持率"]
  sli --> latency["メッセージ遅延"]
  sli --> drop["ドロップ率"]

  conn --> conn_std["標準: 99.0%"]
  conn --> conn_high["高: 99.5%"]
  conn --> conn_crit["クリティカル: 99.9%"]

  latency --> lat_low["低要求: P95 < 1000ms"]
  latency --> lat_mid["中要求: P95 < 500ms"]
  latency --> lat_high["高要求: P95 < 100ms"]

  drop --> drop_std["標準: < 0.1%"]
  drop --> drop_high["高: < 0.01%"]
  drop --> drop_crit["クリティカル: < 0.001%"]

この図から、アプリケーションの要件に応じて、適切な目標値レベルを選択できることが分かります。

具体例

1. 接続維持率の計測実装

接続維持率を計測するには、各クライアントの接続状態を追跡し、定期的に集計する必要があります。

接続状態の管理クラス

まず、接続状態を管理するクラスを実装しましょう。

typescript// 接続状態を管理するクラス
class ConnectionState {
  public clientId: string;
  public connectedAt: Date;
  public lastHeartbeat: Date;
  public disconnectedAt?: Date;
  public reconnectCount: number = 0;

  constructor(clientId: string) {
    this.clientId = clientId;
    this.connectedAt = new Date();
    this.lastHeartbeat = new Date();
  }

  // 接続が維持されているかチェック
  isConnected(heartbeatTimeoutMs: number = 30000): boolean {
    if (this.disconnectedAt) {
      return false;
    }
    const timeSinceLastHeartbeat =
      Date.now() - this.lastHeartbeat.getTime();
    return timeSinceLastHeartbeat < heartbeatTimeoutMs;
  }

  // ハートビートを更新
  updateHeartbeat(): void {
    this.lastHeartbeat = new Date();
  }

  // 切断を記録
  markDisconnected(): void {
    this.disconnectedAt = new Date();
  }
}

このクラスは、各クライアントの接続開始時刻、最後のハートビート時刻、切断時刻を記録します。ハートビートのタイムアウト判定により、実質的な接続状態を判断できるようになっています。

接続維持率の計算ロジック

次に、接続維持率を計算するロジックを実装します。

typescript// 接続維持率を計測するクラス
class ConnectionUptime {
  private connections: Map<string, ConnectionState> =
    new Map();

  // 新しい接続を登録
  registerConnection(clientId: string): void {
    const state = new ConnectionState(clientId);
    this.connections.set(clientId, state);
  }

  // ハートビートを記録
  recordHeartbeat(clientId: string): void {
    const state = this.connections.get(clientId);
    if (state) {
      state.updateHeartbeat();
    }
  }

  // 切断を記録
  recordDisconnection(clientId: string): void {
    const state = this.connections.get(clientId);
    if (state) {
      state.markDisconnected();
      state.reconnectCount++;
    }
  }
}

このクラスは、すべてのクライアント接続を Map で管理し、接続・ハートビート・切断の各イベントを記録します。

接続維持率の集計

定期的に接続維持率を集計する処理を実装しましょう。

typescript// 接続維持率を計算(続き)
class ConnectionUptime {
  // ... 前述のコード ...

  // 接続維持率を計算
  calculateUptimeRate(
    heartbeatTimeoutMs: number = 30000
  ): number {
    const totalConnections = this.connections.size;
    if (totalConnections === 0) {
      return 100.0; // 接続がない場合は100%とする
    }

    let activeConnections = 0;
    this.connections.forEach((state) => {
      if (state.isConnected(heartbeatTimeoutMs)) {
        activeConnections++;
      }
    });

    return (activeConnections / totalConnections) * 100;
  }

  // SLI レポートを生成
  generateUptimeReport(): UptimeReport {
    const uptimeRate = this.calculateUptimeRate();
    const totalConnections = this.connections.size;

    let activeCount = 0;
    let totalReconnects = 0;

    this.connections.forEach((state) => {
      if (state.isConnected()) {
        activeCount++;
      }
      totalReconnects += state.reconnectCount;
    });

    return {
      uptimeRate,
      totalConnections,
      activeConnections: activeCount,
      disconnectedConnections:
        totalConnections - activeCount,
      averageReconnectsPerClient:
        totalReconnects / totalConnections,
      timestamp: new Date(),
    };
  }
}

// レポートの型定義
interface UptimeReport {
  uptimeRate: number;
  totalConnections: number;
  activeConnections: number;
  disconnectedConnections: number;
  averageReconnectsPerClient: number;
  timestamp: Date;
}

この実装により、現時点の接続維持率と詳細な統計情報を取得できます。再接続回数も追跡することで、接続の安定性を多角的に評価できるようになっています。

2. メッセージ遅延の計測実装

メッセージ遅延は、エンドツーエンドでの配信時間を正確に測定する必要があります。

メッセージにタイムスタンプを付与

メッセージ送信時にタイムスタンプを埋め込む実装を行います。

typescript// メッセージ遅延計測用のペイロード型
interface TimestampedMessage {
  id: string;
  payload: any;
  sentAt: number; // Unix timestamp (ms)
  serverReceivedAt?: number;
  serverSentAt?: number;
}

// メッセージ送信時にタイムスタンプを付与
function createTimestampedMessage(
  payload: any
): TimestampedMessage {
  return {
    id: generateMessageId(),
    payload,
    sentAt: Date.now(),
  };
}

// メッセージIDを生成
function generateMessageId(): string {
  return `msg_${Date.now()}_${Math.random()
    .toString(36)
    .substr(2, 9)}`;
}

このコードでは、メッセージに一意の ID と送信時刻を付与しています。これにより、受信側で遅延を正確に計算できますね。

サーバー側での遅延記録

サーバー側では、受信時刻と転送時刻を記録します。

typescript// サーバー側でのメッセージ処理
class MessageLatencyTracker {
  private latencies: number[] = [];
  private readonly maxSamples: number = 10000;

  // メッセージ受信時の処理
  onMessageReceived(
    ws: WebSocket,
    message: TimestampedMessage
  ): void {
    // サーバー受信時刻を記録
    message.serverReceivedAt = Date.now();

    // ビジネスロジック処理
    const processedMessage = this.processMessage(message);

    // サーバー送信時刻を記録
    processedMessage.serverSentAt = Date.now();

    // クライアントへ送信
    ws.send(JSON.stringify(processedMessage));
  }

  // メッセージ処理(ダミー)
  private processMessage(
    message: TimestampedMessage
  ): TimestampedMessage {
    // 実際のビジネスロジックをここに実装
    return message;
  }
}

サーバーでの受信時刻と送信時刻を記録することで、サーバー処理時間とネットワーク遅延を分離して分析できるようになります。

クライアント側での遅延計測

クライアント側で実際の遅延を計測し、記録します。

typescript// クライアント側での遅延計測
class ClientLatencyTracker {
  private latencies: number[] = [];

  // メッセージ受信時の遅延計測
  onMessageReceived(message: TimestampedMessage): void {
    const receivedAt = Date.now();
    const endToEndLatency = receivedAt - message.sentAt;

    // 遅延を記録
    this.latencies.push(endToEndLatency);

    // サーバー処理時間も計算可能
    if (message.serverReceivedAt && message.serverSentAt) {
      const serverProcessingTime =
        message.serverSentAt - message.serverReceivedAt;
      const networkLatency =
        endToEndLatency - serverProcessingTime;

      console.log(
        `End-to-end: ${endToEndLatency}ms, Server: ${serverProcessingTime}ms, Network: ${networkLatency}ms`
      );
    }
  }
}

このコードにより、エンドツーエンド遅延だけでなく、サーバー処理時間とネットワーク遅延を分離して把握できます。

パーセンタイル値の計算

P50、P95、P99 などのパーセンタイル値を計算する実装です。

typescript// 遅延のパーセンタイル計算
class LatencyMetrics {
  // パーセンタイル値を計算
  static calculatePercentile(
    latencies: number[],
    percentile: number
  ): number {
    if (latencies.length === 0) {
      return 0;
    }

    // ソート済みの配列を作成
    const sorted = [...latencies].sort((a, b) => a - b);
    const index =
      Math.ceil((percentile / 100) * sorted.length) - 1;

    return sorted[Math.max(0, index)];
  }

  // 遅延メトリクスレポートを生成
  static generateLatencyReport(
    latencies: number[]
  ): LatencyReport {
    if (latencies.length === 0) {
      return {
        count: 0,
        min: 0,
        max: 0,
        mean: 0,
        p50: 0,
        p95: 0,
        p99: 0,
      };
    }

    const sum = latencies.reduce(
      (acc, val) => acc + val,
      0
    );
    const mean = sum / latencies.length;
    const sorted = [...latencies].sort((a, b) => a - b);

    return {
      count: latencies.length,
      min: sorted[0],
      max: sorted[sorted.length - 1],
      mean: Math.round(mean * 100) / 100,
      p50: this.calculatePercentile(latencies, 50),
      p95: this.calculatePercentile(latencies, 95),
      p99: this.calculatePercentile(latencies, 99),
    };
  }
}

// レポート型定義
interface LatencyReport {
  count: number;
  min: number;
  max: number;
  mean: number;
  p50: number;
  p95: number;
  p99: number;
}

このメトリクス計算により、SLO として設定した P95 や P99 の値を定期的に評価できますね。

3. メッセージドロップ率の計測実装

メッセージドロップ率を計測するには、送信したメッセージと受信確認を対応付ける必要があります。

ACK(確認応答)メカニズムの実装

まず、メッセージに対する ACK を管理する仕組みを作ります。

typescript// ACK 管理用のメッセージ型
interface AckableMessage extends TimestampedMessage {
  requiresAck: boolean;
  ackReceivedAt?: number;
}

// ACK 待ちメッセージを管理するクラス
class MessageAckTracker {
  private pendingAcks: Map<string, AckableMessage> =
    new Map();
  private readonly ackTimeoutMs: number = 5000; // 5秒でタイムアウト

  // メッセージ送信時に記録
  trackMessage(message: AckableMessage): void {
    if (message.requiresAck) {
      this.pendingAcks.set(message.id, message);

      // タイムアウト処理を設定
      setTimeout(() => {
        this.checkTimeout(message.id);
      }, this.ackTimeoutMs);
    }
  }

  // ACK 受信時の処理
  onAckReceived(messageId: string): void {
    const message = this.pendingAcks.get(messageId);
    if (message) {
      message.ackReceivedAt = Date.now();
      this.pendingAcks.delete(messageId);
    }
  }

  // タイムアウトチェック
  private checkTimeout(messageId: string): void {
    const message = this.pendingAcks.get(messageId);
    if (message && !message.ackReceivedAt) {
      // ACK が届かなかった場合の処理
      console.warn(
        `Message ${messageId} did not receive ACK`
      );
      this.pendingAcks.delete(messageId);
    }
  }
}

この実装により、メッセージごとに ACK の受信状況を追跡し、タイムアウトを検出できます。

ドロップ率の計算

送信メッセージ数と ACK 未受信数からドロップ率を計算します。

typescript// メッセージドロップ率を計測するクラス
class MessageDropRateTracker {
  private totalSent: number = 0;
  private totalAcked: number = 0;
  private totalDropped: number = 0;

  // メッセージ送信を記録
  recordSent(): void {
    this.totalSent++;
  }

  // ACK 受信を記録
  recordAck(): void {
    this.totalAcked++;
  }

  // ドロップを記録
  recordDrop(): void {
    this.totalDropped++;
  }

  // ドロップ率を計算
  calculateDropRate(): number {
    if (this.totalSent === 0) {
      return 0;
    }
    return (this.totalDropped / this.totalSent) * 100;
  }

  // 詳細レポートを生成
  generateDropRateReport(): DropRateReport {
    const dropRate = this.calculateDropRate();
    const deliveryRate =
      (this.totalAcked / this.totalSent) * 100;

    return {
      totalSent: this.totalSent,
      totalAcked: this.totalAcked,
      totalDropped: this.totalDropped,
      dropRate: Math.round(dropRate * 10000) / 10000, // 小数点4桁
      deliveryRate:
        Math.round(deliveryRate * 10000) / 10000,
      timestamp: new Date(),
    };
  }
}

// レポート型定義
interface DropRateReport {
  totalSent: number;
  totalAcked: number;
  totalDropped: number;
  dropRate: number;
  deliveryRate: number;
  timestamp: Date;
}

このコードにより、メッセージのドロップ率と配信成功率を正確に把握できます。

WebSocket サーバーへの統合

これまでの計測ロジックを WebSocket サーバーに統合します。

typescriptimport WebSocket from 'ws';

// WebSocket サーバーに計測機能を統合
class MonitoredWebSocketServer {
  private wss: WebSocket.Server;
  private connectionUptime: ConnectionUptime;
  private dropRateTracker: MessageDropRateTracker;
  private ackTracker: MessageAckTracker;

  constructor(port: number) {
    this.wss = new WebSocket.Server({ port });
    this.connectionUptime = new ConnectionUptime();
    this.dropRateTracker = new MessageDropRateTracker();
    this.ackTracker = new MessageAckTracker();

    this.setupEventHandlers();
    this.startPeriodicReporting();
  }

  // イベントハンドラーのセットアップ
  private setupEventHandlers(): void {
    this.wss.on('connection', (ws: WebSocket, req) => {
      const clientId = this.extractClientId(req);

      // 接続を登録
      this.connectionUptime.registerConnection(clientId);

      this.setupClientHandlers(ws, clientId);
    });
  }

  // クライアントIDを抽出(URLパラメータなどから)
  private extractClientId(req: any): string {
    // 実装例: クエリパラメータからclientIdを取得
    const url = new URL(req.url || '', 'http://localhost');
    return (
      url.searchParams.get('clientId') ||
      `client_${Date.now()}`
    );
  }
}

サーバーの基本構造を作成し、各計測機能を統合する準備を整えました。

クライアントごとのハンドラー設定

各クライアント接続に対するハンドラーを設定します。

typescript// クライアントハンドラーの設定(続き)
class MonitoredWebSocketServer {
  // ... 前述のコード ...

  private setupClientHandlers(
    ws: WebSocket,
    clientId: string
  ): void {
    // メッセージ受信時
    ws.on('message', (data: WebSocket.Data) => {
      try {
        const message = JSON.parse(data.toString());

        // ハートビートの場合
        if (message.type === 'heartbeat') {
          this.connectionUptime.recordHeartbeat(clientId);
          return;
        }

        // ACK の場合
        if (message.type === 'ack') {
          this.ackTracker.onAckReceived(message.messageId);
          this.dropRateTracker.recordAck();
          return;
        }

        // 通常のメッセージ処理
        this.handleMessage(ws, clientId, message);
      } catch (error) {
        console.error('Message parsing error:', error);
      }
    });

    // 切断時
    ws.on('close', () => {
      this.connectionUptime.recordDisconnection(clientId);
      console.log(`Client ${clientId} disconnected`);
    });

    // エラー時
    ws.on('error', (error) => {
      console.error(`Client ${clientId} error:`, error);
      this.connectionUptime.recordDisconnection(clientId);
    });
  }

  // メッセージ処理
  private handleMessage(
    ws: WebSocket,
    clientId: string,
    message: any
  ): void {
    // ビジネスロジックをここに実装
    console.log(
      `Received message from ${clientId}:`,
      message
    );
  }
}

ハートビート、ACK、通常メッセージの各タイプに応じた処理を実装しています。これにより、接続状態とメッセージ配信状況を正確に追跡できますね。

メッセージ送信とドロップ検出

サーバーからクライアントへメッセージを送信する際の処理です。

typescript// メッセージ送信機能(続き)
class MonitoredWebSocketServer {
  // ... 前述のコード ...

  // クライアントにメッセージを送信
  sendMessage(clientId: string, payload: any): void {
    // タイムスタンプ付きメッセージを作成
    const message: AckableMessage = {
      id: generateMessageId(),
      payload,
      sentAt: Date.now(),
      requiresAck: true,
    };

    // 送信を記録
    this.dropRateTracker.recordSent();
    this.ackTracker.trackMessage(message);

    // クライアントを検索して送信
    const sent = this.sendToClient(clientId, message);

    if (!sent) {
      // 送信失敗の場合はドロップとして記録
      this.dropRateTracker.recordDrop();
    }
  }

  // 実際の送信処理
  private sendToClient(
    clientId: string,
    message: AckableMessage
  ): boolean {
    let sent = false;

    this.wss.clients.forEach((client) => {
      // クライアントIDのマッチング処理(実装により異なる)
      if (client.readyState === WebSocket.OPEN) {
        try {
          client.send(JSON.stringify(message));
          sent = true;
        } catch (error) {
          console.error(
            `Failed to send to ${clientId}:`,
            error
          );
          this.dropRateTracker.recordDrop();
        }
      }
    });

    return sent;
  }

  // 全クライアントにブロードキャスト
  broadcast(payload: any): void {
    this.wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        const message: AckableMessage = {
          id: generateMessageId(),
          payload,
          sentAt: Date.now(),
          requiresAck: true,
        };

        this.dropRateTracker.recordSent();
        this.ackTracker.trackMessage(message);

        try {
          client.send(JSON.stringify(message));
        } catch (error) {
          console.error('Broadcast error:', error);
          this.dropRateTracker.recordDrop();
        }
      }
    });
  }
}

この実装により、メッセージ送信の成功・失敗を追跡し、ドロップ率の正確な計測が可能になります。

4. 定期的なレポート生成

計測した SLI を定期的に集計し、レポートとして出力します。

統合レポートの生成

すべての SLI を統合したレポートを生成する処理です。

typescript// 統合 SLI レポート型
interface ComprehensiveSLIReport {
  timestamp: Date;
  uptime: UptimeReport;
  latency: LatencyReport;
  dropRate: DropRateReport;
  sloCompliance: SLOComplianceReport;
}

// SLO 達成状況のレポート型
interface SLOComplianceReport {
  uptimeCompliant: boolean;
  latencyCompliant: boolean;
  dropRateCompliant: boolean;
  overallCompliant: boolean;
}

// 定期レポート生成(続き)
class MonitoredWebSocketServer {
  // ... 前述のコード ...

  private latencyTracker: MessageLatencyTracker =
    new MessageLatencyTracker();
  private reportIntervalMs: number = 60000; // 1分ごと

  // 定期レポートを開始
  private startPeriodicReporting(): void {
    setInterval(() => {
      const report = this.generateComprehensiveReport();
      this.publishReport(report);
      this.checkSLOCompliance(report);
    }, this.reportIntervalMs);
  }

  // 統合レポートを生成
  private generateComprehensiveReport(): ComprehensiveSLIReport {
    const uptimeReport =
      this.connectionUptime.generateUptimeReport();
    const dropRateReport =
      this.dropRateTracker.generateDropRateReport();

    // 仮の遅延データ(実際は latencyTracker から取得)
    const latencyReport: LatencyReport = {
      count: 0,
      min: 0,
      max: 0,
      mean: 0,
      p50: 0,
      p95: 0,
      p99: 0,
    };

    const sloCompliance = this.evaluateSLOCompliance(
      uptimeReport,
      latencyReport,
      dropRateReport
    );

    return {
      timestamp: new Date(),
      uptime: uptimeReport,
      latency: latencyReport,
      dropRate: dropRateReport,
      sloCompliance,
    };
  }
}

すべての SLI を統合することで、サービス全体の健全性を一目で把握できるようになります。

SLO 達成状況の評価

設定した SLO に対する達成状況を評価します。

typescript// SLO 達成評価(続き)
class MonitoredWebSocketServer {
  // ... 前述のコード ...

  // SLO の目標値(設定可能にする)
  private sloTargets = {
    uptimeRate: 99.5, // 99.5%
    latencyP95: 500, // 500ms
    dropRate: 0.1, // 0.1%
  };

  // SLO 達成状況を評価
  private evaluateSLOCompliance(
    uptime: UptimeReport,
    latency: LatencyReport,
    dropRate: DropRateReport
  ): SLOComplianceReport {
    const uptimeCompliant =
      uptime.uptimeRate >= this.sloTargets.uptimeRate;
    const latencyCompliant =
      latency.p95 <= this.sloTargets.latencyP95;
    const dropRateCompliant =
      dropRate.dropRate <= this.sloTargets.dropRate;

    return {
      uptimeCompliant,
      latencyCompliant,
      dropRateCompliant,
      overallCompliant:
        uptimeCompliant &&
        latencyCompliant &&
        dropRateCompliant,
    };
  }

  // SLO 違反時の処理
  private checkSLOCompliance(
    report: ComprehensiveSLIReport
  ): void {
    const { sloCompliance } = report;

    if (!sloCompliance.overallCompliant) {
      console.warn('SLO violation detected!');

      if (!sloCompliance.uptimeCompliant) {
        console.warn(
          `Uptime SLO violated: ${report.uptime.uptimeRate}% < ${this.sloTargets.uptimeRate}%`
        );
        this.alertUptimeViolation(report.uptime);
      }

      if (!sloCompliance.latencyCompliant) {
        console.warn(
          `Latency SLO violated: P95 ${report.latency.p95}ms > ${this.sloTargets.latencyP95}ms`
        );
        this.alertLatencyViolation(report.latency);
      }

      if (!sloCompliance.dropRateCompliant) {
        console.warn(
          `Drop rate SLO violated: ${report.dropRate.dropRate}% > ${this.sloTargets.dropRate}%`
        );
        this.alertDropRateViolation(report.dropRate);
      }
    }
  }

  // アラート処理(実装例)
  private alertUptimeViolation(uptime: UptimeReport): void {
    // 実際の環境では、Slack や PagerDuty などに通知
    console.error(
      'ALERT: Connection uptime below SLO threshold'
    );
  }

  private alertLatencyViolation(
    latency: LatencyReport
  ): void {
    console.error(
      'ALERT: Message latency exceeds SLO threshold'
    );
  }

  private alertDropRateViolation(
    dropRate: DropRateReport
  ): void {
    console.error(
      'ALERT: Message drop rate exceeds SLO threshold'
    );
  }
}

SLO 違反を自動検出し、アラートを発することで、サービス品質の低下に迅速に対応できるようになります。

レポートの出力と可視化

生成したレポートを外部システムに送信する処理です。

typescript// レポート出力(続き)
class MonitoredWebSocketServer {
  // ... 前述のコード ...

  // レポートを出力
  private publishReport(
    report: ComprehensiveSLIReport
  ): void {
    // コンソールに出力
    this.logReportToConsole(report);

    // メトリクスシステムに送信(Prometheus, Datadog など)
    this.sendToMetricsSystem(report);

    // ログファイルに記録
    this.writeToLogFile(report);
  }

  // コンソール出力
  private logReportToConsole(
    report: ComprehensiveSLIReport
  ): void {
    console.log(
      '\n========== WebSocket SLI Report =========='
    );
    console.log(
      `Timestamp: ${report.timestamp.toISOString()}`
    );
    console.log(`\nConnection Uptime:`);
    console.log(
      `  Rate: ${report.uptime.uptimeRate.toFixed(2)}%`
    );
    console.log(
      `  Active: ${report.uptime.activeConnections}/${report.uptime.totalConnections}`
    );
    console.log(`\nMessage Latency:`);
    console.log(`  P50: ${report.latency.p50}ms`);
    console.log(`  P95: ${report.latency.p95}ms`);
    console.log(`  P99: ${report.latency.p99}ms`);
    console.log(`\nMessage Drop Rate:`);
    console.log(
      `  Rate: ${report.dropRate.dropRate.toFixed(4)}%`
    );
    console.log(
      `  Sent: ${report.dropRate.totalSent}, Dropped: ${report.dropRate.totalDropped}`
    );
    console.log(`\nSLO Compliance:`);
    console.log(
      `  Overall: ${
        report.sloCompliance.overallCompliant ? '✓' : '✗'
      }`
    );
    console.log(
      `  Uptime: ${
        report.sloCompliance.uptimeCompliant ? '✓' : '✗'
      }`
    );
    console.log(
      `  Latency: ${
        report.sloCompliance.latencyCompliant ? '✓' : '✗'
      }`
    );
    console.log(
      `  Drop Rate: ${
        report.sloCompliance.dropRateCompliant ? '✓' : '✗'
      }`
    );
    console.log(
      '==========================================\n'
    );
  }

  // メトリクスシステムへ送信(例: Prometheus)
  private sendToMetricsSystem(
    report: ComprehensiveSLIReport
  ): void {
    // Prometheus の場合は、メトリクスエンドポイントを公開
    // ここでは簡略化のため省略
  }

  // ログファイルに記録
  private writeToLogFile(
    report: ComprehensiveSLIReport
  ): void {
    const logEntry = JSON.stringify(report, null, 2);
    // ファイルシステムへの書き込み処理
    // 実装例: fs.appendFileSync('sli-reports.log', logEntry + '\n');
  }
}

レポートを複数の形式で出力することで、リアルタイム監視と長期的な分析の両方に対応できます。

5. クライアント側の実装例

サーバー側だけでなく、クライアント側でも適切な実装が必要です。

ハートビート送信

定期的にハートビートを送信して接続状態を維持します。

typescript// クライアント側のハートビート実装
class WebSocketClient {
  private ws: WebSocket | null = null;
  private heartbeatIntervalMs: number = 10000; // 10秒ごと
  private heartbeatTimer: NodeJS.Timeout | null = null;

  // サーバーに接続
  connect(url: string, clientId: string): void {
    this.ws = new WebSocket(`${url}?clientId=${clientId}`);

    this.ws.onopen = () => {
      console.log('Connected to WebSocket server');
      this.startHeartbeat();
    };

    this.ws.onclose = () => {
      console.log('Disconnected from WebSocket server');
      this.stopHeartbeat();
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    this.setupMessageHandler();
  }

  // ハートビート開始
  private startHeartbeat(): void {
    this.heartbeatTimer = setInterval(() => {
      this.sendHeartbeat();
    }, this.heartbeatIntervalMs);
  }

  // ハートビート停止
  private stopHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  // ハートビート送信
  private sendHeartbeat(): void {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      const heartbeat = {
        type: 'heartbeat',
        timestamp: Date.now(),
      };
      this.ws.send(JSON.stringify(heartbeat));
    }
  }
}

定期的なハートビート送信により、サーバー側で接続状態を正確に把握できるようになります。

ACK 応答の実装

サーバーから受信したメッセージに対して ACK を返します。

typescript// メッセージハンドラーとACK送信(続き)
class WebSocketClient {
  // ... 前述のコード ...

  private latencyTracker: ClientLatencyTracker =
    new ClientLatencyTracker();

  // メッセージハンドラーのセットアップ
  private setupMessageHandler(): void {
    if (!this.ws) return;

    this.ws.onmessage = (event) => {
      try {
        const message: AckableMessage = JSON.parse(
          event.data
        );

        // 遅延を計測
        this.latencyTracker.onMessageReceived(message);

        // ACK が必要な場合は送信
        if (message.requiresAck) {
          this.sendAck(message.id);
        }

        // メッセージ処理
        this.handleMessage(message);
      } catch (error) {
        console.error('Error processing message:', error);
      }
    };
  }

  // ACK を送信
  private sendAck(messageId: string): void {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      const ack = {
        type: 'ack',
        messageId,
        timestamp: Date.now(),
      };
      this.ws.send(JSON.stringify(ack));
    }
  }

  // メッセージ処理
  private handleMessage(message: AckableMessage): void {
    // アプリケーション固有の処理
    console.log('Received message:', message.payload);
  }
}

ACK を送信することで、サーバー側でメッセージドロップ率を正確に計測できるようになります。

以下の図は、クライアントとサーバー間の SLI 計測フローを示しています。

mermaidsequenceDiagram
  participant C as クライアント
  participant S as サーバー
  participant M as メトリクス<br/>システム

  C->>S: 接続確立
  S->>M: 接続数をカウント

  loop 10秒ごと
    C->>S: ハートビート
    S->>M: 接続維持率を更新
  end

  S->>C: メッセージ送信<br/>(sentAt付与)
  Note over S: 送信時刻を記録

  C->>C: 遅延計測
  C->>S: ACK 送信
  Note over S: ACK受信を記録

  S->>M: 遅延とドロップ率<br/>を記録

  M->>M: SLI レポート生成<br/>SLO 評価

このシーケンス図から、各計測ポイントでの処理の流れと、メトリクスシステムへのデータ集約が理解できます。

6. モニタリングダッシュボードの設計

計測した SLI を可視化するダッシュボードの設計も重要です。

Prometheus メトリクスエンドポイント

Prometheus 形式でメトリクスを公開する実装例です。

typescriptimport express from 'express';

// Prometheus メトリクスエンドポイント
class PrometheusMetrics {
  private app: express.Application;
  private port: number = 9090;

  constructor(private server: MonitoredWebSocketServer) {
    this.app = express();
    this.setupEndpoints();
  }

  // エンドポイントをセットアップ
  private setupEndpoints(): void {
    this.app.get('/metrics', (req, res) => {
      const metrics = this.generatePrometheusMetrics();
      res.set('Content-Type', 'text/plain');
      res.send(metrics);
    });
  }

  // Prometheus 形式のメトリクスを生成
  private generatePrometheusMetrics(): string {
    const report =
      this.server.generateComprehensiveReport();

    const metrics = [
      // 接続維持率
      `# HELP websocket_uptime_rate Connection uptime rate percentage`,
      `# TYPE websocket_uptime_rate gauge`,
      `websocket_uptime_rate ${report.uptime.uptimeRate}`,

      // アクティブ接続数
      `# HELP websocket_active_connections Number of active connections`,
      `# TYPE websocket_active_connections gauge`,
      `websocket_active_connections ${report.uptime.activeConnections}`,

      // メッセージ遅延
      `# HELP websocket_latency_p95 Message latency P95 in milliseconds`,
      `# TYPE websocket_latency_p95 gauge`,
      `websocket_latency_p95 ${report.latency.p95}`,

      `# HELP websocket_latency_p99 Message latency P99 in milliseconds`,
      `# TYPE websocket_latency_p99 gauge`,
      `websocket_latency_p99 ${report.latency.p99}`,

      // ドロップ率
      `# HELP websocket_drop_rate Message drop rate percentage`,
      `# TYPE websocket_drop_rate gauge`,
      `websocket_drop_rate ${report.dropRate.dropRate}`,

      // SLO 達成状況(1 = 達成, 0 = 未達成)
      `# HELP websocket_slo_compliance SLO compliance status`,
      `# TYPE websocket_slo_compliance gauge`,
      `websocket_slo_compliance{metric="uptime"} ${
        report.sloCompliance.uptimeCompliant ? 1 : 0
      }`,
      `websocket_slo_compliance{metric="latency"} ${
        report.sloCompliance.latencyCompliant ? 1 : 0
      }`,
      `websocket_slo_compliance{metric="drop_rate"} ${
        report.sloCompliance.dropRateCompliant ? 1 : 0
      }`,
    ];

    return metrics.join('\n');
  }

  // メトリクスサーバーを起動
  start(): void {
    this.app.listen(this.port, () => {
      console.log(
        `Prometheus metrics available at http://localhost:${this.port}/metrics`
      );
    });
  }
}

この実装により、Prometheus や Grafana などの標準的な監視ツールで WebSocket のメトリクスを可視化できますね。

Grafana ダッシュボード設定例

Grafana でダッシュボードを作成する際の設定例をご紹介します。

以下のクエリを使用することで、各 SLI を可視化できます。

#パネル名Prometheus クエリ説明
1接続維持率websocket_uptime_rate時系列グラフで推移を表示
2アクティブ接続数websocket_active_connections現在の接続数をゲージで表示
3メッセージ遅延 P95websocket_latency_p95SLO ラインと共に表示
4メッセージドロップ率websocket_drop_rateパーセンテージで表示
5SLO 達成状況sum(websocket_slo_compliance)達成している指標の数

これらのパネルを配置することで、WebSocket サービスの健全性を一目で把握できるダッシュボードが完成します。

7. エラーケースとトラブルシューティング

実際の運用では、さまざまなエラーケースに対応する必要があります。

接続維持率低下時の診断

接続維持率が SLO を下回った場合の診断手順です。

Error Code: CONNECTION_UPTIME_BELOW_SLO

textError: Connection uptime rate fell below SLO threshold
Current Rate: 98.5%
Target SLO: 99.5%
Affected Connections: 150/10000

発生条件

  1. ネットワークの不安定性
  2. サーバー側のリソース不足
  3. クライアント側の不具合

解決方法

  1. 再接続パターンの確認: 頻繁に再接続が発生しているクライアントを特定します
typescript// 再接続が多いクライアントを特定
function identifyProblematicClients(
  connections: Map<string, ConnectionState>
): string[] {
  const threshold = 5; // 5回以上の再接続
  const problematic: string[] = [];

  connections.forEach((state, clientId) => {
    if (state.reconnectCount > threshold) {
      problematic.push(clientId);
      console.log(
        `Client ${clientId} has ${state.reconnectCount} reconnects`
      );
    }
  });

  return problematic;
}
  1. サーバーリソースの確認: CPU やメモリの使用率をチェックします

  2. ネットワーク遅延の調査: クライアントとサーバー間のネットワーク品質を確認します

メッセージ遅延超過時の診断

メッセージ遅延が SLO を超えた場合の対処法です。

Error Code: MESSAGE_LATENCY_EXCEEDED_SLO

textError: Message latency P95 exceeded SLO threshold
Current P95: 750ms
Target SLO: 500ms
Sample Size: 50000 messages

発生条件

  1. サーバー処理の遅延
  2. ネットワーク帯域の逼迫
  3. クライアント処理の遅延

解決方法

  1. 遅延の内訳を分析: サーバー処理時間とネットワーク遅延を分離します
typescript// 遅延の内訳を分析
function analyzeLatencyBreakdown(
  messages: TimestampedMessage[]
): LatencyBreakdown {
  let totalServerTime = 0;
  let totalNetworkTime = 0;
  let count = 0;

  messages.forEach((msg) => {
    if (msg.serverReceivedAt && msg.serverSentAt) {
      const serverTime =
        msg.serverSentAt - msg.serverReceivedAt;
      totalServerTime += serverTime;
      count++;
    }
  });

  return {
    averageServerProcessing:
      count > 0 ? totalServerTime / count : 0,
    // 他の統計情報も追加
  };
}

interface LatencyBreakdown {
  averageServerProcessing: number;
}
  1. ボトルネックの特定: プロファイリングツールを使用してサーバー処理の遅い箇所を特定します

  2. スケーリング: サーバーインスタンスを増やすか、負荷分散を改善します

メッセージドロップ率上昇時の診断

メッセージドロップ率が上昇した場合の対処法です。

Error Code: MESSAGE_DROP_RATE_EXCEEDED_SLO

textError: Message drop rate exceeded SLO threshold
Current Rate: 0.25%
Target SLO: 0.1%
Total Sent: 100000, Dropped: 250

発生条件

  1. クライアントの接続断
  2. ネットワークパケットロス
  3. バッファオーバーフロー

解決方法

  1. ドロップパターンの分析: どのようなメッセージがドロップされているか確認します
typescript// ドロップされたメッセージを分析
function analyzeDroppedMessages(
  droppedMessages: AckableMessage[]
): DropAnalysis {
  const byReason: Map<string, number> = new Map();

  droppedMessages.forEach((msg) => {
    // ドロップ理由を分類
    const reason = categorizeDropReason(msg);
    byReason.set(reason, (byReason.get(reason) || 0) + 1);
  });

  return {
    totalDropped: droppedMessages.length,
    byReason: Object.fromEntries(byReason),
  };
}

function categorizeDropReason(msg: AckableMessage): string {
  // 実際の実装では、より詳細な分類を行う
  if (!msg.ackReceivedAt) {
    return 'ack_timeout';
  }
  return 'unknown';
}

interface DropAnalysis {
  totalDropped: number;
  byReason: { [key: string]: number };
}
  1. 再送メカニズムの導入: 重要なメッセージには再送ロジックを実装します

  2. クライアント側のバッファリング: 一時的な接続断に備えてクライアント側でメッセージをバッファします

これらのエラーケースと診断方法を事前に準備しておくことで、SLO 違反が発生した際に迅速に対応できるようになります。

まとめ

WebSocket のサービスレベルを適切に管理するには、接続維持率、メッセージ遅延、メッセージドロップ率という 3 つの重要な SLI を中心に設計することが効果的です。

接続維持率では、クライアントの接続状態を継続的に追跡し、ハートビートメカニズムにより実質的な接続状況を把握しました。99.0% から 99.9% までの目標値設定により、アプリケーションの要件に応じた柔軟な運用が可能になります。

メッセージ遅延については、エンドツーエンドでの計測が重要でしたね。タイムスタンプをメッセージに埋め込み、サーバー処理時間とネットワーク遅延を分離することで、ボトルネックの特定が容易になります。P95 や P99 などのパーセンタイル値を使用することで、一時的なスパイクに左右されない安定した評価ができるでしょう。

メッセージドロップ率の計測には、ACK メカニズムの実装が欠かせません。各メッセージに対する確認応答を追跡することで、配信の信頼性を数値化し、SLO として管理できます。

これらの指標を Prometheus や Grafana などの標準的な監視ツールと統合することで、リアルタイムでの監視と長期的な傾向分析の両方が実現できます。SLO 違反時には自動的にアラートを発することで、サービス品質の低下に迅速に対応できる体制を整えられるのです。

実際の運用では、ここで紹介した基本的な実装をベースに、アプリケーション固有の要件に合わせてカスタマイズしていくとよいでしょう。定期的に SLO の妥当性を見直し、ビジネス要件とユーザー体験の両面から最適な目標値を設定することが、安定したサービス提供につながります。

関連リンク