メインコンテンツへスキップ
バージョン: 4.x

複数のノードの使用

複数のSocket.IOサーバーを展開する場合、注意すべき点が2つあります。

  • HTTPロングポーリングが有効になっている場合(デフォルト)、スティッキーセッションを有効にする必要があります。詳細は下記を参照してください。
  • 互換性のあるアダプターを使用する必要があります。詳細はこちらを参照してください。

スティッキーロードバランシング

異なるプロセスまたはマシン間で接続の負荷を分散させる場合は、特定のセッションIDに関連付けられたすべてのリクエストが、それらを発信したプロセスに到達するようにする必要があります。

なぜスティッキーセッションが必要なのか

これは、HTTPロングポーリングトランスポートが、Socket.IOセッションのライフタイム中に複数のHTTPリクエストを送信するためです。

実際には、Socket.IOは、(破線で示された)次の同期を行うことで、技術的にはスティッキーセッションなしで動作できます。

Using multiple nodes without sticky sessionsUsing multiple nodes without sticky sessions

実装は可能ですが、Socket.IOサーバー間のこの同期プロセスは、アプリケーションのパフォーマンスに大きな影響を与えると考えられます。

備考

  • スティッキーセッションを有効にしないと、「セッションIDが不明」という理由で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

スティッキーセッションの有効化

スティッキーセッションを実現するには、主に2つの方法があります。

  • Cookieに基づいてクライアントをルーティングする(推奨される方法)
  • 発信元アドレスに基づいてクライアントをルーティングする

一般的なロードバランシングソリューションの例を以下に示します。

他のプラットフォームについては、関連ドキュメントを参照してください。

重要な注意: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 "セッションIDが不明" レスポンスが発生します。詳細についてはこちらを参照してください。

nginxの設定

nginx.confファイルのhttp { }セクション内で、ロードバランスを適用するSocket.IOプロセスのリストを含むupstreamセクションを宣言できます。

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が使用するワーカー数を指定してください。events { }ブロック内のworker_connections設定の調整も検討するとよいでしょう。

リンク

注意

nginxのproxy_read_timeoutの値(デフォルトでは60秒)は、Socket.IOのpingInterval + pingTimeout(デフォルトでは45秒)より大きくする必要があります。そうしないと、nginxは指定された遅延後にデータが送信されない場合、接続を強制的に閉じ、クライアントは「トランスポートクローズ」エラーを受け取ります。

nginx Ingress(Kubernetes)

Ingress設定のannotationsセクション内で、クライアントのIPアドレスに基づくアップストリームハッシュを宣言できます。これにより、Ingressコントローラーは特定のIPアドレスからのリクエストを常に同じポッドに割り当てるようになります。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: your-ingress
namespace: your-namespace
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
set $forwarded_client_ip "";
if ($http_x_forwarded_for ~ "^([^,]+)") {
set $forwarded_client_ip $1;
}
set $client_ip $remote_addr;
if ($forwarded_client_ip != "") {
set $client_ip $forwarded_client_ip;
}
nginx.ingress.kubernetes.io/upstream-hash-by: "$client_ip"
spec:
ingressClassName: nginx
rules:
- host: io.yourhost.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: your-service
port:
number: 80

メモ

  • nginx.ingress.kubernetes.io/upstream-hash-by: "$client_ip"

このアノテーションは、NGINX Ingress Controllerに、クライアントのIPアドレスを使用して、受信トラフィックをKubernetesクラスター内の特定のポッドにルーティングするように指示します。これは、スティッキーセッションを維持するために重要です。

  • nginx.ingress.kubernetes.io/configuration-snippet

このカスタムNGINX設定スニペットには2つの目的があります。

  1. リクエストがX-Forwarded-Forヘッダーを追加するアップストリームリバースプロキシまたはAPIゲートウェイを通過する場合、このスニペットはそのヘッダーから最初のIPアドレスを抽出し、$client_ipを更新するために使用します。

  2. そのようなプロキシやゲートウェイがない場合、スニペットは単にリモートアドレス(ingressに直接接続されたクライアントのIPアドレス)を使用します。

これにより、nginx.ingress.kubernetes.io/upstream-hash-by: "$client_ip"アノテーションによって有効になるスティッキーセッションロジックに正しいクライアントIPが使用されるようになります。このスニペットは、アーキテクチャにリバースプロキシやAPIゲートウェイなどのアップストリームネットワークコンポーネントが含まれている場合に特に重要です。

リンク

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]

# must be bigger than pingInterval (25s by default) + pingTimeout (20s by default)
ProxyTimeout 60

リンク

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 Clusterの使用

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ノードが接続を受け入れるようになったので、すべてのクライアント(または特定のルームのクライアント)にイベントをブロードキャストしたい場合は、プロセス間またはコンピューター間でメッセージをやり取りする方法が必要になります。

メッセージのルーティングを担当するインターフェイスは、アダプターと呼ばれます。