2013年2月7日木曜日

Node.jsってなんじゃ?(Socket.IOとELBのまとめ)

過去の記事でnode.jsの話題をいくつか取り上げて来ましたが、node.jsではAmazonのELBと併用した時の問題があり、その注意点をまとめました。

Socket.IO


Socket.IOはnode.jsでwebsocketを使用するときのデファクトといっていいライブラリです。
他のSocket.IOが人気になったのは、以下のような利点のためです。

  • websocketをサポートしていないブラウザでは、自動的にxhrなどのポーリング使い通信できる
  • 接続に失敗しても再接続などを自動的に行う


ここではSocket.IOを使用する前提で、ELBを経由して複数のnodeサーバーをホストする場合についてまとめてみました。

インストールに関しては以下に書いてありますので割愛します。
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
index.html
client.js



ポートは3000番を利用します。
ここで、httpdを起動しておきます。
また、
node server.js

などで、nodeを起動しておきます。


AWS


単体構成



まず最初の例として、AWSは例として以下のような構成だとします。
nodeサーバーのEC2インスタンスはhtmlのホストとwebsocketの両方を行うので、80と3000のポートをセキュリティグループで開放しておきます。

画面を開いてみます。

普通に成功します。
通信をみてみると、websocketで通信されていることがわかります。


ELB


次に、nodeサーバーのインスタンスをもう一台追加し、新規作成したELB配下に2つのインスタンスをおきます。



ELBのリスナーを以下のように80番と3000番を設定します。


そして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が選べます
Configuring Socket.IO

これを使います。
redisサーバーを追加し、redisを起動しておきます。
インスタンスにはセキュリティグループなどでredisで使用するポートを開放しておきます。



そして、以下のようにserver.jsを変更します。

server.js

これで、redisサーバーでnode1,node2のセッションを共有できます。


何度か試しましたが、うまく通信できているようです。


まとめ


注意点としては、ELBをつかった場合はtcpでリッスンすることと、ELBにかぎらずnodeサーバーをスケールする場合は、redisサーバーでセッション共有する必要があります。

以上です。