過去の記事でnode.jsの話題をいくつか取り上げて来ましたが、node.jsではAmazonのELBと併用した際に問題があり、
その注意点をまとめてみました。
node.jsの参考記事一例 :
AWS SDK for Node.js ってなんじゃ?
Node.jsってなんじゃ?(knox:S3にアクセス)
○Socket.IO
Socket.IOはnode.jsでwebsocketを使用する時のデファクトと言えるライブラリです。
他のSocket.IOが人気になったのは、以下のような利点のためです。
- websocketをサポートしていないブラウザでは、自動的にxhr等のポーリングを使い通信できる
- 接続に失敗しても再接続等を自動的に行う
ここではSocket.IOを使用する前提で、ELBを経由して複数のnodeサーバをホストする場合についてまとめてみました。
Socket.IOのインストールに関しては、WebSocketってなんじゃ?(Node編2 Socket.IOでプッシュ通信)の記事に
記載してあります。
nodeサーバには以下のようにファイルを配置し、publicをhttpdのドキュメントルートにしておきます。
(上記の記事等で使用したチャットアプリになります)
app
├── node
│ └── server.js
└── public
├── assets
│ └── js
│ └── client.js
├── health.txt
└── index.html
public/health.txtはELB用のヘルスチェックファイルで、中身はありません。
その他、各ファイルの内容は以下の通りです。
・server.js
var io = require('socket.io').listen(3000);
io.sockets.on('connection', function (socket) {
socket.emit('info', { msg: 'welcome' });
socket.on('msg', function (msg) {
io.sockets.emit('msg', {msg: msg});
});
socket.on('disconnect', function(){
socket.emit('info', {msg: 'bye'});
});
});
・index.html
nodetest
・client.js
$(function(){
var socket = io.connect('http://'+location.hostname+':3000/');
socket.on('connect', function(){
$("#log").html($("#log").html() + "
" + (new Date()).toLocaleString()+ 'connected');
});
socket.on('disconnect', function(){
$("#log").html($("#log").html() + "
" + (new Date()).toLocaleString()+'disconnected');
});
socket.on('info', function (data) {
$("#log").html($("#log").html() + "
" + (new Date()).toLocaleString()+ data.msg);
});
socket.on('msg', function(data){
$("#log").html($("#log").html() + "
" + (new Date()).toLocaleString()+ "" + data.msg + "");
});
$("#send").click(function(){
var msg = $("#msg").val();
if(!msg){
alert("input your message");
return;
}
socket.emit('msg', msg);
});
});
ポートは3000番を利用し、ここでhttpdを起動しておきます。
また、下記のようにnodeを起動しておきます。
node server.js
○AWS
・単体構成
まず最初の例として、AWSは以下のような構成だとします。
nodeサーバのEC2インスタンスはhtmlのホストとwebsocketの両方を担うため、80と3000のポートを
セキュリティグループで開放しておきます。
画面を開いてみます。
問題なく成功します。
通信を確認してみると、websocketで通信されていることがわかります。
・ELB
次に、nodeサーバのインスタンスをもう一台追加し、新規作成したELB配下に2つのインスタンスを配置します。
以下のように80番と3000番をELBのリスナーに設定します。
そして、ELBのエンドポイントのURLをブラウザで開きます。
そうすると、xhr-pollingになり、接続と切断が繰り返されます。
また、画面を2つ開いてメッセージを送信しても相手に届かない場合があります。
注意点1 : ELBはhttpではなくtcpでポートを設定
websocket通信ではクライアントとサーバの間のハンドシェイクにUpgradeヘッダを送信します。
しかし、ELBはhttpリスナーの場合Upgradeヘッダを削ってしまうようです。
- RFC6455 : The WebSocket Protocol 日本語訳
- ELBでHTTPリスナーだとWebSocketは使えない
そのため、Socket.IOはwebsocketが使えないと判断し、次善策の一つとして通常のhttp通信で
xhr-pollingで接続することになります。
これはajaxの通信と同じです。
そこで、ELBでは上記リンクの通り、websocket用のリスナーはhttpの3000番ではなくtcpの3000番を
設定する必要があります。
注意点2 : Redisでセッション共有を行う
接続や切断が繰り返されるのはxhrのポーリングの度にハンドシェイクが確立したサーバとは別のサーバへ
接続に行くからのようです。
これはELBのリスナー設定をtcpにした場合も同様で、一度websocket通信が確立したかのように見えても、
次に接続した時に別のサーバに繋がると、接続が切れたもしくはwebsocketに失敗したと判断し、
xhr-polling等他の方法で通信しようとするようです。
根本的な問題として、2つのnodeサーバの間で接続情報(セッション)が共有されないため、ELBを通して
node1とnode2にそれぞれ接続したクライアント間ではメッセージのやり取りができません。
そこで、nodeサーバのバックエンドとして、redisを利用してセッション共有を行う方法が有効です。
socket.ioはセッションを保持する方式としてローカルメモリを使用するMemoryStoreを使用しますが、
オプションにてRedisではRedisStoreを使用することができます。
LearnBoost / socket.io : Configuring Socket.IO
上記のモジュールを使用します。
redisサーバを追加し、redisを起動しておきます。
インスタンスにはセキュリティグループ等でredisで使用するポートを開放しておきます。
そして、以下のようにserver.jsを変更します。
・server.js
var io = require('socket.io').listen(3000);
var RedisStore = require('socket.io/lib/stores/redis');
opts = {host:'xxx.xxx.xxx.xxx', port:6379};
io.set('store', new RedisStore({redisPub:opts, redisSub:opts, redisClient:opts}));
io.sockets.on('connection', function (socket) {
socket.emit('info', { msg: 'welcome' });
socket.on('msg', function (msg) {
io.sockets.emit('msg', {msg: msg});
});
socket.on('disconnect', function(){
socket.emit('info', {msg: 'bye'});
});
});
以上で、redisサーバでnode1,node2のセッションを共有できます。
何度か試し、正しく通信できていることを確認しました。
○まとめ
注意点としては、ELBを使用した場合はtcpでリッスンすること。
それと、ELBに限らずnodeサーバをスケールする場合は、redisサーバでセッション情報を共有する必要があります。
こちらの記事はなかの人(memorycraft)監修のもと掲載しています。
元記事は、こちら