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

Tearing my hair out with this one … alguém conseguiu dimensionar o Socket.IO para vários processos de “trabalho” gerados pelo módulo de cluster do Node.js?

Vamos dizer que eu tenho o seguinte em quatro processos de trabalho (pseudo):

// on the server var express = require('express'); var server = express(); var socket = require('socket.io'); var io = socket.listen(server); // socket.io io.set('store', new socket.RedisStore); // set-up connections... io.sockets.on('connection', function(socket) { socket.on('join', function(rooms) { rooms.forEach(function(room) { socket.join(room); }); }); socket.on('leave', function(rooms) { rooms.forEach(function(room) { socket.leave(room); }); }); }); // Emit a message every second function send() { io.sockets.in('room').emit('data', 'howdy'); } setInterval(send, 1000); 

E no navegador …

 // on the client socket = io.connect(); socket.emit('join', ['room']); socket.on('data', function(data){ console.log(data); }); 

O problema: A cada segundo, estou recebendo quatro mensagens, devido a quatro processos de trabalho separados enviando as mensagens.

Como posso garantir que a mensagem seja enviada apenas uma vez?

    Edit: Em Socket.IO 1.0+, em vez de configurar um armazenamento com vários clientes Redis, um módulo de adaptador Redis mais simples pode ser usado agora.

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

    O exemplo mostrado abaixo ficaria mais parecido com isto:

     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 para outros processos Socket.IO, mas não aceitar conexões de soquete, use o socket.io-emitter em vez de socket.io-redis .

    Se você estiver com problemas de escalonamento, execute seus aplicativos do Node com DEBUG=* . O Socket.IO agora implementa a debugging que também imprime as mensagens de debugging do adaptador Redis. Exemplo de saída:

     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 o seu mestre e o filho processarem ambos exibirem as mesmas mensagens do analisador, o seu aplicativo será 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 funcionários, e devido à publicação / assinatura do Redis, as mensagens não são duplicadas, mas escritas quatro vezes, como você pediu ao aplicativo para fazer. Aqui está um diagrama simples do que 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 para o Redis, e será espelhado de outros trabalhadores, que se inscreveram no database Redis. Isso também significa que você pode usar vários servidores de soquete conectados à mesma instância, e um emissor em um servidor será acionado em todos os servidores conectados.

    Com o cluster, quando um cliente se conecta, ele se conectará a um de seus quatro funcionários, não a todos os quatro. Isso também significa que qualquer coisa que você emita daquele trabalhador só será mostrada uma vez para o cliente. Então, sim, o aplicativo está escalando, mas do jeito que você está fazendo, você está emitindo de todos os quatro funcionários, e o database Redis está fazendo como se você estivesse ligando quatro vezes em um único trabalhador. Se um cliente realmente se conectasse a todas as quatro instâncias de soquete, estaria recebendo 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 lidar com os clientes individualmente, não deverá ter problemas, porque o evento de conexão só será triggersdo para um trabalhador por cliente. Se você precisa de um "heartbeat" global, então você pode ter um manipulador de soquete em seu processo mestre. Como os funcionários morrem quando o processo mestre é interrompido, você deve compensar a carga de conexão do processo mestre e permitir que as crianças manipulem 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 Socket.IO, sendo uma delas a principal e quatro as crianças. O servidor mestre nunca chama listen() portanto, não há sobrecarga de conexão nesse processo. No entanto, se você chamar um emissor no processo mestre, ele será publicado no Redis e os quatro processos de trabalho executarão o emissor em seus clientes. Isso compensa a carga de conexão aos trabalhadores e, se um funcionário morrer, a lógica da sua aplicação principal não será afetada no mestre.

    Note que com Redis, todos os emits, mesmo em um namespace ou sala, serão processados ​​por outros processos de trabalho como se você tivesse acionado o emissor daquele processo. Em outras palavras, se você tiver duas instâncias Socket.IO com uma instância Redis, chamar emit() em um soquete no primeiro worker enviará os dados para seus clientes, enquanto o worker dois fará o mesmo como se chamasse o emit de esse trabalhador.

    Deixe o mestre manipular sua pulsação (exemplo abaixo) ou inicie vários processos em portas diferentes internamente e carregue-os com nginx (que também suporta websockets da V1.3 para cima).

    Cluster com Master

     // on the server var express = require('express'); var server = express(); var socket = require('socket.io'); var io = socket.listen(server); var cluster = require('cluster'); var numCPUs = require('os').cpus().length; // socket.io io.set('store', new socket.RedisStore); // set-up connections... io.sockets.on('connection', function(socket) { socket.on('join', function(rooms) { rooms.forEach(function(room) { socket.join(room); }); }); socket.on('leave', function(rooms) { rooms.forEach(function(room) { socket.leave(room); }); }); }); if (cluster.isMaster) { // Fork workers. for (var i = 0; i < numCPUs; i++) { cluster.fork(); } // Emit a message every second function send() { console.log('howdy'); io.sockets.in('room').emit('data', 'howdy'); } setInterval(send, 1000); cluster.on('exit', function(worker, code, signal) { console.log('worker ' + worker.process.pid + ' died'); }); } 

    Isso realmente parece Socket.IO sucesso na escala. Você esperaria que uma mensagem de um servidor fosse para todos os sockets naquela sala, independentemente de qual servidor eles estivessem conectados.

    Sua melhor aposta é ter um processo mestre que envie uma mensagem a cada segundo. Você pode fazer isso apenas executando-o se cluster.isMaster , por exemplo.

    A comunicação entre processos não é suficiente para fazer com que o socket.io 1.4.5 trabalhe com o cluster. Forçar o modo websocket também é uma obrigação. Veja o handshake do WebSocket em Node.JS, Socket.IO e Clusters não funcionando