テックブログ

WebSocketの負荷対策:同時接続数とスケールアウトの基本

WebSocketの負荷対策:同時接続数とスケールアウトの基本

WebSocketは接続を長時間維持するため、負荷を考える際は同時接続数だけでなく、メッセージ頻度、1メッセージあたりのサイズ、heartbeat、CPUを使うアプリケーション処理、メモリ使用量、ネットワーク帯域などを合わせて見る必要があります。1台のサーバーで処理しきれなくなった場合は、複数台へスケールアウトして接続を分散できますが、接続先が分かれることで、別サーバーに接続している利用者へイベントを届ける仕組みも必要になります。最大接続数は環境によって大きく変わるため、固定値ではなく実際の構成とロードテストをもとに判断することが重要です。

1. WebSocketで負荷を考える理由

1-1. 接続を長時間保持する

WebSocketでは、HTTP APIのようにリクエストごとに通信を終えるのではなく、クライアントとの接続を長時間維持します。そのため、サーバーは利用者が画面を開いている間、継続して接続を管理する必要があります。

1つの接続を維持するには、ソケットだけでなく、接続を表すオブジェクト、ユーザー情報、購読中のチャンネル、送信待ちデータなどの状態を保持する場合があります。OS側でもファイルディスクリプターやネットワーク関連のリソースが利用されます。

例えば100人の開発環境では問題なくても、本番で利用者が大きく増えると、同時に維持する接続も増えます。ただし「何接続まで可能か」はOS、WebSocketサーバー、ランタイム、メモリ、CPU、ネットワーク、アプリケーション設計によって異なるため、一律の最大値として考えてはいけません。

1-2. 同時接続数だけでは負荷は決まらない

WebSocketの負荷は、同時接続数だけを見ても正しく判断できません。同じ1万接続でも、ほとんど通信しないシステムと、全接続が頻繁に送受信するシステムでは負荷が大きく異なります。

接続しているだけの状態では、主に接続管理に必要なメモリやOSリソースが消費されます。一方、メッセージを送受信すると、JSONの解析、認証・認可、データベースアクセス、メッセージ生成、暗号化通信、ログ記録などの処理が加わります。

例えば通知サービスで「普段は接続しているだけで、数分に1回通知が届く」場合と、共同編集サービスで「各ユーザーが毎秒何度も更新を送る」場合では、同じ同時接続数でも必要なCPUやネットワーク性能は異なります。接続数と通信量は分けて測定します。

1-3. メッセージ頻度と処理内容も影響する

WebSocketサーバーの負荷には、1秒あたりのメッセージ数と、1メッセージを処理するためのコストが大きく影響します。小さいメッセージでも高頻度に送られれば、処理回数そのものが増えます。

特に注意したいのは、受信したメッセージごとに重いデータベース処理を行う場合や、1件のイベントを多数のユーザーへ配信する場合です。1人から届いた1メッセージが1000人への送信に展開されるなら、受信数だけを見ても実際の送信負荷は分かりません。

heartbeatも同様です。1接続では小さな通信でも、多数の接続で短い間隔のheartbeatを実行すると、定期的な送受信が積み重なります。負荷設計では「接続数 × 通信頻度 × メッセージサイズ × 処理内容」という視点で考えることが重要です。

2. 同時接続数に影響する要素

2-1. メモリと接続ごとの状態

同時接続数を考えるうえでは、接続1件あたりにどれだけメモリを使用するかを確認する必要があります。WebSocket接続を増やせば、接続ごとの状態も同時に増えるためです。

接続オブジェクトだけでなく、ユーザーID、認証状態、購読一覧、送信キュー、受信バッファーなどをメモリに保持することがあります。さらに、利用しているフレームワークやランタイム自体にも接続管理のオーバーヘッドがあります。

例えば、購読チャンネルの一覧を各接続が大量に保持している構成では、単純な通知専用接続よりも接続1件あたりのメモリ使用量が増えます。机上の推測だけでなく、実際に接続数を増やしながらプロセスメモリやGCの動作を計測します。

