Back

WebSocketで100万同時接続を処理する方法:C1M問題へのディープダイブ

「C10K問題」はもはや過去の話です。今日、私たちが直面しているのは、単一サーバーで100万の同時接続を処理するC1M問題です。

チャットアプリ、ライブスポーツ配信、あるいは共同編集ツールのようなリアルタイムアプリケーションを構築していると、いずれ壁にぶつかります。サーバーがクラッシュし、新しい接続がタイムアウトし、レイテンシが急上昇するのです。そして、その原因がアプリケーションコードにあることは稀です。犯人は通常、オペレーティングシステム(OS)の設定にあります。

この記事では、Linuxサーバーをチューニングして100万のWebSocket同時接続を処理するための具体的な手順を解説します。ファイルディスクリプタ、エフェメラルポート(Ephemeral Ports)、そして単一ノードを超えてスケールするためのアーキテクチャまで、深く掘り下げていきましょう。

ボトルネックは(通常)Node.jsではありません

多くの開発者は、Node.js(またはPython/Ruby)が遅すぎて数百万の接続を処理できないと考えがちです。確かにシングルスレッドの限界はありますが、最初に直面するボトルネックはほぼ間違いなくオペレーティングシステムです。

基本的に、Linuxは数百万の持続的なTCP接続を維持するためではなく、一般的なコンピューティング目的のために設定されているからです。

1. "Too Many Open Files" エラー

Linuxでは、すべてがファイルです。ソケットもファイルですし、ディスク上のファイルもファイルです。プロセスが接続を開くたびに、ファイルディスクリプタ(FD)を1つ消費します。

現在の制限を確認してみてください:

ulimit -n # 出力: 1024 (通常はこれです)

もし1,025番目の接続を開こうとすると、アプリケーションは EMFILE: too many open files エラーを吐いてクラッシュするでしょう。

解決策:

システム全体の制限と、プロセスごとの制限の両方を増やす必要があります。

/etc/sysctl.conf を編集します:

fs.file-max = 2097152

/etc/security/limits.conf を編集します:

* soft nofile 1048576 * hard nofile 1048576 root soft nofile 1048576 root hard nofile 1048576

sysctl -p コマンドで変更を適用してください。これでOSは十分なファイルディスクリプタを許可するようになります。しかし、これは始まりに過ぎません。

2. エフェメラルポート枯渇(Ephemeral Port Exhaustion)の罠

これは、大規模なWebSocketアーキテクチャにおいて最も一般的な「静かなる殺人者」です。

クライアントがサーバーに接続するとき、ソースIPとソースポートを使用します。逆に、あなたのサーバーがバックエンドのデータベースや他のサービスに接続するときは、サーバーがクライアントとなり、ローカルポートを使用します。

利用可能なポートは65,535個しかありません。もしWebSocketサーバーの手前にロードバランサーやプロキシがある場合、外向きの接続のためのポートが不足する可能性があります。

TCPタプル(Tuple)

TCP接続は4つの情報で識別されます:
{ソースIP, ソースポート, 宛先IP, 宛先ポート}

ロードバランサーがWebSocketサーバーに接続するとき、両方のサーバーが特定のIPに固定されている場合、利用可能なソースポート数(約6万個)によって制限を受けることになります。

解決策:

  1. ローカルポートの範囲を広げる:

    # /etc/sysctl.conf net.ipv4.ip_local_port_range = 1024 65535
  2. TCP再利用を有効にする:

    net.ipv4.tcp_tw_reuse = 1

    この設定により、OSは TIME_WAIT 状態のポートをより早く再利用できるようになります。

3. メモリ使用量:ソケットの本当のコスト

100万の接続はどれくらいのRAMを消費するのでしょうか?

過去には、接続1つあたり数キロバイトのカーネルメモリを消費していましたが、最新のカーネルは非常に効率的です。むしろ、アプリケーションメモリの方が大きな要因となります。

