Spec-Zone.ru › Socket.IO 3

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

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

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

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

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

Зачем требуется кэширование сессий

Это связано с тем, что транспорт 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

Включение кэширования сессий

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

  • маршрутизация клиентов на основе 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 { }.

Ссылки:

  • Пример
  • Документация 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 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:

Broadcasting with Redis

Отправка сообщений из внешнего мира

Использование адаптера Redis предоставляет еще одно преимущество: вы можете отправлять события извне контекста ваших процессов Socket.IO.

Diagram with Redis adapter and external emitter

Этот эмиттер доступен на нескольких языках:

  • 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

Spec-Zone.ru

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