Сразу код.
В 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.