node.js アプリの負荷分散構成を考える

node.js の負荷分散について考えてみました (フェイルオーバは考慮できていません).個人レベルなので 1 台のハード上に仮想マシンを 5〜6 個立ち上げて実験しています.

見出し

  • はじめに
  • cluster で負荷分散
  • 寄り道:cluster の仕組み
  • 例えばこんな全体構成
  • おわりに

はじめに

node.js は設計上,大量のコネクションを省リソース (プロセス・スレッドをバカスカ生成しない) でさばきます.おそらく想定されているのは I/O バウンドな処理であり,この場合は基本的に非同期で処理されるため,I/O 待ちで他のリクエスト処理がブロックすることはまずありません.
node.js は「サービスをつなぎ・組み合わせるためのハブ」的な位置づけが一番しっくりくるように感じます *1
ただ,

  • 大量のリクエストをさばかなければならない
  • ロジックが重くてコールバック処理に負荷がかかってしまう

等といった場合では,CPU 数に応じて負荷分散させたくなります.
また,Web アプリケーションの静的コンテンツ (js, css, image 等) の配信まで node.js ノードに任せるのは明らかに非効率ですよね.

cluster で負荷分散

この記事 (http://blog.asial.co.jp/807) を読んで cluster を試してみたところ,大変使いやすい.express のコミッタが開発しているためか,express との相性も良いと感じます.
おなじみ express コマンドでスケルトンを生成し,以下のように書き換えます.

// app.js
var express = require('express');
var app     = express.createServer();

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.logger());
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.get('/', function(req, res){
  res.render('index', {
    title: 'Express',
    worker_id: process.env.CLUSTER_WORKER  // worker の ID が取れる
  });
});

module.exports = app;
<!-- views/index.jade -->
h1= title
p Welcome to #{title}
p worker #{worker_id}
// cluster.js
var cluster = require('cluster');

cluster('./app')
  .use(cluster.debug())    // debug ログが出るので,動作が分かりやすくなる
  .use(cluster.logger())
  .use(cluster.pidfiles())
  .on('close', function() { console.log('cluster end'); })
  .listen(8001);

$ node cluster.js して F5 アタック?をかけると,worker の切り替わる様子が分かります.

Readme の Example で 'recommended usage' とあるように,cluster 関数にファイル名 './app' を指定すると,master プロセスでは require('./app') が発生せず,worker プロセスの場合にのみ require('./app') されます (docs/api).
また,use(cluster.repl(port)) することで簡単な管理コンソールを起動することができます (docs/repl).
分散してもセッションを維持したい場合は,express で connect-redis を利用するのが一番手軽です.

寄り道:cluster の仕組み

cluster は以下のような構成で複数の node.js プロセスを管理しています.


Worker プロセス数は外部から指定することも可能ですが,デフォルトでは CPU 数だけ起動します.また,プロセス間のやりとり (赤点線矢印) は JSON で独自の RPC をしています (JSON-RPC とは異なる仕様).

プロセスとインスタンスの構成は以下のようになっています.コードを読む際はこの構成を念頭に置くと,理解しやすくなります.

例えばこんな全体構成

例えばこんな全体構成.動作確認用アプリとして チャットアプリ をイメージしており,手元の環境では動作しています *2



クライアントからは HTTP リクエストと socket.io (WebSocket) による通信が発生します.

サーバ側では,リバースプロキシ (の設定をした nginx) を配置し,静的コンテンツは自身で配信,動的コンテンツについてはバックエンドの cluster にリクエストを投げます.設定としてはこんな感じです.

# /etc/nginx/conf.d/proxy.conf として以下の内容を作成します (IP は適当です).
# ↓ロードバランサ設定の場合
# upstream clusters {
#   server 192.168.11.20:8001;
#   server 192.168.11.20:8002;
# }
server {
  listen 8080;
  server_name 192.168.11.10;
  location / {
    proxy_pass http://192.168.11.20:8001;
#    proxy_pass http://clusters;
  }
  location /images/ {
    alias /var/www/public/images/;
  }
  location /stylesheets/ {
    alias /var/www/public/stylesheets/;
  }
  location /javascripts/ {
    alias /var/www/public/javascripts/;
  }
}

cluster マシンを増やす場合は nginx でロードバランシングさせるか,LVS + keepalived (←こっちはやったことない) で分散させます.

cluster 内の node.js では,express でメイン画面を,リアルタイム通信には socket.io を利用しています.もう鉄板ですね.セッション維持には redis を利用しています.

relay server は,各 node.js プロセスで発生した Socket.io のイベントを別の node.js プロセスに伝達するための中継サーバです.今回は実験ということもあって,適当にこしらえました.

メイン DB には mongoDB を利用しています (後に作成予定のアプリにとって都合が良いため).Replica Set 便利ですね.今回は sharding しないので 1 セットだけです.express からの接続には mongoose を利用しています.


参考:

おわりに

今回は node.js の負荷分散について試してみました.cluster の簡単な解説と,全体構成を示しています.
一応,proxy から DB まで一通りつないで実験していますが,Socket.io と cluster の組み合わせに問題がないのかを調査し切れていません.また,はやいところ計測を済ませてしまいたいです.

あとはテストについて調査をしたら,そろそろアプリを作り始めても良いんじゃないかな,KrdLab よ.

*1:多くの Web アプリがこれに該当すると思いますが

*2:他の人たちはどんな感じでやっているんでしょうか.是非知りたいのですが...