Node.jsにおけるRedis Pub/Subを用いたWebSocketサービスのスケーリング
Ethan Miller
Product Engineer · Leapcell

はじめに
現代のウェブ開発において、WebSocketを通じて提供されるリアルタイム機能は、チャットプラットフォームから共同編集ツールに至るまで、さまざまなアプリケーションに不可欠なものとなっています。アプリケーションの人気と複雑さが増すにつれて、典型的な単一インスタンスのWebSocketサーバーは、スケーラビリティと耐障害性の点で急速に限界に達します。単一のサーバーがダウンすると、接続されているすべてのクライアントが切断され、負荷分散が重大な課題となります。これらの懸念に対処するためには、通常、WebSocketサービスの複数のインスタンスをデプロイすることが含まれます。しかし、重要な問題が生じます。これらの分離されたインスタンスは、どのインスタンスに接続されているかに関わらず、すべての関連クライアントにメッセージをブロードキャストするために、互いにどのように通信するのでしょうか?ここで、Redis Pub/Subがプロセス間通信のための洗練された効率的なソリューションとして輝きを放ち、堅牢でスケーラブルなマルチインスタンスWebSocketアーキテクチャの構築を可能にします。
コアコンセプトの理解
実装の詳細に入る前に、議論の中心となるいくつかの基本的な概念を明確にしましょう。
- WebSocket: 単一のTCP接続を介してフルデュプレックス通信チャネルを提供する通信プロトコルです。クライアントとサーバー間のリアルタイム、双方向のデータ交換を可能にします。
- Node.js: ChromeのV8 JavaScriptエンジン上に構築されたJavaScriptランタイムです。そのイベント駆動型、ノンブロッキングI/Oモデルは、WebSocketサーバーのようなリアルタイムアプリケーションに非常に効率的です。
- Redis: データベース、キャッシュ、メッセージブローカーとして使用されるオープンソースのインメモリデータ構造ストアです。その驚異的なパフォーマンスとPub/Subを含むさまざまなデータ構造のサポートにより、高スループットのメッセージングに最適です。
- Pub/Sub (Publish/Subscribe): 送信者(発行者)がメッセージを特定の受信者(購読者)に直接送信するのではなく、どの購読者が存在するかの知識なしに、発行されたメッセージをクラスに分類するメッセージングパターンです。購読者は1つ以上のメッセージクラスに関心を示し、関心のあるメッセージのみを受信します。
- マルチインスタンスデプロイメント: ロードバランサーの後ろで同じアプリケーションのコピーを複数実行すること。これにより、クライアントリクエストをインスタンス全体に分散させることでスケーラビリティが向上し、1つのインスタンスが失敗した場合でもサービスが利用可能であることを保証することで耐障害性が向上します。
マルチインスタンスWebSocketの課題
ユーザーがチャットアプリケーションに接続されているシナリオを考えてみましょう。2つのNode.js WebSocketサーバーインスタンス、Instance A
とInstance B
があるとします。Instance A
に接続されているユーザーがメッセージを送信します。Instance B
は、それに接続されているユーザーにそれをブロードキャストできるように、このメッセージについてどのように知るのでしょうか?共有通信メカニズムがない場合、Instance A
はそのメッセージを自身の接続済みクライアントにのみ送信できます。ここでRedis Pub/Subがギャップを埋めます。
Redis Pub/SubによるWebSocketのスケーリング
コア原則は、Redisを中央メッセージバスとして使用することです。WebSocketサーバーインスタンスがクライアントからメッセージを受信すると、ローカルクライアントにのみブロードキャストするのではなく、そのメッセージを特定のRedisチャンネルに発行します。同じRedisチャンネルを購読している他のすべてのWebSocketサーバーインスタンスは、このメッセージを受信し、それぞれの接続済みクライアントにブロードキャストします。これにより、どのインスタンスに接続されているかに関わらず、すべてのメッセージがすべての関連クライアントに確実に到達します。
実装の詳細
WebSocketにはws
ライブラリ、Redisクライアントにはioredis
を使用した、Node.jsでの実践的な例でこれを説明しましょう。
まず、必要なパッケージをインストールします。
npm install ws ioredis
次に、WebSocketサービス用の簡略化されたserver.js
ファイルを作成しましょう。
// server.js const WebSocket = require('ws'); const Redis = require('ioredis'); // ローカルでRedisサーバーが実行されていることを確認するか、接続文字列を提供してください const redisPublisher = new Redis(); // デフォルトはlocalhost:6379 const redisSubscriber = new Redis(); // 購読用の別クライアント const wss = new WebSocket.Server({ port: 8080 }); console.log('WebSocket server started on port 8080'); // この特定のインスタンスに接続されているすべてのWebSocketクライアントを保持する配列 const clients = new Set(); wss.on('connection', ws => { console.log('Client connected'); clients.add(ws); // このインスタンスのクライアントからメッセージを受信したとき ws.on('message', message => { console.log(`Received message from client: ${message}`); // Redisチャンネルにメッセージを発行 redisPublisher.publish('chat_messages', message.toString()); }); ws.on('close', () => { console.log('Client disconnected'); clients.delete(ws); }); ws.on('error', error => { console.error('WebSocket error:', error); }); }); // 他のインスタンスからのメッセージのためにRedisチャンネルを購読 redisSubscriber.subscribe('chat_messages', (err, count) => { if (err) { console.error("Failed to subscribe to Redis channel:", err); } else { console.log(`Subscribed to ${count} Redis channel(s).`); } }); // Redisからメッセージを受信したとき(どのインスタンスによって発行されたか) redisSubscriber.on('message', (channel, message) => { console.log(`Received message from Redis channel "${channel}": ${message}`); // このメッセージをこの特定のインスタンスに接続されているすべてのクライアントにブロードキャスト clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(message.toString()); } }); }); // グレースフルシャットダウンの処理 process.on('SIGINT', () => { console.log('Shutting down WebSocket server...'); wss.close(() => { console.log('WebSocket server closed.'); redisPublisher.quit(); redisSubscriber.quit(); process.exit(0); }); });
これをテストするには、このサーバーの複数のインスタンスを異なるポートで実行します(例:ポート8080のnode server.js
、および別のインスタンスのためにポートを8081に変更します)。通常、これらのインスタンスの前にロードバランサーを使用しますが、基本的なテストでは直接接続で十分です。
例クライアント(HTML/JavaScript)
<!-- index.html --> <!DOCTYPE html> <html> <head> <title>WebSocket Chat</title> </head> <body> <h1>WebSocket Chat</h1> <input type="text" id="messageInput" placeholder="Type your message..."> <button onclick="sendMessage()">Send</button> <ul id="messages"></ul> <script> // 複数のWebSocketインスタンスのいずれかに接続します。例:ポート8080または8081 const ws = new WebSocket('ws://localhost:8080'); // または別のインスタンスの場合はws://localhost:8081 ws.onopen = () => { console.log('Connected to WebSocket server'); }; ws.onmessage = event => { const messagesList = document.getElementById('messages'); const listItem = document.createElement('li'); listItem.textContent = event.data; messagesList.appendChild(listItem); }; ws.onclose = () => { console.log('Disconnected from WebSocket server'); }; ws.onerror = error => { console.error('WebSocket error:', error); }; function sendMessage() { const input = document.getElementById('messageInput'); const message = input.value; if (message) { ws.send(message); input.value = ''; } } </script> </body> </html>
アプリケーションシナリオ
このアーキテクチャは、以下に役立ちます。
- リアルタイムチャットアプリケーション: サーバーインスタンス間でメッセージがすべての参加者に配信されることを保証します。
- ライブダッシュボード: サーバー接続に関係なく、すべての接続されているビューアにデータポイントを更新します。
- 共同編集: 同じドキュメントで作業している複数のユーザー間で変更を同期します。
- ゲーミング: ゲームの状態更新をすべてのプレイヤーにブロードキャストします。
結論
Node.js WebSocketサービスにRedis Pub/Subを統合することで、単一インスタンスデプロイメントの制限を効果的に克服できます。この堅牢なパターンは、水平スケーリングを可能にし、耐障害性を向上させ、分散システム全体でのシームレスなリアルタイム通信を保証します。Redisを中央メッセージブローカーとして活用することで、各WebSocketインスタンスは独立して動作しながらグローバルに通信でき、リアルタイムアプリケーションを高度にスケーラブルで回復力のあるものにします。これは、最新の高性能ウェブサービスを構築するための強力な組み合わせです。