Node.jsを使用している場合、すべての Socket オブジェクトはヒープ領域を占有します。

  • 空のソケット: ~2-4 KB
  • メタデータ込み(User ID, チャンネル情報など): ~10 KB

計算してみましょう:
1,000,000 接続 * 10 KB = 10 GB RAM

最新のサーバーであれば十分に可能な数値ですが、注意が必要です。

最適化のヒント:
ソケットオブジェクトにUserオブジェクト全体を保存しないでください。userId だけを保存し、詳細は必要なときにRedisから取得するようにしましょう。

// 悪い例 socket.user = { id: 1, name: "Alice", email: "...", bio: "..." }; // 良い例 socket.userId = 1;

4. イベントループの遅延(Event Loop Lag)

Node.jsはシングルスレッドです。100万人のユーザーが接続されている状態で、その全員にメッセージをブロードキャストしようとすると、イベントループがブロックされます。

// 絶対にやってはいけません users.forEach(user => { user.socket.send("こんにちは!"); });

100万個のパケットを送信するには時間がかかります。1送信あたり0.01msかかるとすると、合計で10秒間のブロッキングが発生します。サーバーは10秒間応答しなくなります。

解決策:バッチ処理(Batching)とワーカー(Workers)

  1. 一度に全員に送信しないでください。 ブロードキャストをチャンク(chunk)単位に分割して送信しましょう。
  2. マルチプロセスを使用してください。 Node.jsの cluster モジュールを使用するか、複数のコンテナインスタンスを実行してください。

5. 無限スケールのためのアーキテクチャ

単一サーバーのチューニングで100万接続は可能です。しかし、1,000万、1億接続はどうでしょうか?

分散WebSocketアーキテクチャが必要です。

レイヤー化アプローチ(The Layered Approach)

  1. ロードバランサー(Nginx/HAProxy): SSLを終端し、バックエンドノードに接続を分散します。
  2. WebSocketノード(Node.js/Go): アクティブな接続を維持します。ソケットを保持している点では「Stateful」ですが、ビジネスロジックの観点からは「Stateless」に保つべきです。
  3. Pub/Subレイヤー(Redis/NATS): すべてをつなぐ接着剤の役割を果たします。

Pub/Subの仕組み

サーバー1に接続されたユーザーAが、サーバー2に接続されたユーザーBにメッセージを送りたい場合:

  1. ユーザーAがサーバー1にメッセージを送信します。
  2. サーバー1はRedisチャンネルにメッセージを発行(Publish)します:publish('user:B', payload)
  3. サーバー2(そして他のすべてのサーバー)はRedisを購読(Subscribe)しています。
  4. サーバー2はメッセージを受け取り、ユーザーBがローカルに接続されているか確認します。
  5. サーバー2がユーザーBにメッセージを送信します。

このアーキテクチャを採用すれば、サーバーを水平方向に無制限に拡張できます。

6. 限界をテストする

本番環境でクラッシュするのを待ってはいけません。テストが必要です。

Artilleryk6のようなツールも素晴らしいですが、大規模な同時接続をテストするには、クライアントマシンの艦隊(fleet)が必要になるかもしれません。

Tsungは、数百万の同時ユーザーをシミュレーションするのに優れたErlangベースの分散負荷テストツールです。ぜひ一度使ってみてください。

結論

100万接続を処理することは、システムエンジニアにとって勲章のようなものです。それは「npm install」の快適さを抜け出し、Linuxの内部を覗き込むことを要求します。

要約チェックリスト:

  1. ulimit を増やす(Open Files)。
  2. sysctl をチューニングする(ポート範囲、TCP再利用)。
  3. アプリケーションメモリを最適化する(オブジェクトではなくIDを保存)。
  4. 水平スケールのためにPub/Subバックエンド(Redis)を使用する。

次にトラフィックの急増でサーバーが落ちたときは、インスタンスサイズを上げるだけでなく、カーネルを覗いてみてください。答えは通常そこにあります。

System DesignWebSocketsPerformanceLinuxNode.js

関連ツールを見る

Pockitの無料開発者ツールを試してみましょう