Spec-Zone.ru › Socket.IO 4

Использование нескольких узлов

При развертывании нескольких серверов Socket.IO нужно учесть два момента:

  • включить «липкую сессию», если включен HTTP-опрос с длительным временем ожидания (по умолчанию): см. ниже
  • использовать совместимый адаптер, см. здесь

Липкое распределение нагрузки

Если вы планируете распределить нагрузку подключений между различными процессами или машинами, необходимо убедиться, что все запросы, связанные с определенным идентификатором сессии, попадают в процесс, который их инициировал.

Почему требуется липкая сессия

Это связано с тем, что транспорт HTTP-опрос с длительным временем ожидания отправляет несколько HTTP-запросов в течение жизни сессии Socket.IO.

На самом деле, Socket.IO технически мог бы работать без липких сессий, с такой синхронизацией (пунктирными линиями):

Using multiple nodes without sticky sessions

Хотя такая реализация, безусловно, возможна, мы считаем, что такой процесс синхронизации между серверами 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

Включение липкой сессии

Для достижения липкой сессии существуют два основных решения:

  • маршрутизация клиентов на основе куки (рекомендуемое решение)
  • маршрутизация клиентов на основе их исходного адреса

Ниже приведены примеры с распространёнными решениями балансировки нагрузки:

  • NginX (на основе IP)
  • Apache HTTPD (на основе куки)
  • HAProxy (на основе куки)
  • Traefik (на основе куки)
  • Node.js cluster модуль

Для других платформ, пожалуйста, обратитесь к соответствующей документации:

  • Kubernetes: https://kubernetes.github.io/ingress-nginx/examples/affinity/cookie/
  • AWS (балансировщики приложений): https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html
  • GCP: https://cloud.google.com/load-balancing/docs/backend-service#session_affinity
  • Heroku: https://devcenter.heroku.com/articles/session-affinity

Важное замечание: если у вас есть ситуация с CORS (домен клиента отличается от домена сервера) и аффинити сессий достигается с помощью куки, вам необходимо разрешить аутентификационные данные:

Сервер

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
});

Без этого куки не будет отправлен браузером, и вы столкнётесь с ответами 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 { }.

Ссылки:

  • Пример
  • Документация NginX

Настройка 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 numCPUs = require("os").cpus().length;
const { setupMaster, setupWorker } = require("@socket.io/sticky");
const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);

  const httpServer = http.createServer();

  // setup sticky sessions
  setupMaster(httpServer, {
    loadBalancingMethod: "least-connection",
  });

  // setup connections between the workers
  setupPrimary();

  // needed for packets containing buffers (you can ignore it if you only send plaintext objects)
  // Node.js < 16.0.0
  cluster.setupMaster({
    serialization: "advanced",
  });
  // Node.js > 16.0.0
  // cluster.setupPrimary({
  //   serialization: "advanced",
  // });

  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);

  // use the cluster adapter
  io.adapter(createAdapter());

  // setup connection with the primary process
  setupWorker(io);

  io.on("connection", (socket) => {
    /* ... */
  });
}

Передача событий между узлами

Теперь, когда у вас есть несколько узлов Socket.IO, принимающих подключения, если вы хотите транслировать события всем клиентам (или клиентам в определенной комнате), вам понадобится способ передачи сообщений между процессами или компьютерами.

Интерфейс, отвечающий за маршрутизацию сообщений, называется Адаптер.

© 2014–2021 Automattic
Licensed under the MIT License.
https://socket.io/docs/v4/using-multiple-nodes

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API