javascript - tutorial - Escalar Socket.IO a múltiples procesos Node.js utilizando cluster
socket io tutorial (4)
Deje que el maestro maneje los latidos de su corazón (ejemplo a continuación) o inicie múltiples procesos en puertos diferentes internamente y equilibre la carga con nginx (que también admite websockets desde V1.3 hacia arriba).
Cluster con 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'');
});
}
Arrancándome el cabello con este ... ¿alguien ha logrado escalar Socket.IO a múltiples procesos de "trabajo" generados por el módulo de cluster de Node.js?
Digamos que tengo lo siguiente en cuatro procesos de trabajo (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);
Y en el navegador ...
// on the client
socket = io.connect();
socket.emit(''join'', [''room'']);
socket.on(''data'', function(data){
console.log(data);
});
El problema: cada segundo, recibo cuatro mensajes, debido a cuatro procesos de trabajo separados que envían los mensajes.
¿Cómo me aseguro de que el mensaje solo se envíe una vez?
Esto realmente parece que Socket.IO tiene éxito en la escala. Es de esperar que un mensaje de un servidor vaya a todos los sockets en esa habitación, independientemente del servidor al que estén conectados.
Su mejor opción es tener un proceso maestro que envíe un mensaje por segundo. Puede hacer esto ejecutando solo si cluster.isMaster
, por ejemplo.
La comunicación entre procesos no es suficiente para hacer que socket.io 1.4.5 funcione con clúster. Forzar el modo websocket también es imprescindible. Ver WebSocket handshake en Node.JS, Socket.IO y Clusters no funciona
Editar: En Socket.IO 1.0+, en lugar de configurar una tienda con múltiples clientes Redis, ahora se puede usar un módulo adaptador 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 vería más como 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 Socket.IO, pero no acepta conexiones de socket, use socket.io-emitter lugar de socket.io-redis .
Si tiene problemas para escalar, ejecute sus aplicaciones Node con DEBUG=*
. Socket.IO ahora implementa debug que también imprimirá los mensajes de depuración del adaptador Redis. Ejemplo de salida:
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 ambos procesos, el principal y el secundario, muestran los mismos mensajes del analizador, entonces la aplicación escalará 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 múltiples servidores de socket conectados a la misma instancia, y se emitirá una emisión en un servidor en todos los servidores conectados.
Con el clúster, cuando un cliente se conecta, se conectará con uno de sus cuatro trabajadores, no con los cuatro. Eso también significa que cualquier cosa que emitas de ese trabajador solo se mostrará una vez al cliente. Entonces, sí, la aplicación está escalando, pero de la forma en que lo hace, está emitiendo de los cuatro trabajadores, y la base de datos de Redis lo está haciendo como si lo llamara cuatro veces con un solo trabajador. Si un cliente realmente se conectara a las cuatro instancias de su socket, recibiría dieciséis mensajes por segundo, no cuatro.
El tipo de manejo del socket depende del tipo de aplicación que va 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 cada cliente. Si necesita un "latido" global, entonces podría tener un controlador de socket en su proceso maestro. Dado que los trabajadores mueren cuando el proceso maestro muere, debe compensar la carga de conexión del proceso maestro y dejar que los niños manejen las conexiones. Aquí hay 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 siendo el maestro y cuatro siendo los hijos. El servidor maestro nunca llama a listen()
para que no exista una sobrecarga de conexión en ese proceso. Sin embargo, si llama a emitir 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 la aplicación principal no se tocaría en el maestro.
Tenga en cuenta que con Redis, todos los emite, incluso en un espacio de nombres o sala, serán procesados por otros procesos de trabajo como si activara la emisión desde ese proceso. En otras palabras, si tiene dos instancias de Socket.IO con una instancia de Redis, al llamar a emit()
en un socket en el primer trabajador enviará los datos a sus clientes, mientras que el worker two hará lo mismo que si llamara a emit from ese trabajador