Масштабирование Socket.IO к нескольким узлам.процессы js с использованием кластера


вырывая мои волосы с этим... кто-нибудь сумел масштабировать Socket.IO для нескольких" рабочих " процессов, порожденных узлом.в JS это кластер модуль?

допустим у меня есть следующие на четыре рабочие процессы (псевдо):

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

и в браузере...

// on the client
socket = io.connect();
socket.emit('join', ['room']);

socket.on('data', function(data){
  console.log(data);
});

проблема: каждую секунду, я получаю четыре сообщения, из-за четырех отдельных рабочих процессов отправки сообщение.

Как убедиться, что сообщение отправляется только один раз?

4 54

4 ответа:

Edit: In Socket.IO 1.0+, вместо установки хранилища с несколькими клиентами Redis, теперь можно использовать более простой модуль адаптера Redis.

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

пример, показанный ниже, будет выглядеть примерно так:

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

если у вас есть главный узел, который необходимо опубликовать в другой Socket.IO процессы, но не принимает сам сокет соединения, используйте socket. io-эмиттер вместо socket. io-redis.

если вы возникли проблемы с масштабированием, запустите приложения узла с DEBUG=*. Socket.IO теперь реализует debug который также распечатает сообщения отладки адаптера Redis. Пример вывода:

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

если оба ваших основных и дочерних процесса отображают одни и те же сообщения синтаксического анализатора, то ваше приложение правильно масштабируется.


не должно быть проблем с настройкой, если вы излучаете от одного работника. То, что вы делаете, излучает все четыре работника, и из-за Redis publish/subscribe сообщения не дублируются, а записываются четыре раза, как вы просили приложение сделать. Вот простая схема того, что делает Redis:

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

как вы можете видеть, когда вы излучаете от работника, он опубликует излучение в Redis, и он будет зеркально отражен от других работников, которые подписались на базу данных Redis. Это также означает, что вы можете использовать несколько серверов сокетов, подключенных к одному экземпляру, и эмиссия на одном сервере будет запускаться на всех подключенных серверах.

С кластером, когда клиент подключается, он будет подключаться к одному из ваших четырех работников, а не ко всем четырем. Это также означает, что все, что вы излучаете от этого работника, будет показано клиенту только один раз. Так что да, приложение масштабируется, но то, как вы это делаете, вы излучаете от всех четырех работников, и база данных Redis делает это так, как если бы вы вызывали ее четыре раза на одном работнике. Если клиент действительно подключен ко всем четырем вашим сокетам например, они будут получать шестнадцать сообщений в секунду, а не четыре.

тип обработки сокета зависит от типа приложения вы будете иметь. Если вы собираетесь обрабатывать клиентов индивидуально, то у вас не должно быть проблем, потому что событие подключения будет срабатывать только для одного работника на одного клиента. Если вам нужен глобальный "пульс", то вы могли бы иметь обработчик сокетов в вашем главном процессе. Поскольку рабочие умирают, когда умирает главный процесс, вы должны компенсировать загрузка соединения от главного процесса, и пусть дети обрабатывают соединения. Вот пример:

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

в примере есть пять Socket.IO примеры: один-мастер, а четверо-дети. Главный сервер никогда не вызывает listen() таким образом, нет никаких накладных расходов на этот процесс. Однако если вы вызываете эмиссию в главном процессе, она будет опубликована в Redis, и четыре рабочих процесса выполнят эмиссию на своих клиентах. Это компенсирует загрузка соединения с рабочими, и если рабочий должен был умереть, ваша основная логика приложения была бы нетронута в Мастере.

обратите внимание, что с Redis все эмиссии, даже в пространстве имен или комнате, будут обрабатываться другими рабочими процессами, как если бы вы запустили эмиссию из этого процесса. Другими словами, если у вас есть два Socket.IO экземпляры с одним экземпляром Redis, вызывая emit() на сокете в первом работнике будет отправлять данные своим клиентам, в то время как рабочий два будет делать то же самое, как если бы вы вызвал эмиссию от этого работника.

пусть мастер обрабатывает ваше сердцебиение (пример ниже) или запускает несколько процессов на разных портах внутри и балансирует их с помощью nginx (который также поддерживает websockets от V1.3 вверх).

кластер с мастером

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

это на самом деле выглядит так Socket.IO преуспевая в масштабировании. Вы ожидаете, что сообщение от одного сервера будет отправлено во все сокеты в этой комнате, независимо от того, к какому серверу они подключены.

лучше всего иметь один главный процесс, который отправляет сообщение каждую секунду. Вы можете сделать это, только запустив его, если cluster.isMaster, например.

межпроцессное общение недостаточно, чтобы сделать socket.io 1.4.5 работа с кластером. Принудительный режим websocket также является обязательным. Смотрите WebSocket рукопожатие в узле.JS, Socket.IO и кластеры не работают