2013-08-19 4 views
47

Разрывание моих волос с помощью этого ... кому-нибудь удалось масштабировать Socket.IO до нескольких «рабочих» процессов, порожденных модулем cluster от Node.js?Масштабирование Socket.IO для нескольких процессов Node.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); 
}); 

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

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

+0

Какую версию socket.io вы используете? Socket.IO 0.6 разработан как один сервер процессов. Обратите внимание на ответ 3rdEden в этой статье stackoverflow. http://stackoverflow.com/questions/5944714/how-can-i-scale-socket-io – HariKrishnan

+3

0.9.16 с помощью RedisStore –

+0

Вы можете использовать SocketCluster (интерфейс сокета совместим с Socket.io): https: // github.com/topcloud/socketcluster – Jon

ответ

70

Edit: В 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-emitter вместо socket.io-redis.

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

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

Как вы можете видеть, когда вы испускаете от работника, он будет издавать Emit к 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(), поэтому на этот процесс нет никаких дополнительных затрат на соединение. Однако, если вы вызываете emit в основном процессе, он будет опубликован в Redis, и четыре рабочих процесса будут выполнять emit на своих клиентах. Это компенсирует нагрузку на соединение для рабочих, и если работник должен умереть, ваша основная логика приложения будет не затронута в мастер.

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

+2

Отличный ответ, очень полезно, спасибо! –

+0

Хороший ответ. Благодаря! работал в некоторой степени. Когда я испускаю io.sockets.emit ('userstreamssock', postid); от мастера, я не получаю это в рабочих. Не знаю, почему. –

+4

Только для информации: он больше не работает с socket.io> 1.0. Необходимо использовать адаптер redis. http://socket.io/docs/using-multiple-nodes/ Мне еще не удалось запустить пример с кластером и socket.io 1.1.0. – DerM

1

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

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

+0

Успешно «разделять» сокеты, но не удается выяснить, какие сообщения не дублировать. Кластер - отличная идея, но тогда это не действительно «масштабирование» ... это один из процессов, управляющих работой 4 –

+0

@Lee. Какую логику вы ожидаете использовать для принятия решения о «дублировании» сообщений? Когда вы отправляете сообщение в комнату, все идет в комнату - это ожидаемое поведение. У вас может быть место для каждого процесса, если вы хотите, чтобы каждый отправлял сообщения с интервалом. –

+0

Я предполагаю, что лучшая логика будет для socket.emit как-то синхронизироваться между процессами. Не знаете, как это достичь. Подход «одна комната к процессу» не позволяет решить проблему масштабируемости, когда у него 10 разных серверов с 4 ядрами каждый ... но это может быть хорошей идеей, когда задействован только один сервер. –

2

Дайте мастеру обработать ваше сердцебиение (пример ниже) или запустите несколько процессов на разных портах внутри и загрузите их с помощью nginx (который поддерживает также веб-порты от 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'); 
    }); 
} 
+0

Неплохое предложение, но тогда это еще один мастер-процесс, ответственный за потенциально 500 000 подключений к сети ... на самом деле не справляется с проблемой «масштабируемости» на нескольких серверах/процессах на сервере –

+0

Как насчет этого: Используйте 2 слоя балансировщики нагрузки. Пример AWS: Первый слой распределяет рабочую нагрузку между несколькими машинами с помощью эластичной балансировки нагрузки. Второй уровень распределяет рабочую нагрузку между несколькими экземплярами на машине. Вы можете запускать экземпляры узла cpu.count и распределять рабочую нагрузку с них через nginx или использовать кластер узлов (в этом случае нет необходимости в nginx). Я бы предпочел версию nginx. Для автоматического масштабирования используйте OpsWork и дайте ему обработать масштабирование, основанное на загрузке процессора. Он автоматически добавит и удалит машины и будет легко настроен. –