使用多个节点

部署多个 Socket.IO 服务器时,需要注意两件事:

  • 如果启用了 HTTP 长轮询(这是默认设置),则启用粘性会话:见下文
  • 使用兼容的适配器,请参见此处

粘性负载平衡

如果您计划在不同的进程或机器之间分配连接负载,则必须确保与特定会话 ID 关联的所有请求都到达发起它们的进程。

为什么需要粘性会话

这是因为 HTTP 长轮询传输在 Socket.IO 会话的生命周期内发送多个 HTTP 请求。

事实上,Socket.IO 在技术上可以在没有粘性会话的情况下工作,具有以下同步(虚线):

Using multiple nodes without sticky sessions

虽然显然可以实现,但我们认为 Socket.IO 服务器之间的这种同步过程会对您的应用程序造成很大的性能影响。

评论:

  • 如果不启用粘性会话,由于“会话 ID 未知”,您将遇到 HTTP 400 错误
  • WebSocket 传输没有这个限制,因为它依赖于整个会话的单个 TCP 连接。这意味着如果您禁用 HTTP 长轮询传输(这在 2021 年是一个完全有效的选择),您将不需要粘性会话:
  1. const socket = io("https://io.yourhost.com", {
  2. // WARNING: in that case, there is no fallback to long-polling
  3. transports: [ "websocket" ] // or [ "websocket", "polling" ] (the order matters)
  4. });

文档:transports

启用粘性会话

要实现粘性会话,主要有两种解决方案:

  • 基于 cookie 路由客户端(推荐解决方案)
  • 根据客户端的原始地址路由客户端

您将在下面找到一些常见负载平衡解决方案的示例:

其他平台请参考相关文档:

重要提示:如果您处于 CORS 情况(前端域与服务器域不同)并且会话亲和性是通过 cookie 实现的,则需要允许凭据:

服务器

  1. const io = require("socket.io")(httpServer, {
  2. cors: {
  3. origin: "https://front-domain.com",
  4. methods: ["GET", "POST"],
  5. credentials: true
  6. }
  7. });

客户端

  1. const io = require("socket.io-client");
  2. const socket = io("https://server-domain.com", {
  3. withCredentials: true
  4. });

没有它,浏览器将不会发送 cookie,您将遇到 HTTP 400“会话 ID 未知”响应。更多信息在这里.

NginX 配置

在文件的http { }部分中nginx.conf,您可以声明一个upstream包含要平衡负载的 Socket.IO 进程列表的部分:

  1. http {
  2. server {
  3. listen 3000;
  4. server_name io.yourhost.com;
  5. location / {
  6. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  7. proxy_set_header Host $host;
  8. proxy_pass http://nodes;
  9. # enable WebSockets
  10. proxy_http_version 1.1;
  11. proxy_set_header Upgrade $http_upgrade;
  12. proxy_set_header Connection "upgrade";
  13. }
  14. }
  15. upstream nodes {
  16. # enable sticky session with either "hash" (uses the complete IP address)
  17. hash $remote_addr consistent;
  18. # or "ip_hash" (uses the first three octets of the client IPv4 address, or the entire IPv6 address)
  19. # ip_hash;
  20. # or "sticky" (needs commercial subscription)
  21. # sticky cookie srv_id expires=1h domain=.example.com path=/;
  22. server app01:3000;
  23. server app02:3000;
  24. server app03:3000;
  25. }
  26. }

请注意hash指示连接将是粘性的说明。

确保您还在worker_processes最顶层配置以指示 NginX 应该使用多少工作人员。您可能还想研究调整块worker_connections内的设置events { }

链接:

