GREEが悩むNode.jsの問題を考えるヒント
先日 GREEを支える大規模インフラテクノロジー」-GREE Platform Summer Conference 2012 という記事が公開され、GREEのCTOの藤本さんが、
javascriptをサーバーサイドでも使うケースが多くなってきていて、必然的にnode.jsを使うことになるが、大きく3つの問題がある。
- ひたすらすごい勢いでバージョンアップしているので安定しない。コストを払ってついていく覚悟を持って取り組んでいる。
- メモリリークがあるので、サーバを起動しっぱなしにするとメモリが食いつぶされる。
- コードをデプロイしても再起動しないと読み込まれない。
(中略)
これで絶対大丈夫という解決策がなくて、node.jsで一番悩んでいる。これでバッチリ解決するというものがあれば、是非教えて欲しい。
といった話が掲載されていました。
GREEさんに限らず一般的に Node に対して同じ問題意識を持ってらっしゃる方も多いのではないかと思います。
これら3つの問題を考える上で(さすがに「これでバッチリ解決」とはいきませんが)、どのようなアプローチがあるか「考えるヒント」みたいなものを私なりに書いてみたいと思います。
Disclaimer
なお GREEさんの指摘は、私がここで書いたことを既にきちんと検討した上でのご意見かもしれません。しかも実際に直面している問題について私は具体的な事を全く知りません。あくまでGREEさんを始めとした一般的に指摘される Node の問題点を考えるヒントとしてお読みください。
では3つの問題点に対して一つ一つ考えていきます。
1. ひたすらすごい勢いでバージョンアップしているので安定しない。
以前まとめた Node の歴史表を以下に載せます。
これ見てわかるよう、Nodeの安定版(偶数バージョン)は6〜9ヶ月の間隔でリリースされています。
現在(8月9日時点)の最新の安定バージョンは 0.8.6 で、ほぼ毎週マイナーバージョンアップされていることになります。
この頻繁なマイナーバージョンアップはある意味意図的に行われているようです。 安定版では新機能の追加やAPIの変更などは行わず、バグフィックスが中心としたリリースです。1週間の間にある程度バグ修正がコミットがされているなら、定期的に修正版をリリースしてユーザにできるだけ安定している Node を提供しようという意図がうかがえます。(実際に isaacs に聞いていないのでわかりませんが…)
なので安定版をサービスに採用しているなら、バグにぶち当たらない限り毎週のリリースに追従する必要はありません。 API 仕様の変更がないので数バージョン飛ばしても動作がおかしくなるリスクは少ないです。サービスのメンテナンス計画に合わせてバージョンアップを行えばいいと思います。(といっても数か月のスパンですが…)
ただし issue を報告する際は最新安定版での動作挙動を尋ねられますので、最新安定バージョンが適応できる環境は常に整えておかないといけないでしょう。
次に 0.6から0.8のように安定版のバージョンアップにはどう対応すべきでしょうか?
0.6の後半のバージョンからマニュアルに "Stability Index" という項目が追加されています。これは各API仕様の安定度がどの程度かというカテゴリーの説明です。 1:Experimental, 2:Unstable のインデックスのついたAPIは今後変更される可能性が高いです。
このAPIのカテゴリー化は始まったばかりで、今後どこまで厳格に適応されるかまだ不明ですが、使っているAPIに Experimental や Unstable とカテゴリーされているものが含まれていれば github master での機能変更に注意するといった対応が必要です。
Nodeの開発は github 上で行われているので非常に透明性が高く、ほとんどの場合公開の場でコードレビューが行われています。どこか別のところで大きな開発が進み、知らないうちに突然巨大なコミットが適応される、ということはありません。github repository を watch していれば、安心して最新版についていけます。
とはいうもののの頻繁にバージョンを上げる Node に不安を抱くのも仕方がないと思います。進化とのトレードオフなので、藤本さんが述べられているよう割り切るしかないです。
2. メモリリークがあるので、サーバを起動しっぱなしにするとメモリが食いつぶされる。
Nodeでメモリリークの issue が報告されると、原因を特定して修正するまでは結構やっかいな作業になります。(これは別にNodeに限らないと思いますが…) その際、必ず次のステップが求められます。
これを行ういくつかの方法を書きます。
2.1 process.memoryUsage() の推移をみる。
process.memoryUsage() は稼働中の Node プロセスとV8のメモリ情報を出力します。
> process.memoryUsage() { rss: 6909952, heapTotal: 4149120, heapUsed: 1903896 }
時間経過とともにヒープ領域の食われ具合の推移を出力してメモリのリーク度合を調べます。
2.2 ツールの出力を見る。
メモリリークの issue を登録すると、よくアドバイスされるのが valgrind です。v8 profiler も有力な情報を出力します。
- valgrindを使う方法 Node.js leaking memory? Valgrind! (ただし性能低下が激しいです。結果出力をちゃんと解析するのが難しい。)
- v8 profiler Debugging memory leaks in node.js や node-webkit-agentモジュール を使ってV8 Profileの出力結果で判断する方法 (現状node-v0.8では動作確認できませんでした。V8プロファイル出力の見方が結構大変)
個人的にはメモリリーク調査の参考程度にしかまだツールを活用できていません。
2.3 メモリリークが起きていないバージョン・コミットまでさかのぼる
ホント力技ですがこれは個人的には一番やってます。
コミット前後の process.memoryUsage() の推移が極端に違うところを見つけ出し、 diff からバグの原因を見つけます。
2.4 メモリリークと勘違いしていないか確認する。
- Full GC が行われていないことをメモリリークと間違えるケースもあります。 (例:socket.ioのheartbeat時におけるメモリリークについて ) 確認方法はこのMLに返答したよう手動 gc を入れ込んで挙動の変化を見ることです。
- FAQ的な挙動でないか? 意図した挙動がメモリリークと誤解される場合もあります。 (例:Memory leak / segfault when outputting with streams in a single tick)
メモリリークの修正には王道はない、と思います。やはり地道に出力データを調べながらひたすらコードを追っかけるしかありません。
最後に繰り返しになりますが、 Node はオープンソースです。ユーザからの貢献で成り立っています。報告されていない未知の問題があれば是非 github の issue にあげていただきたいと願うばかりです。
3. コードをデプロイしても再起動しないと読み込まれない。
いわゆる "hot deploy" または "hot reload" や "no downtime restart" と呼ばれている機能です。
Node(というか一般的な JavaScript 処理系)では、その仕組み上 eval() でも使わない限り、動作中に動的にコードを読み込んで実行するのは難しい *1 と思います。(単なる私の知識不足かもしれませんが…) しかも eval() は最適化を妨げる悪玉として通常利用することが推奨されていません。
仕組み上完全なホットデプロイを実現するのは難しいので、代わりに求められるのは graceful restart (継続中のセッションを切らずに再起動を行う)の方法です。とは言っても Node は基本シングルプロセス・シングルスレッドで動作するので、永遠に接続したままのセッションが存在すればいつまでたっても再起動はできません。
しかしアプリが分散環境で動作するように設計し、ある程度セッションの接続タイムアウトを許容すれば、複数プロセス間でサービスの切り替えながら実行コードを更新することができます。
3.1 http.Server.close() の挙動の理解
gracefull restart をするための一番プリミティブなAPIは http.Server.close() です。このAPIは新規 http セッションの接続を停止するだけで既存の接続セッションは継続されます。close() 後 http セッションが全てなくなると server オブジェクトで close イベントが発生します。根本的にこの仕組みを理解することが必要です。
3.2 cluster.disconnect() の利用
node-v0.8で導入された cluster の新機能では、worker プロセスの gracefull shutdown を実現する cluster.disconnect() APIが提供されました。 (内部的には server.close() 処理に合わせて子プロセスの管理をしているだけです。)
これを使えば、従来より簡単に複数プロセスを使ったgracefulな処理がしやすくなります。
3.3 リクエスト毎に処理プロセスを変える
LearnBoost の upモジュール のアプローチがこれです。(参考記事: Staying up with Node.JS )この仕組みでは、 httpリクエストのイベント毎に複数プロセスの別ポートに接続し、 http や ws のリクエスト処理を実行します。マスタープロセスは毎回動的に割り振りを行うだけです。
新しいコードがデプロイされたら一時的に worker へのリクエスト処理の割り振りを停止し、接続のない worker はコードを再読み込みします。マクロの見た目ではホットデプロイに最も近い方法です。接続のオーバヘッドがかかり処理性能に影響する可能性がありますが、現状ホットデプロイを実現するには最も現実的なアプローチです。
フォールトトレラントな要件で Node を動作させるのはまだまだ足りないものが多いと思います。今後、様々なモジュールやフレームワークでその機能が充実していくものと期待しています。
*1: コメントで指摘していただきましたが、vmモジュールで動的に実行コンテキストを作成することができます。 ただvmモジュールはいくつか問題点を指摘されていて、近いうちに改良が加えられる予定です。 vm module correction