Использование нескольких узлов
При развертывании нескольких серверов Socket.IO необходимо учесть два момента:
- включить кэширование сессий, если включена HTTP-передача с длительным опросом (по умолчанию): см. ниже
- использовать адаптер Redis (или другой совместимый адаптер): см. ниже
Стикерное распределение нагрузки
Если планируется распределять нагрузку подключений между разными процессами или машинами, необходимо убедиться, что все запросы, связанные с конкретным идентификатором сессии, достигают процесса, который их инициировал.
Зачем требуется кэширование сессий
Это связано с тем, что транспорт HTTP-передачи с длительным опросом отправляет несколько HTTP-запросов в течение жизни сессии Socket.IO.
На самом деле, Socket.IO теоретически мог бы работать и без кэширования сессий, с такой синхронизацией (пунктирными линиями):
Хотя, очевидно, возможно реализовать такую синхронизацию, мы считаем, что этот процесс синхронизации между серверами Socket.IO повлечет за собой значительный удар по производительности вашего приложения.
Замечания:
- без включения кэширования сессий у вас возникнут ошибки HTTP 400 из-за «Неизвестный идентификатор сессии»
- транспорт WebSocket не имеет этого ограничения, поскольку он использует одно TCP-соединение для всей сессии. Это означает, что если вы отключите транспорт HTTP-передачи с длительным опросом (что является вполне допустимым выбором в 2021 году), вам не потребуется кэширование сессий:
const socket = io("https://io.yourhost.com", {
// WARNING: in that case, there is no fallback to long-polling
transports: [ "websocket" ] // or [ "websocket", "polling" ] (the order matters)
}); |
Документация: transports
Включение кэширования сессий
Для достижения кэширования сессий существуют два основных решения:
- маршрутизация клиентов на основе cookie (рекомендуемое решение)
- маршрутизация клиентов на основе их исходного адреса
Ниже приведены примеры с распространенными решениями для распределения нагрузки:
- NginX (на основе IP)
- Apache HTTPD (на основе cookie)
- HAProxy (на основе cookie)
- Traefik (на основе cookie)
- Node.js
clusterмодуль
Для других платформ, пожалуйста, обратитесь к соответствующей документации:
- Kubernetes: https://kubernetes.github.io/ingress-nginx/examples/affinity/cookie/
- AWS (балансировщики приложений): https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#sticky-sessions
- GCP: https://cloud.google.com/load-balancing/docs/backend-service#session_affinity
- Heroku: https://devcenter.heroku.com/articles/session-affinity
Важно: если вы находитесь в ситуации CORS (домен клиента отличается от домена сервера) и сродство сессий достигается с помощью cookie, вам необходимо разрешить данные:
Сервер
const io = require("socket.io")(httpServer, {
cors: {
origin: "https://front-domain.com",
methods: ["GET", "POST"],
credentials: true
}
}); |
Клиент
const io = require("socket.io-client");
const socket = io("https://server-domain.com", {
withCredentials: true
}); |
Без этого браузер не отправит cookie, и у вас возникнут ответы HTTP 400 «Неизвестный идентификатор сессии». Дополнительная информация здесь.
Настройка NginX
В разделе http { } вашего файла nginx.conf вы можете объявить раздел upstream со списком процессов Socket.IO, между которыми нужно распределять нагрузку:
http {
server {
listen 3000;
server_name io.yourhost.com;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://nodes;
# enable WebSockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
upstream nodes {
# enable sticky session with either "hash" (uses the complete IP address)
hash $remote_addr consistent;
# or "ip_hash" (uses the first three octets of the client IPv4 address, or the entire IPv6 address)
# ip_hash;
# or "sticky" (needs commercial subscription)
# sticky cookie srv_id expires=1h domain=.example.com path=/;
server app01:3000;
server app02:3000;
server app03:3000;
}
} |
Обратите внимание на инструкцию hash, которая указывает на то, что соединения будут кэшироваться.
Убедитесь, что вы также настроите worker_processes на самом верхнем уровне, чтобы указать, сколько рабочих процессов должно использовать NginX. Возможно, вам также захочется настроить параметр worker_connections внутри блока events { }.
Ссылки:
Настройка Apache HTTPD
Header add Set-Cookie "SERVERID=sticky.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
<Proxy "balancer://nodes_polling">
BalancerMember "http://app01:3000" route=app01
BalancerMember "http://app02:3000" route=app02
BalancerMember "http://app03:3000" route=app03
ProxySet stickysession=SERVERID
</Proxy>
<Proxy "balancer://nodes_ws">
BalancerMember "ws://app01:3000" route=app01
BalancerMember "ws://app02:3000" route=app02
BalancerMember "ws://app03:3000" route=app03
ProxySet stickysession=SERVERID
</Proxy>
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) balancer://nodes_ws/$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*) balancer://nodes_polling/$1 [P,L]
ProxyTimeout 3 |
Ссылки:
Настройка HAProxy
# Reference: http://blog.haproxy.com/2012/11/07/websockets-load-balancing-with-haproxy/ listen chat bind *:80 default_backend nodes backend nodes option httpchk HEAD /health http-check expect status 200 cookie io prefix indirect nocache # using the `io` cookie set upon handshake server app01 app01:3000 check cookie app01 server app02 app02:3000 check cookie app02 server app03 app03:3000 check cookie app03 |
Ссылки:
Traefik
Использование меток контейнеров:
# docker-compose.yml
services:
traefik:
image: traefik:2.4
volumes:
- /var/run/docker.sock:/var/run/docker.sock
links:
- server
server:
image: my-image:latest
labels:
- "traefik.http.routers.my-service.rule=PathPrefix(`/`)"
- traefik.http.services.my-service.loadBalancer.sticky.cookie.name=server_id
- traefik.http.services.my-service.loadBalancer.sticky.cookie.httpOnly=true |
С помощью поставщика файла:
## Dynamic configuration
http:
services:
my-service:
rule: "PathPrefix(`/`)"
loadBalancer:
sticky:
cookie:
name: server_id
httpOnly: true |
Ссылки:
Использование кластера Node.js
Так же, как и NginX, Node.js имеет встроенную поддержку кластеризации через модуль cluster.
Существует несколько решений, в зависимости от вашего варианта использования:
| Пакет NPM | Как это работает |
|---|---|
@socket.io/sticky |
маршрутизация основана на параметре запроса sid |
sticky-session |
маршрутизация основана на connection.remoteAddress |
socketio-sticky-session |
маршрутизация основана на заголовке x-forwarded-for |
Пример с @socket.io/sticky:
const cluster = require("cluster");
const http = require("http");
const { Server } = require("socket.io");
const redisAdapter = require("socket.io-redis");
const numCPUs = require("os").cpus().length;
const { setupMaster, setupWorker } = require("@socket.io/sticky");
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
const httpServer = http.createServer();
setupMaster(httpServer, {
loadBalancingMethod: "least-connection", // either "random", "round-robin" or "least-connection"
});
httpServer.listen(3000);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on("exit", (worker) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
} else {
console.log(`Worker ${process.pid} started`);
const httpServer = http.createServer();
const io = new Server(httpServer);
io.adapter(redisAdapter({ host: "localhost", port: 6379 }));
setupWorker(io);
io.on("connection", (socket) => {
/* ... */
});
} |
Передача событий между узлами
Адаптер Redis
Теперь, когда у вас есть несколько узлов Socket.IO, принимающих подключения, если вы хотите транслировать события всем клиентам (или клиентам в определенной комнате), вам потребуется способ передачи сообщений между процессами или компьютерами.
Интерфейс, отвечающий за маршрутизацию сообщений, называется адаптером. Вы можете реализовать свой собственный адаптер на базе socket.io-adapter (наследуя его) или использовать адаптер, реализованный на основе Redis: socket.io-redis:
const io = require("socket.io")(3000);
const redis = require("socket.io-redis");
io.adapter(redis({ host: "localhost", port: 6379 })); |
Затем следующий вызов:
io.emit("hi", "all sockets"); |
будет транслироваться всем клиентам через механизм опубликовать/подписаться Redis:
Отправка сообщений из внешнего мира
Использование адаптера Redis предоставляет еще одно преимущество: вы можете отправлять события извне контекста ваших процессов Socket.IO.
Этот эмиттер доступен на нескольких языках:
- Javascript: https://github.com/socketio/socket.io-emitter
- Java: https://github.com/sunsus/socket.io-java-emitter
- Python: https://pypi.org/project/socket.io-emitter/
- PHP: https://github.com/rase-/socket.io-php-emitter
- Golang: https://github.com/yosuke-furukawa/socket.io-go-emitter
- Perl: https://metacpan.org/pod/SocketIO::Emitter
- Rust: https://github.com/epli2/socketio-rust-emitter
© 2014–2021 Automattic
Licensed under the MIT License.
https://socket.io/docs/v3/using-multiple-nodes