2-2. CPUとアプリケーション処理

CPU負荷は、接続を維持しているだけの状態より、メッセージ処理が発生したときに増えやすくなります。特にJSON変換、暗号処理、認証・認可、集計などを高頻度で実行すると影響が大きくなります。

例えば受信したメッセージごとにJSONを解析し、データベースから権限情報を取得し、結果を複数ユーザー向けにシリアライズして送信する場合、1メッセージの処理でも複数のCPU処理が発生します。ログ出力が多すぎる場合も負荷要因になります。

CPU使用率を見るときは平均値だけでなく、メッセージが集中するピーク時も確認します。また、イベントループを利用するランタイムでは、CPUを長時間占有する同期処理があると、他の接続の処理まで遅延する可能性があります。

2-3. ネットワーク帯域と送受信量

多数のクライアントへメッセージを配信するシステムでは、ネットワーク帯域がボトルネックになることがあります。特に同じイベントを大量の接続へブロードキャストする場合は、送信量が大きくなります。

例えば1件あたりのメッセージが小さくても、数多くのクライアントへ高頻度に配信すれば、総送信量は増加します。逆に接続数が多くても、ほとんど通信しないシステムではネットワーク負荷が比較的小さい場合があります。

ロードテストでは、1秒あたりの送受信バイト数、メッセージ数、送信キューの増加、クライアント側での受信遅延などを確認します。メッセージサイズを削減する、不要な更新をまとめる、必要な利用者だけへ配信するといった工夫も有効です。

3. 1台で足りない場合のスケールアウト

3-1. スケールアップとスケールアウトの違い

1台のWebSocketサーバーで負荷を処理できなくなった場合、主な拡張方法としてスケールアップとスケールアウトがあります。両者の違いを理解して使い分けることが重要です。

スケールアップは、CPUやメモリなど1台のサーバー自体を高性能にする方法です。スケールアウトは、WebSocketサーバーを複数台に増やして処理や接続を分散する方法です。

スケールアップは構成を比較的単純に保ちやすい一方、1台で拡張できる範囲には限界があります。スケールアウトは容量を増やしやすい反面、複数サーバー間のイベント共有や接続状態の扱いが必要になり、構成が複雑になります。

3-2. WebSocketサーバーを複数台にする

WebSocketサーバーを複数台へスケールアウトすると、クライアントの接続を異なるサーバーへ分散できます。これにより1台あたりが保持する接続や処理を減らせます。

単一サーバー構成は次のように考えられます。

Client A ─┐
Client B ─┼────> WebSocket Server
Client C ─┤          │
Client D ─┘          └─ 接続状態・配信処理

複数サーバーへ増やすと、例えば次のようになります。

                ┌──> WebSocket Server A ── Client A
Clients ──> LB ─┤                       └─ Client B
                │
                └──> WebSocket Server B ── Client C
                                        └─ Client D

この構成が必要な理由: 接続やメッセージ処理を複数台へ分散できるため、1台への集中を避けられます。

注意点: サーバー台数を2倍にしたからといって性能が必ず2倍になるわけではありません。データベース、メッセージブローカー、ネットワーク、外部APIなど別の場所がボトルネックになることがあります。

3-3. ロードバランサー経由で接続を分散する

複数のWebSocketサーバーへ接続を分散する場合、ロードバランサーを手前に置く構成が一般的です。クライアントは1つのURLへ接続し、ロードバランサーが接続先サーバーを決定します。

ただし、利用するロードバランサーがWebSocket接続を正しく扱えることを確認する必要があります。長時間接続への対応、アイドルタイムアウト、接続ドレイン、ヘルスチェックなどの設定がWebSocketの運用に影響します。

