Redis
 sql >> Base de Dados >  >> NoSQL >> Redis

Escalando Socket.IO para vários processos Node.js usando cluster


Editar: No Socket.IO 1.0+, em vez de configurar um armazenamento com vários clientes Redis, um módulo adaptador Redis mais simples agora pode ser usado.
var io = require('socket.io')(3000);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));

O exemplo mostrado abaixo seria mais parecido com isso:
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 você tiver um nó mestre que precisa publicar em outros processos Socket.IO, mas não aceitar conexões de soquete, use socket.io-emitter em vez de socket.io-redis.

Se você estiver tendo problemas para dimensionar, execute seus aplicativos Node com DEBUG=* . O Socket.IO agora implementa o debug, que também imprimirá as mensagens de depuração do adaptador Redis. Saída de exemplo:
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 os processos mestre e filho exibirem as mesmas mensagens do analisador, seu aplicativo está sendo dimensionado corretamente.

Não deve haver um problema com sua configuração se você estiver emitindo de um único trabalhador. O que você está fazendo está emitindo de todos os quatro trabalhadores e, devido à publicação/assinatura do Redis, as mensagens não são duplicadas, mas escritas quatro vezes, conforme você solicitou ao aplicativo. Aqui está um diagrama simples do que o Redis faz:
Client  <--  Worker 1 emit -->  Redis
Client  <--  Worker 2  <----------|
Client  <--  Worker 3  <----------|
Client  <--  Worker 4  <----------|

Como você pode ver, quando você emite de um trabalhador, ele publicará a emissão no Redis e será espelhado de outros trabalhadores que se inscreveram no banco de dados Redis. Isso também significa que você pode usar vários servidores de soquete conectados à mesma instância e uma emissão em um servidor será acionada em todos os servidores conectados.

Com o cluster, quando um cliente se conecta, ele se conecta a um de seus quatro trabalhadores, não a todos os quatro. Isso também significa que qualquer coisa que você emitir desse trabalhador será mostrada apenas uma vez ao cliente. Então, sim, o aplicativo está sendo dimensionado, mas do jeito que você está fazendo isso, você está emitindo de todos os quatro trabalhadores, e o banco de dados Redis está fazendo isso como se você estivesse chamando quatro vezes em um único trabalhador. Se um cliente realmente se conectar a todas as quatro instâncias de soquete, ele receberá dezesseis mensagens por segundo, não quatro.

O tipo de manipulação de soquete depende do tipo de aplicativo que você terá. Se você for manipular clientes individualmente, não deverá ter problemas, porque o evento de conexão será acionado apenas para um trabalhador por cliente. Se você precisar de um "heartbeat" global, poderá ter um manipulador de soquete em seu processo mestre. Como os trabalhadores morrem quando o processo mestre morre, você deve compensar a carga da conexão do processo mestre e deixar os filhos manipularem as conexões. Aqui está um exemplo:
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);
}

No exemplo, há cinco instâncias de Socket.IO, sendo uma a master e quatro as filhas. O servidor mestre nunca chama listen() portanto, não há sobrecarga de conexão nesse processo. No entanto, se você chamar uma emissão no processo mestre, ela será publicada no Redis e os quatro processos de trabalho executarão a emissão em seus clientes. Isso compensa a carga de conexão para os trabalhadores e, se um trabalhador morresse, a lógica do aplicativo principal permaneceria intocada no mestre.

Observe que, com o Redis, todas as emissões, mesmo em um namespace ou sala, serão processadas por outros processos de trabalho como se você tivesse acionado a emissão desse processo. Em outras palavras, se você tiver duas instâncias do Socket.IO com uma instância do Redis, chamando emit() em um soquete no primeiro trabalhador enviará os dados para seus clientes, enquanto o trabalhador dois fará o mesmo que se você chamasse a emissão desse trabalhador.