Apache HTTPD 配置

  1. Header add Set-Cookie "SERVERID=sticky.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
  2. <Proxy "balancer://nodes_polling">
  3. BalancerMember "http://app01:3000" route=app01
  4. BalancerMember "http://app02:3000" route=app02
  5. BalancerMember "http://app03:3000" route=app03
  6. ProxySet stickysession=SERVERID
  7. </Proxy>
  8. <Proxy "balancer://nodes_ws">
  9. BalancerMember "ws://app01:3000" route=app01
  10. BalancerMember "ws://app02:3000" route=app02
  11. BalancerMember "ws://app03:3000" route=app03
  12. ProxySet stickysession=SERVERID
  13. </Proxy>
  14. RewriteEngine On
  15. RewriteCond %{HTTP:Upgrade} =websocket [NC]
  16. RewriteRule /(.*) balancer://nodes_ws/$1 [P,L]
  17. RewriteCond %{HTTP:Upgrade} !=websocket [NC]
  18. RewriteRule /(.*) balancer://nodes_polling/$1 [P,L]
  19. ProxyTimeout 3

链接:

HAProxy 配置

  1. # Reference: http://blog.haproxy.com/2012/11/07/websockets-load-balancing-with-haproxy/
  2. listen chat
  3. bind *:80
  4. default_backend nodes
  5. backend nodes
  6. option httpchk HEAD /health
  7. http-check expect status 200
  8. cookie io prefix indirect nocache # using the `io` cookie set upon handshake
  9. server app01 app01:3000 check cookie app01
  10. server app02 app02:3000 check cookie app02
  11. server app03 app03:3000 check cookie app03

链接:

Traefik

使用容器标签:

  1. # docker-compose.yml
  2. services:
  3. traefik:
  4. image: traefik:2.4
  5. volumes:
  6. - /var/run/docker.sock:/var/run/docker.sock
  7. links:
  8. - server
  9. server:
  10. image: my-image:latest
  11. labels:
  12. - "traefik.http.routers.my-service.rule=PathPrefix(`/`)"
  13. - traefik.http.services.my-service.loadBalancer.sticky.cookie.name=server_id
  14. - traefik.http.services.my-service.loadBalancer.sticky.cookie.httpOnly=true

使用文件提供程序

  1. ## Dynamic configuration
  2. http:
  3. services:
  4. my-service:
  5. rule: "PathPrefix(`/`)"
  6. loadBalancer:
  7. sticky:
  8. cookie:
  9. name: server_id
  10. 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:

  1. const cluster = require("cluster");
  2. const http = require("http");
  3. const { Server } = require("socket.io");
  4. const numCPUs = require("os").cpus().length;
  5. const { setupMaster, setupWorker } = require("@socket.io/sticky");
  6. const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
  7. if (cluster.isMaster) {
  8. console.log(`Master ${process.pid} is running`);
  9. const httpServer = http.createServer();
  10. // setup sticky sessions
  11. setupMaster(httpServer, {
  12. loadBalancingMethod: "least-connection",
  13. });
  14. // setup connections between the workers
  15. setupPrimary();
  16. // needed for packets containing buffers (you can ignore it if you only send plaintext objects)
  17. // Node.js < 16.0.0
  18. cluster.setupMaster({
  19. serialization: "advanced",
  20. });
  21. // Node.js > 16.0.0
  22. // cluster.setupPrimary({
  23. // serialization: "advanced",
  24. // });
  25. httpServer.listen(3000);
  26. for (let i = 0; i < numCPUs; i++) {
  27. cluster.fork();
  28. }
  29. cluster.on("exit", (worker) => {
  30. console.log(`Worker ${worker.process.pid} died`);
  31. cluster.fork();
  32. });
  33. } else {
  34. console.log(`Worker ${process.pid} started`);
  35. const httpServer = http.createServer();
  36. const io = new Server(httpServer);
  37. // use the cluster adapter
  38. io.adapter(createAdapter());
  39. // setup connection with the primary process
  40. setupWorker(io);
  41. io.on("connection", (socket) => {
  42. /* ... */
  43. });
  44. }

在节点之间传递事件

既然您有多个接受连接的Socket.IO 节点,如果您想向所有客户端(或某个房间中的客户端)广播事件,您将需要某种方式在进程或计算机之间传递消息。

负责路由消息的接口就是我们所说的Adapter