web-dev-qa-db-fra.com

Mise à l'échelle de Socket.IO vers plusieurs processus Node.js à l'aide d'un cluster

Arracher mes cheveux avec celui-ci ... quelqu'un a-t-il réussi à adapter Socket.IO à plusieurs processus "de travail" engendrés par le module cluster de Node.js?

Disons que j'ai les éléments suivants sur quatre processus de travail (pseudo):

// 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);

Et sur le navigateur ...

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

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

Le problème: Chaque seconde, je reçois quatre messages, dus à quatre processus de travail distincts envoyant les messages.

Comment m'assurer que le message n'est envoyé qu'une seule fois?

60
Lee Benson

Modifier: Dans Socket.IO 1.0+, plutôt que de définir un magasin avec plusieurs clients Redis, un module adaptateur Redis plus simple peut maintenant être utilisé.

var io = require('socket.io')(3000);
var redis = require('socket.io-redis');
io.adapter(redis({ Host: 'localhost', port: 6379 }));

L'exemple ci-dessous ressemblerait davantage à ceci:

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);
}

Si vous avez un nœud maître qui doit publier sur d'autres processus Socket.IO, mais n'accepte pas les connexions socket lui-même, utilisez socket.io-emitter au lieu de socket.io-redis .

Si vous rencontrez des problèmes de mise à l'échelle, exécutez vos applications Node avec DEBUG=*. Socket.IO implémente désormais debug qui imprimera également les messages de débogage de l'adaptateur Redis Exemple de sortie:

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

Si vos processus maître et enfant affichent tous les deux les mêmes messages d'analyseur, votre application évolue correctement.


Il ne devrait pas y avoir de problème avec votre configuration si vous émettez à partir d'un seul travailleur. Ce que vous faites émet des quatre employés, et en raison de la publication/abonnement de Redis, les messages ne sont pas dupliqués, mais écrits quatre fois, comme vous l'avez demandé à l'application. Voici un schéma simple de ce que fait Redis:

Client  <--  Worker 1 emit -->  Redis
Client  <--  Worker 2  <----------|
Client  <--  Worker 3  <----------|
Client  <--  Worker 4  <----------|

Comme vous pouvez le voir, lorsque vous émettez à partir d'un travailleur, il publiera l'émission sur Redis, et elle sera reflétée par d'autres travailleurs, qui se sont abonnés à la base de données Redis. Cela signifie également que vous pouvez utiliser plusieurs serveurs socket connectés à la même instance et qu'une émission sur un serveur sera déclenchée sur tous les serveurs connectés.

Avec le cluster, lorsqu'un client se connecte, il se connecte à l'un de vos quatre employés, pas aux quatre. Cela signifie également que tout ce que vous émettez de ce travailleur ne sera montré qu'une seule fois au client. Alors oui, l'application évolue, mais la façon dont vous le faites, vous émettez des quatre employés, et la base de données Redis fait comme si vous l'appeliez quatre fois sur un seul employé. Si un client se connectait réellement à vos quatre instances de socket, il recevrait seize messages par seconde, pas quatre.

Le type de gestion des sockets dépend du type d'application que vous allez avoir. Si vous allez gérer les clients individuellement, vous ne devriez avoir aucun problème, car l'événement de connexion ne se déclenchera que pour un travailleur par client. Si vous avez besoin d'un "battement de coeur" global, alors vous pourriez avoir un gestionnaire de socket dans votre processus maître. Étant donné que les travailleurs meurent à la fin du processus maître, vous devez compenser la charge de connexion du processus maître et laisser les enfants gérer les connexions. Voici un exemple:

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);
}

Dans l'exemple, il existe cinq instances Socket.IO, l'une étant le maître et quatre les enfants. Le serveur maître n'appelle jamais listen() donc il n'y a pas de surcharge de connexion sur ce processus. Cependant, si vous appelez une émission sur le processus maître, elle sera publiée dans Redis et les quatre processus de travail effectueront l'émission sur leurs clients. Cela compense la charge de connexion pour les travailleurs, et si un travailleur venait à mourir, votre logique d'application principale serait intacte dans le maître.

Notez qu'avec Redis, toutes les émissions, même dans un espace de noms ou une salle, seront traitées par d'autres processus de travail comme si vous aviez déclenché l'émission à partir de ce processus. En d'autres termes, si vous avez deux instances Socket.IO avec une instance Redis, l'appel de emit() sur une socket du premier travailleur enverra les données à ses clients, tandis que le travailleur deux fera la même chose que si vous appelé l'émission de ce travailleur.

94
hexacyanide

Laissez le maître gérer votre rythme cardiaque (exemple ci-dessous) ou démarrez plusieurs processus sur différents ports en interne et équilibrez la charge avec nginx (qui prend également en charge les Websockets à partir de la V1.3).

Cluster avec Master

// 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

Cela ressemble en fait à Socket.IO réussissant à évoluer. Vous vous attendriez à ce qu'un message provenant d'un serveur soit envoyé à toutes les sockets de cette pièce, quel que soit le serveur auquel il se trouve connecté.

Votre meilleur pari est d'avoir un processus maître qui envoie un message chaque seconde. Vous pouvez le faire en l'exécutant uniquement si cluster.isMaster, par exemple.

1
Aaron Dufour

La communication interprocessus ne suffit pas pour que socket.io 1.4.5 fonctionne avec le cluster. Forcer le mode websocket est également un must. Voir poignée de main WebSocket dans Node.JS, Socket.IO et les clusters ne fonctionnent pas

0
gdorbes