heroku上でnode.jsを使ってみた(所感)
Heroku で node.js が使えるようになっていたので、ちょっとしたアプリを書いた。
なぜnode.jsなのか
サーバサイドJavaScriptの本命「Node.js」の基礎知識(1/3)- @ITより。
Node.jsとは、サーバサイドJavaScriptの1つですが、「シングルスレッドベースの非同期処理環境」という特徴を持っています。 普通、非同期というと、サーバサイド側でマルチスレッドでのロジックを組むことを思い浮かべるかと思います。シングルスレッドでどうやって、非同期処理を行うかというと、「イベントループ」と呼ばれるアーキテクチャを用いて、非同期処理を行います。
node.jsは大量にリクエストが来た場合に、効率よく処理をこなせるという理解。
例えるなら、通常のマルチプロセス・マルチスレッドでの受付は、銀行の窓口のようなもの。 列に並んで、順番が来たら窓口のどれかに案内される。窓口では、1対1での応対になる。 一方のnode.jsは聖徳太子。1人の聖徳太子が同時に複数の人の話を聞く。聖徳太子は部下に処理を依頼し、終わったものから返事をしていくイメージでいる。
インストール&セットアップ
ほぼHeroku | Dev Center | Getting Started With Node.js on Heroku/Cedarの手順どおり。 Herokuとgitに慣れていれば、特に迷うところは無いかな。
アプリの構成
node.jsはアプリケーションサーバのようなものなので、素でアプリを書くのはちょっと大変(らしい)。servletを直接書くのに近い? なので、SinatraライクのExpressというライブラリを使った。 データベースにはMongoDBのホスティングサービスであるMongoHQを使う。 gemとbundleの代わりにnpmを使ったりと、なんとなく似ている。
アプリを書いてみた
Amazon API認証プロキシ用のリバース・プロキシをnode.jsに移植してみた。 といっても、書けたのはリバースプロキシの処理部分だけ。 ユーザ管理部分までは作っていない。 コードはgithubに置いている。
書いてみて実感したのは、思った以上に非同期処理のプログラムを書くのが大変ということ。
- プログラムの出口が1本じゃない(処理が非同期で分岐する)
- ループ処理を書くのが大変
まぁ、大変というか、考え方を変える必要があるんだろう。
例えば、リバースプロキシでは、あらかじめ登録されているサーバに接続し、応答がなかった場合には次のサーバに接続する、という処理が必要になる。 はじめはこう書こうとした。
var res; // 登録されているサーバへ順にアクセス、302が返ってきたらループを抜ける for (i = 0; i < proxies.length; i++) { var endpoint = url.parse(proxies[i].endpoint); var options = {host: endpoint.host, port: endpoint.port, path: endpoint.pathname } http.get(options, function(proxy_res) { if (res.statusCode == 302) { res = proxy_res; break; } } } // サーバからの返り値を出力 if (res.statusCode == 302) { console.log(res.headers.locaton); } else { console.log("error"); }
でも、この書き方は上手く行かない。resは'undefined'のままになる。 なぜかというと、http.get()は非同期で実行されるから。外側の関数が先に最後まで実行された後に、http.get()で登録したコールバックが実行される。 ループの内側よりループの外側が先に実行されるので、途中でループから抜けることもできないし、ループの後の処理を書く場所も変わる。 そのため、単純なループでも非同期処理と組み合わせると、再帰的に書く必要がある。 これ、関数型言語っぽい考え方が必要なんじゃないかな…。
async.jsを使ってフロー制御
途方に暮れかけていたら、すでに解決策が提示されていた。
async.jsが提供するasync.util()関数を使うと、先ほどのループを以下のように書くことができる。
var res; async.until( // ループ継続判定 function() { return (proxies.length == 0 || res.statusCode == 302); }, // ループ処理 function(callback) { var endpoint = url.parse(proxies.shift().endpoint); var options = {host: endpoint.host, port: endpoint.port, path: endpoint.pathname } http.get(options, function(proxy_res) { res = proxy_res; // ループ判定処理へ callback(); }); }, // ループ後処理 function(err) { if (res.statusCode != 302) { console.log(res.headers.locaton); } else { console.log("error"); } } );
かなり苦戦したけど、感じはつかめた。 クセがあるので、慣れるまでは大変そうだなぁ。これ。
速度比較してみた
結局、リバースプロキシはSinatraで書き直しているんだけど、せっかくなので速度を比較してみた。 結果はなかなか興味深いものになった。
測定環境
- MacBook Pro (2.2GHz Core 2 Duo + 4GB Memory), OS X 10.6.7
- node.jsとSinatra (thin) はローカルで起動
- 測定にはabコマンドを使用
測定結果(n10 -c1: 10回接続, 同時接続数1)
同時接続数1では、node.jsのほうがちょっと速いかな、といったところ。
測定項目 | node.js | sinatra |
スループット | 2.36 [#/sec] | 0.86 [#/sec] |
応答速度 | 423.064 [ms] | 1164.615 [ms] |
測定結果(n10 -c5: 10回接続, 同時接続数5)
同時接続数を5にしてみる。 node.jsはスループット・応答速度ともに同時接続数1のときより良くなっている。 一方のsinatra版は同時接続数1とほぼ同じ結果。
測定項目 | node.js | sinatra |
スループット | 4.19 [#/sec] | 0.82 [#/sec] |
応答速度 | 238.568 [ms] | 1220.792 [ms] |
スループットだけをグラフ化した。node.jsが同時接続に強いのが良く分かる。
sinatra版で同時接続数が変わってもスループットと応答速度が同じだったのは、まぁ当然といえば当然。 1プロセスしか起動していないので、同時に処理できるのは1接続だけだから。 一方のnode.jsでは、1プロセスしか起動していなくても、複数接続を同時に(というか、非同期に)処理できるので、同時接続数が多い方が効率的に処理できたということか。
アプリを書くのはちょっと大変だけど、適材適所でうまく使いこなせると便利そう。