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

разрывая мои волосы с этим... кто-нибудь сумел масштабировать гнездо.ИО к нескольким" рабочим " процессам, порожденным узлом.в 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 ответов


Edit: В Гнезде.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);
}

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

если вы возникли проблемы с масштабированием, запустите приложения узла с DEBUG=*. Розетка.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 сообщения не дублируются, а записываются четыре раза, как вы просили приложение. Вот простая диаграмма того, что делает Redis:

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

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

С кластером, когда клиент подключается, он подключится к одному из ваших четырех работников, а не ко всем четырем. Это также означает, что все, что вы излучаете от этого работника, будет показано клиенту только один раз. Так что да, приложение масштабируется, но так, как вы это делаете, вы излучаете от всех четырех работников, и база данных 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);
}

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

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


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

кластер с 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');
    }); 
}

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

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


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