Redis
 sql >> Database >  >> NoSQL >> Redis

Ridimensionamento di Socket.IO su più processi Node.js usando il cluster

Modifica: In Socket.IO 1.0+, invece di impostare un negozio con più client Redis, ora è possibile utilizzare un modulo adattatore Redis più semplice.

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

L'esempio mostrato di seguito sarebbe più simile a questo:

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

Se hai un nodo master che deve pubblicare su altri processi Socket.IO, ma non accetta connessioni socket, usa socket.io-emitter invece di socket.io-redis.

Se riscontri problemi di ridimensionamento, esegui le tue applicazioni Node con DEBUG=* . Socket.IO ora implementa il debug che stamperà anche i messaggi di debug dell'adattatore Redis. Esempio di output:

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

Se entrambi i processi master e figlio visualizzano gli stessi messaggi del parser, significa che l'applicazione viene ridimensionata correttamente.

Non dovrebbero esserci problemi con la tua configurazione se stai emettendo da un singolo lavoratore. Quello che stai facendo è emettere da tutti e quattro i lavoratori e, a causa della pubblicazione/sottoscrizione di Redis, i messaggi non vengono duplicati, ma scritti quattro volte, come hai chiesto all'applicazione di fare. Ecco un semplice diagramma di cosa fa Redis:

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

Come puoi vedere, quando emetti da un lavoratore, questo pubblicherà l'emissione su Redis e verrà rispecchiato da altri lavoratori, che si sono iscritti al database Redis. Ciò significa anche che puoi utilizzare più server socket collegati alla stessa istanza e un'emissione su un server verrà attivata su tutti i server collegati.

Con il cluster, quando un client si connette, si connetterà a uno dei tuoi quattro lavoratori, non a tutti e quattro. Ciò significa anche che tutto ciò che emetti da quel lavoratore verrà mostrato solo una volta al cliente. Quindi sì, l'applicazione è in scala, ma il modo in cui lo stai facendo, stai emettendo da tutti e quattro i lavoratori e il database Redis sta facendo in modo che tu lo chiamassi quattro volte su un singolo lavoratore. Se un client si connettesse effettivamente a tutte e quattro le tue istanze socket, riceverebbe sedici messaggi al secondo, non quattro.

Il tipo di gestione dei socket dipende dal tipo di applicazione che avrai. Se hai intenzione di gestire i client individualmente, non dovresti avere problemi, perché l'evento di connessione verrà attivato solo per un lavoratore per un client. Se hai bisogno di un "heartbeat" globale, potresti avere un gestore di socket nel tuo processo master. Poiché i lavoratori muoiono quando il processo principale si interrompe, è necessario compensare il carico di connessione dal processo principale e lasciare che i figli gestiscano le connessioni. Ecco un esempio:

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

Nell'esempio, ci sono cinque istanze Socket.IO, una è il master e quattro sono i figli. Il server principale non chiama mai listen() quindi non vi è alcun sovraccarico di connessione su quel processo. Tuttavia, se si chiama un'emissione sul processo master, verrà pubblicata su Redis e i quattro processi di lavoro eseguiranno l'emissione sui propri client. Questo compensa il carico di connessione per i lavoratori e, se un lavoratore dovesse morire, la logica dell'applicazione principale non sarebbe stata modificata nel master.

Tieni presente che con Redis, tutte le emissioni, anche in uno spazio dei nomi o in una stanza, verranno elaborate da altri processi di lavoro come se avessi attivato l'emissione da quel processo. In altre parole, se hai due istanze Socket.IO con un'istanza Redis, chiamando emit() su un socket il primo lavoratore invierà i dati ai suoi clienti, mentre il lavoratore due farà lo stesso come se tu chiamassi l'emit da quel lavoratore.