sticky sessionを使って同じクライアントを同じサーバーへ寄せる方法もありますが、万能な解決策ではありません。サーバーが停止すれば再接続先は変わりますし、他サーバーに接続しているユーザーへ通知する問題も残ります。可能な限り、特定サーバーに強く依存しない構成を検討します。

4. 複数サーバーで起きる問題

4-1. クライアントの接続先が分かれる

WebSocketサーバーを複数台にすると、利用者ごとの接続先が異なります。これが単一サーバー構成との大きな違いです。

例えばユーザーAはServer A、ユーザーBはServer Bへ接続しているとします。Server Aのメモリにある接続一覧にはユーザーAしか存在せず、Server Bの接続オブジェクトをServer Aから直接操作することはできません。

User A
  │
  ▼
Server A

Server B
  ▲
  │
User B

この状態でユーザーAからユーザーBへ通知したい場合、Server Aだけを見てもユーザーBのWebSocket接続は見つかりません。そのためサーバー間でイベントを伝える仕組みが必要になります。

4-2. 別サーバーの接続へ直接送信できない

1つのWebSocket接続は、その接続を受け持っているサーバープロセスが管理します。別のサーバーから、そのソケットへ直接send()することはできません。

例えば次のような通信を考えます。

User A
  │ message
  ▼
Server A
  │
  │ 「User Bへ通知したい」
  │
  ✕ 直接は送れない
  │
Server B
  │
  ▼
User B

Server Aは「User Bへ通知したい」というイベントをServer Bへ伝え、Server Bが自分の管理するWebSocket接続へ送信する必要があります。

なぜ必要か: WebSocket接続そのものをサーバー間で共有するのではなく、「誰へ何を届けるか」というイベント情報を共有するためです。これがPub/Subやメッセージブローカーを利用する理由になります。

4-3. 接続状態をどこまで共有するか考える

スケールアウトでは、すべての接続状態を中央へ保存すればよいとは限りません。何を各サーバーのローカル状態として持ち、何を共有するかを分ける必要があります。

実際のWebSocketオブジェクトは、その接続を保持しているサーバーのメモリに置きます。一方、「ユーザーAは現在Server Aに接続中」「ユーザーBはroom-123を購読中」といった情報は、必要に応じて共有ストアで管理できます。

ただし、共有状態を増やすほど同期処理も複雑になります。イベントを全サーバーへ配信し、各サーバーが自分の接続だけへ送る設計なら、詳細な接続先情報を中央管理しなくてもよい場合があります。必要な情報だけを共有することが重要です。

5. サーバー間でイベントを共有する

5-1. Pub/Subという考え方

Pub/Subは、イベントを発行する側と受け取る側を直接結び付けず、トピックやチャンネルを介して情報を配信する考え方です。複数WebSocketサーバー間のイベント共有に利用できます。

Publisherはイベントを発行し、Subscriberは必要なチャンネルを購読します。WebSocketサーバー同士が直接相手のアドレスを知る必要がないため、サーバー台数を増減しやすくなります。

例えばuser:123room:456のようなチャンネルを設け、対象イベントをPublishします。各WebSocketサーバーは必要なイベントを受け取り、自分の管理する接続の中から対象ユーザーへ配信します。

5-2. メッセージブローカーを介して配信する

複数WebSocketサーバーでイベントを共有する場合、メッセージブローカーを中継地点として利用できます。これにより、Server Aで発生したイベントをServer Bへ伝えられます。

ユーザーAからユーザーBへメッセージを届ける流れは、次のようになります。

User A
  │
  │ 1. メッセージ送信
  ▼
WebSocket Server A
  │
  │ 2. 「User B向けイベント」をPublish
  ▼
Message Broker / Pub/Sub
  │
  │ 3. Server Bがイベントを受信
  ▼
WebSocket Server B
  │
  │ 4. User Bの接続へsend
  ▼
User B

何をしているか: Server AはServer BのWebSocketへ直接触らず、イベントだけを共有基盤へ送ります。Server Bはそのイベントを受け取り、自分が保持しているUser Bの接続へ配信します。

