sql >> Base de Datos >  >> NoSQL >> Redis

Escalando Socket.IO a múltiples procesos de Node.js usando un clúster

Editar: En Socket.IO 1.0+, en lugar de configurar una tienda con varios clientes de Redis, ahora se puede usar un módulo adaptador de Redis más simple.

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

El ejemplo que se muestra a continuación se parecería más a esto:

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 tiene un nodo maestro que necesita publicar en otros procesos de Socket.IO, pero no acepta conexiones de socket, use socket.io-emitter en lugar de socket.io-redis.

Si tiene problemas para escalar, ejecute sus aplicaciones Node con DEBUG=* . Socket.IO ahora implementa la depuración que también imprimirá los mensajes de depuración del adaptador Redis. Salida de ejemplo:

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 tanto el proceso maestro como el secundario muestran los mismos mensajes del analizador, entonces su aplicación se está escalando correctamente.

No debería haber un problema con su configuración si está emitiendo desde un solo trabajador. Lo que está haciendo es emitir desde los cuatro trabajadores y, debido a la publicación/suscripción de Redis, los mensajes no se duplican, sino que se escriben cuatro veces, como le pidió a la aplicación. Aquí hay un diagrama simple de lo que hace Redis:

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

Como puede ver, cuando emite desde un trabajador, publicará la emisión en Redis y se reflejará desde otros trabajadores, que se han suscrito a la base de datos de Redis. Esto también significa que puede usar varios servidores de socket conectados a la misma instancia, y una emisión en un servidor se activará en todos los servidores conectados.

Con el clúster, cuando un cliente se conecta, se conectará a uno de sus cuatro trabajadores, no a los cuatro. Eso también significa que cualquier cosa que emita de ese trabajador solo se mostrará una vez al cliente. Entonces, sí, la aplicación se está escalando, pero de la forma en que lo hace, está emitiendo desde los cuatro trabajadores, y la base de datos de Redis lo hace como si estuviera llamando cuatro veces a un solo trabajador. Si un cliente realmente se conectara a las cuatro instancias de socket, recibiría dieciséis mensajes por segundo, no cuatro.

El tipo de manejo de socket depende del tipo de aplicación que vaya a tener. Si va a manejar clientes individualmente, entonces no debería tener ningún problema, porque el evento de conexión solo se activará para un trabajador por cliente. Si necesita un "latido" global, entonces podría tener un controlador de socket en su proceso maestro. Dado que los trabajadores mueren cuando muere el proceso maestro, debe compensar la carga de conexión del proceso maestro y dejar que los niños manejen las conexiones. He aquí un ejemplo:

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

En el ejemplo, hay cinco instancias de Socket.IO, una es la maestra y cuatro son las secundarias. El servidor maestro nunca llama a listen() por lo que no hay sobrecarga de conexión en ese proceso. Sin embargo, si llama a una emisión en el proceso maestro, se publicará en Redis y los cuatro procesos de trabajo realizarán la emisión en sus clientes. Esto compensa la carga de conexión a los trabajadores, y si un trabajador muriera, la lógica de su aplicación principal no se modificaría en el maestro.

Tenga en cuenta que con Redis, todas las emisiones, incluso en un espacio de nombres o sala, serán procesadas por otros procesos de trabajo como si hubiera activado la emisión desde ese proceso. En otras palabras, si tiene dos instancias de Socket.IO con una instancia de Redis, llame a emit() en un socket en el primer trabajador enviará los datos a sus clientes, mientras que el trabajador dos hará lo mismo como si llamara a la emisión desde ese trabajador.