web-dev-qa-db-ja.com

クラスターを使用してSocket.IOを複数のNode.jsプロセスにスケーリングする

これで髪を引き裂く... Node.jsの cluster モジュールによって生成された複数の「ワーカー」プロセスに Socket.IO を拡張できた人はいますか?

たとえば、fourワーカープロセス(疑似)について次のようにします。

// on the server
var express = require('express');
var server = express();
var socket = require('socket.io');
var io = socket.listen(server);

// socket.io
io.set('store', new socket.RedisStore);

// set-up connections...
io.sockets.on('connection', function(socket) {

  socket.on('join', function(rooms) {
    rooms.forEach(function(room) {
      socket.join(room);
    });
  });

  socket.on('leave', function(rooms) {
    rooms.forEach(function(room) {
      socket.leave(room);
    });
  });

});

// Emit a message every second
function send() {
  io.sockets.in('room').emit('data', 'howdy');
}

setInterval(send, 1000);

そしてブラウザ上で...

// on the client
socket = io.connect();
socket.emit('join', ['room']);

socket.on('data', function(data){
  console.log(data);
});

問題:毎秒、fourメッセージを受信して​​いますメッセージを送信する4つの個別のワーカープロセスに。

メッセージが一度だけ送信されるようにするにはどうすればよいですか?

60
Lee Benson

編集: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 を使用します。

スケーリングに問題がある場合は、Nodeアプリケーションを_DEBUG=*_で実行してください。Socket.IOは debug を実装し、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つのサーバーでの発行は、接続されているすべてのサーバーで実行されます。

クラスターでは、クライアントが接続すると、4人すべてではなく、4人のワーカーのいずれかに接続します。また、そのワーカーから放出するものはすべてクライアントに一度だけ表示されることを意味します。そのため、はい、アプリケーションはスケーリングされますが、その方法で、4つのワーカーすべてから放出し、Redisデータベースは、1つのワーカーで4回呼び出しているかのように作成します。クライアントが実際に4つすべてのソケットインスタンスに接続している場合、4つではなく1秒あたり16のメッセージを受信します。

ソケット処理のタイプは、使用するアプリケーションのタイプによって異なります。クライアントを個別に処理する場合、接続イベントはクライアントごとに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()を呼び出さないため、そのプロセスでの接続オーバーヘッドはありません。ただし、マスタープロセスで発行を呼び出すと、Redisに発行され、4つのワーカープロセスがクライアントで発行を実行します。これにより、ワーカーへの接続負荷が相殺され、ワー​​カーが死亡した場合、メインアプリケーションのロジックはマスターで変更されません。

Redisを使用すると、名前空間またはルーム内であっても、他のワーカープロセスによって、そのプロセスからの発行をトリガーしたかのように、すべての発行が処理されます。つまり、1つのRedisインスタンスを持つ2つのSocket.IOインスタンスがある場合、最初のワーカーのソケットでemit()を呼び出すと、クライアントにデータが送信されますが、ワーカー2は、そのワーカーからの放出と呼ばれます。

94
hexacyanide

マスターがハートビートを処理できるようにする(下の例を参照)か、異なるポートで複数のプロセスを内部で開始し、nginx(V1.3以降のwebsocketもサポートします)でそれらを負荷分散します。

クラスターとマスター

// on the server
var express = require('express');
var server = express();
var socket = require('socket.io');
var io = socket.listen(server);
var cluster = require('cluster');
var numCPUs = require('os').cpus().length;

// socket.io
io.set('store', new socket.RedisStore);

// set-up connections...
io.sockets.on('connection', function(socket) {
    socket.on('join', function(rooms) {
        rooms.forEach(function(room) {
            socket.join(room);
        });
    });

    socket.on('leave', function(rooms) {
        rooms.forEach(function(room) {
            socket.leave(room);
        });
    });

});

if (cluster.isMaster) {
    // Fork workers.
    for (var i = 0; i < numCPUs; i++) {
        cluster.fork();
    }

    // Emit a message every second
    function send() {
        console.log('howdy');
        io.sockets.in('room').emit('data', 'howdy');
    }

    setInterval(send, 1000);


    cluster.on('exit', function(worker, code, signal) {
        console.log('worker ' + worker.process.pid + ' died');
    }); 
}
2
Taner Topal

これは、実際にはスケーリングに成功したSocket.IOのように見えます。接続先のサーバーに関係なく、あるサーバーからのメッセージがその部屋のすべてのソケットに送られることを期待します。

最善の策は、毎秒メッセージを送信するマスタープロセスを1つ用意することです。これを行うには、cluster.isMaster、 例えば。

1
Aaron Dufour

プロセス間通信では、socket.io 1.4.5をクラスターで使用するには不十分です。 Websocketモードを強制することも必須です。 Node.JS、Socket.IO、および動作していないクラスターでのWebSocketハンドシェイク を参照してください

0
gdorbes