なぜ必要か: 接続先サーバーが分散していても、アプリケーション全体としてイベントを届けられるようにするためです。ただし、メッセージブローカー自体の可用性や負荷も設計対象になります。

5-3. Redis Pub/Subなどは実装手段の一例

Redis Pub/Subは、複数WebSocketサーバー間でイベントを共有するための実装手段の1つです。ただし、Redisが唯一の正解というわけではありません。

例えばServer AがRedisのチャンネルへPublishし、Server Bが同じチャンネルをSubscribeしていれば、イベントを受け取れます。構成が比較的理解しやすいため、小規模から中規模のリアルタイム配信で使われることがあります。

                ┌── WebSocket Server A
                │          │
Clients ── LB ──┤          ├── Redis Pub/Sub
                │          │
                └── WebSocket Server B

ただしRedis Pub/Subは、購読していなかった期間のメッセージを後から必ず再取得できる仕組みではありません。メッセージの永続化、再送、順序保証などが必要な場合は、Redis Streamsや他のメッセージングシステムなど、要件に合う仕組みを検討します。

つまり「サーバー間でリアルタイムに知らせたい」のか、「障害時にもイベントを失わず後から再処理したい」のかで選択肢が変わります。Pub/Subという設計概念と、実際に利用する製品を分けて考えます。

6. 本番運用前に確認すること

6-1. ロードバランサーのWebSocket対応とタイムアウト

本番環境では、ロードバランサーやリバースプロキシがWebSocketを正しく扱えるか確認する必要があります。アプリケーションサーバーだけが対応していても、途中の機器が接続を切れば通信は維持できません。

特に確認したいのが、WebSocketへの対応方法とアイドルタイムアウトです。長時間メッセージが流れない接続を一定時間後に切断する設定があると、アプリケーション側では予期しない切断として見えることがあります。

heartbeatを利用すればアイドル状態を避けられる場合がありますが、heartbeatを短くすればよいわけではありません。ロードバランサーの設定、heartbeat間隔、必要な切断検知時間を合わせて設計します。

6-2. 接続数・CPU・メモリ・通信量を監視する

WebSocket運用では、同時接続数だけでなく複数の指標を継続的に監視します。どの資源がボトルネックになっているかを判断するためです。

最低限、次のような指標を確認すると状況を把握しやすくなります。

  • 現在のWebSocket接続数
  • 新規接続数・切断数
  • 再接続回数
  • 1秒あたりの受信メッセージ数
  • 1秒あたりの送信メッセージ数
  • 送受信バイト数
  • CPU使用率
  • メモリ使用量
  • GCの回数や停止時間
  • イベントループ遅延
  • 送信キュー・バッファーの増加
  • メッセージ処理時間
  • 配信遅延
  • エラー率
  • 認証・認可エラー数
  • heartbeatのタイムアウト数
  • メッセージブローカーの遅延やエラー
  • ロードバランサーの接続数・切断数

例えばCPUに余裕があっても、送信キューが増え続けているならネットワークやクライアントの受信速度が問題かもしれません。メモリだけが継続的に増えるなら、切断済み接続の解放漏れなども疑います。

6-3. 想定負荷でロードテストする

WebSocketの最大同時接続数や処理能力は、実際の構成に近い環境でロードテストして確認します。一般的な「何万接続まで可能」という数字をそのまま自分のシステムへ当てはめることはできません。

ロードテストでは、単に大量の接続を開くだけでなく、本番の利用パターンを再現することが重要です。例えば「接続後はほぼ待機」「heartbeatのみ」「定期的に通知」「高頻度に双方向送信」ではサーバーへの負荷が異なります。

