Сразу код.
В socket.io >= 1.0 проблема горизонтального масштабирования решается с помощью «адаптеров». Адаптеры реализуют интерфейс по управлению комнатами, рассылке сообщений всем клиентам и получение списка клиентов. По-умолчанию используется адаптер, хранящий все данные в памяти, а для горизонтального масштабирования нам понадобится хранение данных во внешней системе. Достаточно популярным является socket.io-redis, его и будем использовать.
В теории вся система будет выглядеть так:
* запущено несколько экземпляров нашего приложения с socket.io сервером;
* каждый из экземпляров связан с другим через Redis с помощью socket.io-redis;
* перед экземплярами стоит балансировщик, который распределяет клиентов между всеми серверами.
Для начала установим и настроим балансировщик. Я буду использовать HAProxy только потому, что он умеет делать балансировку по query-параметрам бесплатно (у NGINX такой функционал есть только в платной версии). А балансировка по query-параметрам мне нужна только потому что я тестировал все на одном компьютере. В реально жизни, скорее всего, придется использовать балансировку по IP-адресам клиентов.
Устанавливаем HAProxy:
sudo apt-get install haproxy
Конфиг:
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
contimeout 5000
clitimeout 50000
srvtimeout 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
listen webfarm 0.0.0.0:3000
mode http
stats enable
stats uri /haproxy?stats
balance url_param userId
option httpclose
option forwardfor
server webserver01 127.0.0.1:3001 cookie server1 check
server webserver02 127.0.0.1:3002 cookie server2 check
Здесь идут настройки по-умолчанию и в последнем блоке настройки нашего приложения. Читаются они примерно так:
listen webfarm 0.0.0.0:3000 — слушать на всех интерфейсах и назвать приложение «webfarm».
mode http — это http-приложение (в смысле ожидать http-трафик).
stats enable и stats uri /haproxy?stats — включить статистику и выводить ее по этому адресу.
balance url_param userId — использовать балансировку по query-параметру userId (например, ?userId=152).
server webserver01 127.0.0.1:3001 cookie server1 check — объявление сервера для балансировки, который слушает по адресу 127.0.0.1:3001 и называется webserver01. Также устанавливать клиентам cookie server1 и включить проверку сервера на живучесть.
Теперь нужно запустить несколько экземпляров сервера. Я буду использовать подход двух файлов: master/worker.
Master очень простой:
var child_process = require('child_process'),
fork = child_process.fork,
workers = [],
workersNum = 2;
for(var i = 0; i < workersNum; i++) {
workers[i] = fork('socket.io-server-worker.js', [], {
env: {
PORT: 3000 + i + 1,
DEBUG: 'socket.io:*,engine'
}
});
}
Здесь запускается 2 экземпляра worker и в каждый пробрасываются переменные окружения PORT и DEBUG.
Код worker:
var express = require('express'),
sio_redis = require('socket.io-redis'),
app = new express(),
server = app.listen(process.env.PORT || 3001),
io = require('socket.io')(server);
app.use(express.static(__dirname));
app.get('/', function(req, res) {
res.sendFile('./index.html', {root: __dirname});
});
io.adapter(sio_redis({ host: 'localhost', port: 6379 }));
io.on('connection', function(socket) {
console.log(socket.id, 'connected to', process.pid);
// io.emit('newSocket', socket.id);
socket.join('common');
socket.on('message', function(data) {
console.log('message', data);
io.to('common').emit('message', data);
});
socket.on('privateMessage', function(data) {
console.log('privateMessage', data);
io.to('/#' + data.to).emit('privateMessage', data);
});
socket.on('broadcast', function(data) {
console.log('broadcast', data);
io.emit('broadcast', data);
});
socket.on('disconnect', function(reason) {
console.log(socket.id, 'disconnected with reason:', reason);
});
});
Здесь запускается HTTP-сервер, слушающий на порту, полученным от master-процесса. Этот же сервер отдает статическую страницу index.html и на него же вешается слушатель от socket.io. На 12 строке видно как устанавливается Redis-адаптер, а ниже идут обычные обработчики событий.
В index.html подключается socket.io-client и скрипт main.js:
var sockets = [],
socketsNum = 2;
for(var i = 0; i < socketsNum; i++) {
(function(i) {
setTimeout(function() {
sockets[i] = io.connect('http://' + document.location.host + '/?userId=' + i, {
'force new connection': true
}).on('message', function(data) {
console.log('socket '+ i +': got `message`', data);
}).on('broadcast', function(data) {
console.log('socket '+ i +': got `broadcast`', data);
}).on('privateMessage', function(data) {
console.log('socket '+ i +': got `privateMessage`', data);
});
}, 1000 * i);
})(i);
}
setTimeout(function() {
sockets[0].emit('privateMessage', {to: sockets[1].id, test: 'some text'});
}, 1000 * socketsNum);
Здесь создается 2 подключения к серверу, вешаются обработчики. Обратите внимание на query-параметр userId — он разный для разных подключений. Так мы можем быть уверены, что оба подключения попадут на разные экземпляры приложения. После подключения всех клиентов отправляется событие privateMessage, которое должно прийти клиенту, подключенному к другому экземпляру приложения.
Ну, запускаем всю эту балалайку, открываем в браузере http://localhost:3000/index.html и смотрим в консоль. Отлично, сообщение дошло от клиента1 до сервера1, тот понял, что клиент2 подключен к серверу2, отправил сообщение в Redis, откуда его получил сервер2 и отправил клиенту2.
Вот и все, весь код на GitHub.