Node.js の Heroku 環境におけるパフォーマンス
Heroku で Node.js を動かしても絶対的なパフォーマンスは得られないのだけれど、最近仕事で Node.js on Heroku をやっているので、実際にどれくらいのパフォーマンスがでるのか測ってみました。
今回調べたのは、主に3点。
2X Dyno (
CPUとメモリが2倍のDyno。ただし、コア数は4のまま)を使うと速くなるのか?Cluster に効果はあるのか?
Dyno 数はどれくらいが良いのか?
ベンチマークは Node.js v0.10.9 を対象とし、全て siege のベンチマーク機能の結果です。ベンチマーク対象のコードは、Node.js 本家トップページに載っている例のHTTP サーバで、AWS の us-east-1 リージョンの EC2 m1.large インスタンスで実行した結果です。
$ siege -b 60S -c 100 http://server
結果
1X Dyno
まずはノーマル。これが基準ですね。
Transactions: 155084 hits Availability: 99.99 % Elapsed time: 59.87 secs Data transferred: 1.77 MB Response time: 0.04 secs Transaction rate: 2590.35 trans/sec Throughput: 0.03 MB/sec Concurrency: 99.63 Successful transactions: 155084 Failed transactions: 17 Longest transaction: 5.07 Shortest transaction: 0.00
2X Dyno
2X Dyno を使うと、なぜかパフォーマンスが下がりました。 何回か測りなおしたのですが、結果は同じでした。
Transactions: 127151 hits Availability: 99.99 % Elapsed time: 59.16 secs Data transferred: 1.46 MB Response time: 0.05 secs Transaction rate: 2149.27 trans/sec Throughput: 0.02 MB/sec Concurrency: 99.32 Successful transactions: 127151 Failed transactions: 7 Longest transaction: 4.18 Shortest transaction: 0.00
1X Dyno + Cluster
Cluster でもパフォーマンスは低下します。 Failed transactions が減っているのがせめてもの救いでしょうか。
Transactions: 122164 hits Availability: 100.00 % Elapsed time: 59.52 secs Data transferred: 1.40 MB Response time: 0.05 secs Transaction rate: 2052.49 trans/sec Throughput: 0.02 MB/sec Concurrency: 99.73 Successful transactions: 122164 Failed transactions: 4 Longest transaction: 5.27 Shortest transaction: 0.00
2X Dyno + Cluster
予想はつきましたが、これが最低のパフォーマンスになりました。 2X Dyno はまだ Heroku の Router がうまく捌けていないのか、と思うくらいパフォーマンスが落ちますね。
Transactions: 116440 hits Availability: 100.00 % Elapsed time: 59.48 secs Data transferred: 1.33 MB Response time: 0.05 secs Transaction rate: 1957.63 trans/sec Throughput: 0.02 MB/sec Concurrency: 99.80 Successful transactions: 116440 Failed transactions: 3 Longest transaction: 5.09 Shortest transaction: 0.00
1X Dyno 2台
2台にしても、パフォーマンスが落ちます。 振り分けのコストがそれなりにでかいわけですね。 Logest transaction が大きく下がっているので、引っ掛かりみたいなものを減らせていると思います。
Transactions: 80351 hits Availability: 100.00 % Elapsed time: 59.94 secs Data transferred: 0.92 MB Response time: 0.07 secs Transaction rate: 1340.52 trans/sec Throughput: 0.02 MB/sec Concurrency: 99.79 Successful transactions: 80351 Failed transactions: 4 Longest transaction: 1.70 Shortest transaction: 0.00
1X Dyno 3台
3台でやっと1台の時と同じくらいの Transaction rate が出るようになります。 Logest transaction がさらに下がっているのがわかります。
Transactions: 143521 hits Availability: 100.00 % Elapsed time: 59.72 secs Data transferred: 1.64 MB Response time: 0.04 secs Transaction rate: 2403.23 trans/sec Throughput: 0.03 MB/sec Concurrency: 99.67 Successful transactions: 143521 Failed transactions: 5 Longest transaction: 0.85 Shortest transaction: 0.00
1X Dyno 4台
4台だとまた下がってしまいました。
Transactions: 97675 hits Availability: 99.99 % Elapsed time: 59.05 secs Data transferred: 1.12 MB Response time: 0.06 secs Transaction rate: 1654.11 trans/sec Throughput: 0.02 MB/sec Concurrency: 98.85 Successful transactions: 97675 Failed transactions: 6 Longest transaction: 1.93 Shortest transaction: 0.00
まとめ
今回の結果だけをまとめると
- Heroku で Node.js を使う場合、2X Dyno も Cluster も使わない方が良い。
- Dyno 数は処理能力的には 1 で十分だが、可用性とかを考慮して 3 に設定すると良い。
となります。実際には処理の内容によって結果に差異はでると思うので、実際に測ってみると良いと思います。 自分は効果あると思って Cluster 使ってましたが、今回の結果をみて、使うのをやめました。
2X Dyno は ImageMagick などで画像処理を行うなど、CPU負荷の高い処理を書く場合は意味あるかもしれないが、それは Node.js で書くべきプログラムでもないし、倍の値段払うほどでもないかなと思います。
2X Dyno は Rails または、JVM 系の言語 (Scala / Java)のための Dyno であり、Node.js にはあまり意味がないかと。前述した公式Blog にもそう書いてありますしね。
補足
2X Dyno はまだ Public Beta (でも、明日から本サービス開始)なので、もしかしたら Router の調整とかがうまくいっていない可能性はあります。なので、1か月後くらいにまた測る予定。
追伸
Quipper という会社に転職しました。
YAND (Yet Another Node.js Documentation) のソースを公開しました
1年前にこんなエントリを書きました。
僕の考えた最強の Node.js Document Viewer - hakobera's blog
YAND 自体は結構使っていたのですが、色々あって Node.js v0.6.10 で更新を止めていました。 が、さすがに v0.8 用のものが欲しくなってきたので、v0.8.18 のドキュメントに更新しました。
YAND - Yet Another Node.js Documentation
これに合わせて、ソースの整理もしたので、GitHub で公開しました。
以下の様な感じで、特定のバージョン(多分、v0.8 以上じゃないと動かないと思いますが)のドキュメントを自分で作成できるようにしてあります。NODE_VERSION にドキュメントを作成したバージョンを指定します。
$ make NODE_VERSION=v0.8.18 build
Herokuにもデプロイできるし、そのまま HTTP サーバにコピーしても使えるので、色々と便利に使える場面はあるのかなと思います。 Node.js でスクレイピングして、加工して、サイト作成のサンプルコードにもなるんじゃないかな。
EventEmitter.emit() によくある勘違い
突然ですが、Node.jsで次のプログラムを実行した結果を答えてください。
var EventEmitter = require('events').EventEmitter; var event = new EventEmitter(); console.log('1'); event.on('open', function () { console.log('2'); }); event.emit('open'); console.log(3);
正解は少し下の方に書いてあります。
少しだけスクロールを我慢して考えてみてください。
正解は
1 2 3
です。
1 3 2
だと思っていた人も多いのではないでしょうか。(私だけかもしれませんが)
つまり、表題の「EventEmitter.emit() によくある勘違い」とは、EventEmitter.emit()が次のイベントループで実行されるノンブロッキング処理である、という点です。しかし、EventEmitter.emit()はコールバックを同期呼出しする、つまり、ブロッキングします。
では、1, 3, 2 と出力したい場合は、どう書くかというと、次のように書きます。
var EventEmitter = require('events').EventEmitter; var event = new EventEmitter(); console.log('1'); event.on('open', function () { process.nextTick(function () { console.log('2'); }); }); event.emit('open'); console.log(3);
もしくはこう。
var EventEmitter = require('events').EventEmitter; var event = new EventEmitter(); console.log('1'); event.on('open', function () { console.log('2'); }); process.nextTick(function () { event.emit('open'); }); console.log(3);
開発者はコールバック関数の中でprocess.nextTick()を呼び出すか、emit()自体をprocess.nextTick()のコールバック内で呼び出すことで、処理を次のイベントループに回すことができます。 emit()がコールバックを同期呼び出しするのは、開発者に処理をブロッキングで処理するのか、ノンブロッキングで処理するのかの選択肢を与えるために、意図してそのようにデザインされているのです。
考えてみれば、当たり前のことなのですが、自分がついこの間まで勘違いしていたので、戒めも兼ねて書いてみました。 みなさんも思い込みには気をつけて、必ず動作を確認するようにしましょう。
「Node.jsは静的コンテンツには向いていない」のか?
この記事は東京Node学園祭2012 アドベントカレンダーの8日目の記事です。
この記事を書こうと思った理由
Node.jsに関するWeb上の記事を読んでいると、「Node.jsは静的コンテンツに弱い」とだけ書いてある記事をよく見かけます。有名なところだと、LinkedInのNode.jsのパフォーマンスに関する10個のTipsの3番目のTipsに"Don't use Node.js for static assets"とばっちり書いてあります。
確かにCDNやNginxに比べれば、Node.jsは静的コンテンツの扱いが遅いとは思います。しかし、それは LinkedIn くらいの超大規模なトラフィックがある場合には問題になるとは思いますが、小〜中規模なサイトでもNginxは必須なほど遅いのでしょうか?512MBしかメモリのないVPSにNginxとNode.jsを入れてやりくりすることがホントに効率的なやり方なのでしょうか?
実は自分自身もちょっと前まで「静的ファイルはNginxに任せる」病にかかっていたのですが、PaaSを使って開発するようになり、そもそもNginxインストールできない状況で、Node.jsで静的ファイル配信もありじゃないかと思い始めてきたので、今回、実際にベンチマークで検証してみようと思った次第です。
比較対象
- Nginx 1.2.4
- Node.js 0.8.12
- Node.js標準のhttp, fsモジュール
- node-static 0.6.4
- 静的コンテンツ用のNPMモジュールとしては古参の部類。インメモリキャッシュしてくれる。
- connect 2.6.0 のstaticミドルウェア
- Express の基盤。一番良く使われていると思ったので採択
- st 0.0.9
- 比較的新しい静的コンテンツ用のNPMモジュール。gzip圧縮を標準でサポート。ETagなどクライアント側でも積極的にキャッシュさせるようにして効率的な静的コンテンツ配信が強く意識されています。
比較方法
- Apache Bench (ab) を利用
$ ab -c 200 -n 100000 <url> # gzipありの場合は以下 $ ab -c 200 -n 100000 -H "Accept-Encoding: gzip,deflate,sdch" <url>
- リクエスト/秒で比較
- 画像(png)とHTMLで比較
- gzip 圧縮ありと圧縮なしを比較
- 一部のベンチマークでは、Nginx は worker数を増やしたり、Node.jsはclusterモジュールを利用してcluster化
環境
OS | CPU | Memory | |
---|---|---|---|
Server | CentOS 6.2 | AMD Phenom II X6 1090T | 16GB |
Client | CentOS 5.8 | AMD Phenom II X6 1090T | 16GB |
Server <-> Client 間は1Gbpsのローカルネットワークで繋がっています。注意点として、この環境だとネットワーク帯域の限界から、15,000リクエスト/秒(=約13億リクエスト/日)あたりで性能が頭打ちになります。
ベンチマーク結果
使用したソースファイルや設定ファイルは Gistにおいてあります。
画像 (png)
Node公式サイトのロゴ (5,081 byte) を取得
Nginx | http | node-static | node-static (cluster x4) | connect | st | st (cluster x4) |
---|---|---|---|---|---|---|
11,419 | 5,243 | 5,568 | 11,546 | 3,007 | 4,547 | 11,876 |
worker数=1で比較するとNgixがダブルスコアで圧勝ですが、 node-static, st に関しては、cluster を使えば Nginxとほぼ同等の速度が出ています。
NodeがNgixよりも遅いのは、CPUの使用率が100%に張り付く、つまりCPUボトルネックで速度がでていない。 ただし、CPU利用率は Nginx より高かったことは注記しておきます。
HTML
Node公式サイトをindex.html (8,550 byte)を取得
gzip 圧縮なし
Nginx | http | node-static | node-static (cluster x4) | connect | connect (cluster x4) | st | st (cluster x4) |
---|---|---|---|---|---|---|---|
12,385 | 4885 | 4,975 | 12,297 | 2,921 | 8,982 | 4,268 | 12,401 |
画像と傾向は同じ。
Total transferred: 877906604 bytes HTML transferred: 855101360 bytes
gzip 圧縮あり
http, node-static, connect は非対応なので省略。(connect.compress()はcluster x4でも遅かったので省略)
Nginx | Nginx (worker x4) | st | st (cluster x4) |
---|---|---|---|
5,543 | 15,975 | 4,425 | 12,232 |
Total transferred: 345644803 bytes HTML transferred: 320434219 bytes
gzipありだと NginxがCPUを100%使いきり、速度が大きく低下しました。worker を増やすことでパフォーマンスは改善できますが、VPSなどのCPUのコア数が少ないサーバでgzipを有効化すると逆にパフォーマンスが低下する可能性があることを示唆しています。stは既にgzipなしの時点でCPUボトルネックが発生していたので、gzipなし、ありで性能差が殆ど無い、という面白い結果になりました。gzip圧縮ありだと転送量がgzip圧縮なしの半分以下となりでネットワークに優しく、全体的なスループットの向上に貢献しそうです。
まとめ
- Node.jsの静的コンテンツの性能は、同一のworker数のNginxの約半分だということがわかりました。たしかに、Nginxに比べれば、Node.jsは静的コンテンツの配信には向いていないようです。とはいっても、4,000リクエスト/秒(=約3.4億リクエスト/日)は捌けるので、小〜中規模のサイトなら十分に捌ききれるレベルです。
- NPMモジュールとしては、st がお勧めです。gzip圧縮ありでも大きなパフォーマンス劣化はなく、(CPU効率は良くないですが)cluster を組めば、絶対性能としてNginx の8割程度の速度を出すことはできます。
- connect の static ミドルウェアはダントツで遅い(つまり、connectを拡張したフレームワークであるexpressも遅い)ので、express を使っているサイトは要注意です。Node.jsが静的コンテンツに向かない、と言われる1つの要因は、この最も有名なNode.jsのフレームワーク express (connect) の staticモジュールが遅いということも原因の1つなんじゃないかと、今回測定して感じました。
「Node.jsは静的コンテンツにむいていない」という言葉だけが独り歩きしていますが、実際に測ってみると同じNode.jsのモジュールでも特徴が出ていて面白かったです。ある程度以上の規模になったらNginxやCDNを使うようにした方が良いというのも事実ですが、噂に惑わされずに、ファクトベースで自分の用途にあっているかどうかを見極めていくのが良いと思います。
Tuppari コミュニティ始めました & IE に対応しました
Tuppari コミュニティについて
これまで個人で Tuppari を書いていましたが、@seratch が Ruby ライブラリ書いてくれてので、これを機にコミュニティ体制に移行しました。
Tuppari の Ruby クライアントを書きました - case class HatenaDiary(id: Symbol = ’seratch2)
具体的には、Github の Organization にリポジトリを移動し、Node.js、Java、そして今回追加された Ruby ライブラリをまとめました。
Tuppari のメンテナになりたいという方、もしくは、Tuppari の各種言語のライブラリ書いたという方は権限を追加しますので、ご連絡ください。
IE 9 対応について
Tuppari は WebSocket を使うので、原則は IE9 以下では動作しないのですが、Flash の WebSocket 実装である web-socket-js を利用して、IE9 でも動作するようにしました。
IE に対応させたい場合は、以下のように条件付き Include を使って、必要なライブラリを読み込むようにしてください。
<!-- Include the Tuppari client JavaScript libraries on your page from CDN --> <script src="http://cdn.tuppari.com/0.2.0/tuppari.min.js"></script> <!-- If you want to support IE6, 7, 8, 9 include script for flash sockt --> <!--[if lte IE 9]> <script src="http://cdn.tuppari.com/flashsocket/swfobject.js"></script> <script src="http://cdn.tuppari.com/flashsocket/web_socket.js"></script> <script src="http://cdn.tuppari.com/flashsocket/json2.js"></script> <!endif-->
デモも更新しておいたので、IEの方はお試しください。
8/17追記: IE6, 7, 8 だと動かない場合があることが分かったので取り急ぎ IE9 対応に変更しました。現在、対応中です。
- 通貨レートの Push 更新:
- GeoタグにもとづいてTweetをGoogleマップ上に表示:
Tuppari の Java クライアントを作った
Tuppari - WebSocket on Your Cloud - - Scalaとlift のはずだった ・・・
前回、Node.js で構築した Tuppari (コンセプトは Pusher クローン)を紹介しましたが、実は API 仕様が公開されていて、Node.js 以外の言語/環境でもクライアントを作ることができます。
で、これにもとづいて Java クライアントを作りました。他の言語でも書けると思うので、興味がある方は是非挑戦してみてください。そして、できたら教えて下さい。
Tuppari 公開記念ハッカソン
興味があればどうぞー。Node.js はちょっとわからないけど、Java なら任せろー、な方も大歓迎です。ご希望があれば、Tuppari の使い方、各自の環境へのインストール方法などサポートします。
使い方
maven から利用することを想定しています。pom.xml に以下の設定を記入してください。
<repositories> <repository> <id>tuppari-java</id> <name>tuppari-java repo</name> <url>http://hakobera.github.com/tuppari-java/releases</url> </repository> </repositories>
<dependencies> <dependency> <groupId>tuppari</groupId> <artifactId>tuppari-java</artifactId> <version>0.1.0</version> </dependency> </dependencies>
サンプルコード
以下の様な感じで、ほぼ Node.js のコード同じ感じで書けます。
package com.tuppari; import com.tuppari.TuppariChannel; import com.tuppari.TuppariClient; import java.net.URI; import java.util.Map; public class TuppariExample { /** * Usage: java com.tuppari.example.TuppariExample [applicationId] [accessKeyId] [accessSecretKey] */ public static void main(String[] args) { if (args.length != 3) { throw new IllegalArgumentException("Usage: java com.tuppari.example.TuppariExample [applicationId] [accessKeyId] [accessSecretKey]"); } String applicationId = args[0]; String accessKeyId = args[1]; String accessSecretKey = args[2]; TuppariClient client = new TuppariClient(applicationId, accessKeyId, accessSecretKey); // Run on you own cloud, specify the target URL. // TuppariClient client = new TuppariClient(applicationId, accessKeyId, accessSecretKey, URI.create("http://localhost:5100")); TuppariChannel channel = client.join("your_channel"); Map<String, String> result = channel.send("your_event", "hello"); System.out.print(result); } }
Tuppari - WebSocket on Your Cloud -
Tuppari とは
東京Node学園 6時限目で発表した Node.js で作られた Pusher クローンです。
簡単に言うと WebSocket を利用した大規模 Broadcast に特化したサービスです。
インフラとしては Amazon Web Services (以降、AWS)上で動かすことに最適化されていますが、AWS以外でも動かすことは可能です。つまり、クラウド上に自分自身の Pusher を構築することが可能になります。
現時点では Pusher の機能には追いついていませんが、今後も鋭意開発を進めていくので、是非使ってみてください。
あと、WebSocket を使うので、IE9 以下では動きません。
8/15 追記: v0.2.0 から IE もサポートしました。
詳細は以下の記事を参照して下さい。
Tuppari コミュニティ始めました & IE に対応しました - Scalaとlift のはずだった ・・・
Tuppari 公開記念ハッカソン
興味があればどうぞー。
ご希望があれば、Tuppari の使い方、各自の環境へのインストール方法などサポートします。
ソース
CLI & npm モジュール: https://github.com/tuppari/tuppari
デモ
- 通貨レートの Push 更新:
- GeoタグにもとづいてTweetをGoogleマップ上に表示:
試してみよう
Tuppari は各自のインフラで動作させることを想定していますが、インストール済みサンプルとして、 https://api.tuppari.com でサービスを稼働させてありますので、まずはお試しください。
公式ドキュメント: https://github.com/tuppari/tuppari/wiki
アカウントの作成
まずは、アカウントを作成します。
アカウント名は他人と同じものは使用できません。
tuppari register [YOUR_ACCOUNT_NAME] Password: [ENTER YOUR PASSWORD] Create new account name "[YOUR_ACCOUNT_NAME]" success.
ログイン
アカウントを作成したらログインします。
tuppari login Account Name: [ENTER_YOUR_ACCOUNT_NAME] Password: [ENTER_YOUR_PASSWORD] Login success.
アプリケーションの作成
tuppari create [YOUR_APPLICATION_NAME] Create a new application named "[YOUR_APPLICATION_NAME]" success. Following is the application Information to access from your code. { "applicationId": "[SHOW_YOUR_APPLICATION_ID]", "accessKeyId": "[SHOW_YOUR_ACCESS_KEY_ID]", "accessSercretKey": "[SHOW_YOUR_ACCESS_SECRET_KEY]" }
ここで出力される情報は後で使うので、メモしておいてください。
HTML ファイルの作成
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <!-- 1. Include the Tuppari client JavaScript libraries on your page from CDN --> <script src="http://cdn.tuppari.com/0.1.0/tuppari.js"></script> <script> // 2. Instantiate connection object to tuppari server. var client = tuppari.createClient({ applicationId: 'YOUR_APPLICATION_ID' // Replace this with your Application ID. }); // 3. Create channel you want to use. var channel = client.subscribe('your_channel'); // 4. Listen for some events on you channel channel.bind('your_event', function (data) { console.log('An event was triggered with message: %s', data); }); </script> </head> <body> </body> </html>
Node.js のコードからイベントを発行する
package.json と app.js を以下のように作成します。
package.json
{ "name": "helloworld", "version": "0.1.0", "dependencies": { "tuppari": "0.1.0" } }
app.js
var Tuppari = require('tuppari'); var tuppari = new Tuppari({ applicationId: 'YOUR APPLICATION ID', accessKeyId: 'YOUR ACCESS KEY ID', accessSecretKey: 'YOUR ACCESS SECRET KEY' }); var channel = tuppari.join('your_channel'); channel.send('your_event', 'hello world', function (err, res, body) { if (err) { console.error(err); } console.log(res.statusCode, body); });
ファイルを作成後、以下のコマンドで実際のイベント発行します。
cd [YOUR_WORKING_DIRECTORY] npm install node app.js
ブラウザのコンソールに以下の文字列が出力されていることを確認してください。
An event was triggered with message: hello world