テストシナリオでは、少なくとも次の観点を用意します。

  • 接続数を段階的に増やしたときのCPU・メモリ
  • 接続確立に必要な時間
  • 一斉接続・一斉再接続時の負荷
  • 通常時のheartbeat負荷
  • 1接続あたりの平均メッセージ頻度
  • ピーク時のメッセージ頻度
  • 平均・最大メッセージサイズ
  • ブロードキャスト時の送信負荷
  • Server AからServer BへのPub/Sub遅延
  • メッセージブローカー障害時の挙動
  • サーバー1台停止時の再接続
  • ロードバランサー経由での切断・再接続
  • メッセージ処理の平均・上位パーセンタイル遅延
  • 接続数増加によるエラー率の変化
  • テスト終了後にメモリや接続数が正常に戻るか

「接続できた」という結果だけでは十分ではありません。メッセージが期待する時間内に届くか、負荷が上がったときに再接続が集中しないか、サーバーを1台停止しても復旧できるかまで確認します。

7. まとめ

7-1. 接続数とメッセージ負荷を分けて考える

WebSocketの負荷設計では、同時接続数とメッセージ処理の負荷を分けて考えることが重要です。接続数が多くてもほとんど通信しないシステムと、高頻度に大量配信するシステムでは必要なリソースが異なります。

負荷には、メモリ、CPU、ネットワーク帯域、heartbeat、メッセージサイズ、認証・認可、データベース処理、ブロードキャストなどが関係します。「何接続まで可能か」という1つの数字だけでは判断できません。

1台で処理しきれない場合はスケールアウトできますが、複数サーバーへ接続が分かれるため、サーバー間でイベントを共有する仕組みが必要です。ロードバランサー、Pub/Sub、監視、ロードテストまで含めて1つの構成として考えます。

7-2. スケール設計のチェックリスト

WebSocketを本番運用する際は、「とりあえずサーバーを増やせばよい」と考えるのではなく、どのリソースが不足しているかを測定してから対策します。スケールアップ、スケールアウトのどちらが適切かも状況によって異なります。

また、複数サーバーへ分散した後は、ユーザー同士のイベントをどう届けるかという新しい課題が発生します。ロードバランサーだけでは、サーバー間のイベント共有までは解決しません。

実装・運用時には、次の項目を確認してください。

  • 同時接続数を監視できているか
  • 接続1件あたりのおおよそのメモリ使用量を把握しているか
  • 接続数だけで負荷を判断していないか
  • 1秒あたりの送受信メッセージ数を計測しているか
  • メッセージサイズを把握しているか
  • heartbeatの通信量を考慮しているか
  • 高頻度メッセージ時のCPU負荷を確認しているか
  • JSON処理や認証・認可のコストを測定しているか
  • ネットワーク帯域に余裕があるか
  • ブロードキャスト時の送信量を確認しているか
  • OSの接続関連制限を確認しているか
  • ランタイムやWebSocketライブラリの特性を確認しているか
  • スケールアップで解決する問題か整理したか
  • スケールアウトが必要か判断したか
  • ロードバランサーがWebSocketをサポートしているか
  • ロードバランサーのアイドルタイムアウトを確認したか
  • sticky sessionへ過度に依存していないか
  • サーバーAからサーバーBへイベントを共有できるか
  • Pub/Subやメッセージブローカーが必要か検討したか
  • メッセージの永続化や再送が必要か整理したか
  • Redis Pub/Subを唯一の選択肢として考えていないか
  • CPU・メモリ・ネットワークを継続監視しているか
  • 送信キューや配信遅延を監視しているか
  • 一斉再接続を含むロードテストを実施したか
  • サーバー障害時の挙動をテストしたか
  • 本番に近いメッセージ頻度・サイズで検証したか

WebSocketのスケール設計で重要なのは、「最大何接続まで可能か」という数字を探すことではありません。自分のシステムがどのような接続とメッセージを扱うのかを測定し、ボトルネックを特定しながら、必要に応じてサーバー分散とイベント共有の仕組みを追加することが重要です。

8. 参考リンク

この記事では、次の公式仕様・公式資料を参考にしています。