Redis
 sql >> Datenbank >  >> NoSQL >> Redis

Skalierung von Socket.IO auf mehrere Node.js-Prozesse mithilfe von Cluster

Bearbeiten: In Socket.IO 1.0+ kann jetzt ein einfacheres Redis-Adaptermodul verwendet werden, anstatt einen Speicher mit mehreren Redis-Clients einzurichten.

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

Das unten gezeigte Beispiel würde eher so aussehen:

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

Wenn Sie einen Master-Knoten haben, der für andere Socket.IO-Prozesse veröffentlichen muss, aber selbst keine Socket-Verbindungen akzeptiert, verwenden Sie socket.io-emitter anstelle von socket.io-redis.

Wenn Sie Probleme bei der Skalierung haben, führen Sie Ihre Node-Anwendungen mit DEBUG=* aus . Socket.IO implementiert jetzt Debug, das auch Redis-Adapter-Debug-Meldungen ausgibt. Beispielausgabe:

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

Wenn sowohl Ihr Master- als auch Ihr untergeordneter Prozess dieselben Parser-Meldungen anzeigen, wird Ihre Anwendung richtig skaliert.

Es sollte kein Problem mit Ihrem Setup geben, wenn Sie von einem einzelnen Arbeiter emittieren. Was Sie tun, ist das Emittieren von allen vier Workern, und aufgrund von Redis Publish/Subscribe werden die Nachrichten nicht dupliziert, sondern viermal geschrieben, wie Sie es von der Anwendung verlangt haben. Hier ist ein einfaches Diagramm dessen, was Redis tut:

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

Wie Sie sehen können, wird die Emit von einem Worker in Redis veröffentlicht und von anderen Workern gespiegelt, die die Redis-Datenbank abonniert haben. Das bedeutet auch, dass Sie mehrere Socket-Server verwenden können, die mit derselben Instanz verbunden sind, und ein Emit auf einem Server wird auf allen verbundenen Servern ausgelöst.

Wenn sich ein Client mit Cluster verbindet, stellt er eine Verbindung zu einem Ihrer vier Worker her, nicht zu allen vier. Das bedeutet auch, dass alles, was Sie von diesem Mitarbeiter ausgeben, dem Kunden nur einmal angezeigt wird. Also ja, die Anwendung skaliert, aber so, wie Sie es tun, senden Sie Daten von allen vier Workern aus, und die Redis-Datenbank macht es so, als würden Sie sie viermal für einen einzelnen Worker aufrufen. Wenn sich ein Client tatsächlich mit allen vier Ihrer Socket-Instanzen verbinden würde, würde er sechzehn Nachrichten pro Sekunde empfangen, nicht vier.

Die Art der Socket-Handhabung hängt von der Art der Anwendung ab, die Sie haben werden. Wenn Sie Clients einzeln behandeln, sollten Sie kein Problem haben, da das Verbindungsereignis nur für einen Worker pro Client ausgelöst wird. Wenn Sie einen globalen "Heartbeat" benötigen, könnten Sie einen Socket-Handler in Ihrem Master-Prozess haben. Da Worker sterben, wenn der Master-Prozess stirbt, sollten Sie die Verbindungslast des Master-Prozesses ausgleichen und die untergeordneten Verbindungen behandeln lassen. Hier ist ein Beispiel:

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

In dem Beispiel gibt es fünf Socket.IO-Instanzen, von denen eine der Master und vier die untergeordneten Instanzen sind. Der Master-Server ruft niemals listen() auf Es gibt also keinen Verbindungsaufwand für diesen Prozess. Wenn Sie jedoch eine Emit für den Masterprozess aufrufen, wird sie in Redis veröffentlicht, und die vier Worker-Prozesse führen die Emit für ihre Clients aus. Dadurch wird die Verbindungslast auf die Worker ausgeglichen, und wenn ein Worker ausfällt, bleibt Ihre Hauptanwendungslogik im Master unberührt.

Beachten Sie, dass mit Redis alle Emits, sogar in einem Namespace oder Raum, von anderen Arbeitsprozessen verarbeitet werden, als ob Sie die Emit von diesem Prozess ausgelöst hätten. Mit anderen Worten, wenn Sie zwei Socket.IO-Instanzen mit einer Redis-Instanz haben, rufen Sie emit() auf auf einem Socket im ersten Worker sendet die Daten an seine Clients, während Worker 2 dasselbe tut, als ob Sie die Ausgabe von diesem Worker aufgerufen hätten.