Redis
 sql >> Base de données >  >> NoSQL >> Redis

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

Modifier : Dans Socket.IO 1.0+, plutôt que de configurer un magasin avec plusieurs clients Redis, un module adaptateur Redis plus simple peut désormais ê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 plutôt à 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 le débogage 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 est émis par les quatre travailleurs, et grâce à la publication/abonnement 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 il sera mis en miroir à partir 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 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 travailleurs, 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 travailleurs, et la base de données Redis fait comme si vous l'appeliez quatre fois sur un seul travailleur. Si un client se connectait réellement à vos quatre instances de socket, il recevrait seize messages par seconde, et non 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 pas avoir de 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, vous pouvez avoir un gestionnaire de socket dans votre processus maître. Étant donné que les travailleurs meurent lorsque le processus maître meurt, 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 y a cinq instances Socket.IO, une étant le maître et quatre étant les enfants. Le serveur maître n'appelle jamais listen() il n'y a donc pas de surcharge de connexion sur ce processus. Cependant, si vous appelez une émission sur le processus maître, elle sera publiée sur Redis et les quatre processus de travail effectueront l'émission sur leurs clients. Cela compense la charge de connexion des travailleurs, et si un travailleur venait à mourir, la logique de votre application principale resterait 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, appelez emit() sur un socket dans le premier worker enverra les données à ses clients, tandis que le worker deux fera la même chose que si vous appeliez l'émission depuis ce worker.