編集: Socket.IO 1.0以降では、複数のRedisクライアントでストアを設定するのではなく、よりシンプルなRedisアダプターモジュールを使用できるようになりました。
var io = require('socket.io')(3000);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
以下に示す例は、次のようになります。
var cluster = require('cluster');
var os = require('os');
if (cluster.isMaster) {
// we create a HTTP server, but we do not use listen
// that way, we have a socket.io server that doesn't accept connections
var server = require('http').createServer();
var io = require('socket.io').listen(server);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
setInterval(function() {
// all workers will receive this in Redis, and emit
io.emit('data', 'payload');
}, 1000);
for (var i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
}
if (cluster.isWorker) {
var express = require('express');
var app = express();
var http = require('http');
var server = http.createServer(app);
var io = require('socket.io').listen(server);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
io.on('connection', function(socket) {
socket.emit('data', 'connected to worker: ' + cluster.worker.id);
});
app.listen(80);
}
他のSocket.IOプロセスに公開する必要があるが、ソケット接続自体を受け入れないマスターノードがある場合は、socket.io-redisの代わりにsocket.io-emitterを使用してください。
スケーリングに問題がある場合は、DEBUG=*
を使用してNodeアプリケーションを実行してください 。 Socket.IOは、Redisアダプターのデバッグメッセージも出力するデバッグを実装するようになりました。出力例:
socket.io:server initializing namespace / +0ms
socket.io:server creating engine.io instance with opts {"path":"/socket.io"} +2ms
socket.io:server attaching client serving req handler +2ms
socket.io-parser encoding packet {"type":2,"data":["event","payload"],"nsp":"/"} +0ms
socket.io-parser encoded {"type":2,"data":["event","payload"],"nsp":"/"} as 2["event","payload"] +1ms
socket.io-redis ignore same uid +0ms
マスタープロセスと子プロセスの両方が同じパーサーメッセージを表示する場合、アプリケーションは適切にスケーリングされています。
単一のワーカーから放出している場合は、セットアップに問題はありません。あなたがしていることは、4人のワーカーすべてから送信されており、Redisのパブリッシュ/サブスクライブにより、メッセージは複製されませんが、アプリケーションに要求したとおりに4回書き込まれます。これがRedisの機能の簡単な図です:
Client <-- Worker 1 emit --> Redis
Client <-- Worker 2 <----------|
Client <-- Worker 3 <----------|
Client <-- Worker 4 <----------|
ご覧のとおり、ワーカーから放出すると、放出がRedisに公開され、Redisデータベースにサブスクライブしている他のワーカーからミラーリングされます。これは、同じインスタンスに接続された複数のソケットサーバーを使用できることも意味し、1つのサーバーでのemitは、接続されているすべてのサーバーで起動されます。
クラスタでは、クライアントが接続すると、4つすべてではなく、4つのワーカーの1つに接続します。つまり、そのワーカーから送信したものはすべて、クライアントに1回だけ表示されます。そうです、アプリケーションはスケーリングしていますが、それを実行している方法では、4つのワーカーすべてから放出されており、Redisデータベースは、1つのワーカーで4回呼び出しているかのようにアプリケーションを作成しています。クライアントが実際に4つのソケットインスタンスすべてに接続している場合、クライアントは1秒間に4つではなく16のメッセージを受信します。
ソケット処理のタイプは、使用するアプリケーションのタイプによって異なります。クライアントを個別に処理する場合は、接続イベントが1つのクライアントごとに1つのワーカーに対してのみ発生するため、問題はありません。グローバルな「ハートビート」が必要な場合は、マスタープロセスにソケットハンドラーを含めることができます。マスタープロセスが停止するとワーカーが停止するため、マスタープロセスの接続負荷を相殺し、子に接続を処理させる必要があります。次に例を示します:
var cluster = require('cluster');
var os = require('os');
if (cluster.isMaster) {
// we create a HTTP server, but we do not use listen
// that way, we have a socket.io server that doesn't accept connections
var server = require('http').createServer();
var io = require('socket.io').listen(server);
var RedisStore = require('socket.io/lib/stores/redis');
var redis = require('socket.io/node_modules/redis');
io.set('store', new RedisStore({
redisPub: redis.createClient(),
redisSub: redis.createClient(),
redisClient: redis.createClient()
}));
setInterval(function() {
// all workers will receive this in Redis, and emit
io.sockets.emit('data', 'payload');
}, 1000);
for (var i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
}
if (cluster.isWorker) {
var express = require('express');
var app = express();
var http = require('http');
var server = http.createServer(app);
var io = require('socket.io').listen(server);
var RedisStore = require('socket.io/lib/stores/redis');
var redis = require('socket.io/node_modules/redis');
io.set('store', new RedisStore({
redisPub: redis.createClient(),
redisSub: redis.createClient(),
redisClient: redis.createClient()
}));
io.sockets.on('connection', function(socket) {
socket.emit('data', 'connected to worker: ' + cluster.worker.id);
});
app.listen(80);
}
この例では、5つのSocket.IOインスタンスがあり、1つはマスターで、4つは子です。マスターサーバーがlisten()
を呼び出すことはありません したがって、そのプロセスに接続のオーバーヘッドはありません。ただし、マスタープロセスでemitを呼び出すと、Redisに公開され、4つのワーカープロセスがクライアントでemitを実行します。これにより、ワーカーへの接続負荷が相殺され、ワーカーが停止した場合、メインのアプリケーションロジックはマスターで変更されません。
Redisを使用すると、名前空間やルーム内であっても、すべてのエミットは、他のワーカープロセスによって、そのプロセスからのエミットをトリガーしたかのように処理されることに注意してください。つまり、1つのRedisインスタンスを持つ2つのSocket.IOインスタンスがある場合、emit()
を呼び出します。 最初のワーカーのソケットでは、データがクライアントに送信されますが、ワーカー2は、そのワーカーからの送信を呼び出した場合と同じように動作します。