Node.jsはコールバック・スパゲティを招くか
近ごろ話題のNode.jsですが、その理由は以下のようにいくつかあると思います。
- イベント・ループを使った非同期処理で、同時接続クライアント数が多数となる高負荷時のスケーラビリティに優れる。急増中のNode.js向けライブラリは最初からすべてノンブロッキングであることもポイント。
- クライアントで使われるJavaScriptと同じ言語でサーバサイドのアプリも作れる。
- Google Chromeに搭載されるJavaScriptエンジン「V8」はバージョンが上がるたびに高速化していて、V8を利用したNode.jsもそれに伴い高速化している。
- パッケージライブラリの充実。「時代の変わり目ならオレにも天下が取れるかも!」と思ったかどうか、新しい物好きの人々が、盛大な勢いでライブラリを書きまくっている。
- シンプルさ。Webサーバとアプリケーションサーバ、処理系がすべて一体。ライブラリをrequireするだけで既存のミドルウェアやCライブラリも使える。
- Cloud9のような「サーバアプリ+WebSocket+ブラウザ」で動くIDEが実用レベルに近付いている。サーバ側なのでオンラインストレージやGitHubのようなレポジトリとダイレクトにつながる上に、CI環境との統合にも期待できる。ローカルマシンのIDEよりもデータセンター上の開発環境のほうが強力になる可能性がある。
2番をメリットと考える人は現時点では多数派ではないかもしれません。でも、プログラマ全体での学習コストを考えると、「C(POSIX)→C++(GUI)→Java(サーバ)」と進んできた「最もメジャーな言語」の変遷の先にあるものが、「JavaScript(ブラウザ)」である可能性は高いと思います。jQueryのように見た目やイベント処理を頑張るライブラリだけでなく、CoffeeScriptのようなものが普及すれば、ずいぶん状況が変わりそうな気もします。
さて、本題は1番です。Node.jsやNginx、EventMachine、Twistedなど、イベント駆動モデルを使ったWebサーバが注目されています。スレッドやプロセスをたくさん用意して処理性能をスケールさせるのではなく、リクエストはキューに入れて、ワーカーで処理するというアーキテクチャです。リアルタイム性が高く、WebSocketやCometを使ってストリームとしてデータを吐き出すAPIを実装するようなケースで威力を発揮すると言われています。
コールバックのネスティングでメンテ困難に
Node.jsの未来は明るいように見えますが、一方で、非同期処理を自然に記述する方法としてコールバックを多様したプログラミングモデルは、別の問題を引き起こすという指摘があります。コールバックが数百もネストし始めたときに増す複雑さにより、コードの見通しやデバッグ、メンテナンスが難しくなると、PostRankのCTO、Ilya Grigorik氏は自分たちの経験に基づいて論じています。PostRankはソーシャルネットワーク上での反応をアグリゲートして数値化するサービスです。収集データをリアルタイムに処理して、そのデータをビジネスパートナーであるメディア企業などにAPI経由で提供するのがビジネスモデルのようです。
Grigorik氏はPostRankではNode.jsとは違ったアプローチを取ったといいます。それは、Ruby 1.9で取り入れられたFiberを使うことで、実際の処理は非同期で行いつつも、プログラマには従来通りに同期APIのように見えるRack対応のアプリケーションサーバ「Goliath」(ゴリアテ。英語読みならゴライアス)を実装することでした。PostRankは2008年からGoliathの開発をスタートして、現在の実装はバージョン4に当たるそうです。最新バージョンは1年ほど稼働実績を積んで十分な安定性が得られたので、2011年3月8日にGoliathをオープンソースで公開したといいます(ブログ)。Grigorik氏によればRubyのFiberは、スレッドに比べてオーバーヘッドが小さく、メモリ使用量も少なく済むといいます。現時点でGoliathはMRI、JRuby、Rubiniusで動作していて、MRI 1.9.2+EventMachineが最も性能が良いということです。
「継続」(コンティニュエーション)はSchemeなどでは広く使われている概念・機能で、「処理の途中の状態」を変数などにバインドしておいて、処理の流れを変えて、再び処理を再開するような使い方ができるようです。RubyのEnumerableもFiberで動いているといいます。継続それ自体は並行処理のためのプリミティブではなく、例外処理や相互再帰など大域ジャンプが必要な制御構造を実装するためのプリミティブというのが私の理解ですが、非同期処理も書きやすいということでしょうか。
【追記】YARV作者のささださんから、Fiberについての私の勘違いをご指摘いただきましたので当該箇所を消しました。詳しくはコメント欄をご覧ください。いい加減な理解で書いてスミマセン……。
Grigorik氏はInfoQのインタビュー中で、「GoliathはRuby版のNode.jsか?」という質問に対して、基本的に同ジャンルだとしながらも、次のように答えています。
Goliathの3つのメジャーバージョンアップの過程で見出したのは、非同期のコード(RubyであれJavaScriptであれ)をコールバックを使って書いていると、読んだりテストしたりメンテするのが非常に難しいコードベースになってしまうということです。従って、Node.js人気を横目で見つつも、われわれは違うアプローチを取ったのです。すでにあるコードを“解きほぐす”方法を探したのです。“コールバック・スパゲティ”とRubyのエレガントさは、非常におかしな不調和にも思えましたし、正直、PostRankはJavaScriptも大好きなんですが、RubyやRubyの素晴らしいライブラリ、フレームワークを全部捨ててJavaScriptを選ぶ必要があるようには感じませんでしたね。
コールバック・スパゲティとは具体的にどういうことで、Goliathの場合どうかという解説はコードサンプルとともに、このブログエントリで読めます。
Node.jsのようなイベントループを基本としたフレームワークが時代の要請応えているのだとしたら、GoliathもRubyコミュニティで一定の支持を得ることになるのかもしれません。ただし、Goliathは高負荷が予想されるリソース公開用APIのエンドポイントなどで使うべきもので、RailsやSinatra、あるいはMongrelやThinといった既存のフレームワークやアプリケーションサーバを代替するようなものではない、ということですから、この意味ではNoSQLと似ていますね。ほとんどのケースでは従来通りの手法を使ったほうがよく、パフォーマンスやスケーラビリティの問題があるなら、NoSQLやイベントループ系のWeb・アプリサーバに目を向けるということでしょうか。
コメント
がると申します。
多分、コールバックという便利な機能「を使いこなす」か、コールバックという便利な機能「に振り回される」か、の違いなのだろう、と思います。
お道具はすべからく「使わないのが最良の使い方」だと思っているので。
便利なお道具を「その本質を見極めたうえで」「いかに最小限に、いかに的確に」使うか、がやはり肝要なのではなかろうか、と思いました。
西村賢
がるさん、コメントありがとうございます。
おっしゃることは一般論としては妥当でしょうけど、どうでしょうね。
結構イケてる(ように見える)Webサービスのストリーム系APIを
作っている現場のトップ、しかもRubyにもJavaScriptにも
精通していて、わざわざWeb層を3年かけて自前実装してきた人が、
「振り回されている」のだと考える理由が私には分かりません。
もちろん何らかのデザイン・パターンのようなものがあって、
それを踏まえれば混乱も減るという可能性はあるのでしょうけど、
それにしても多くのプログラマは、OOパラダイムや同期APIに
慣れているわけで、同様の混乱は今後起こる可能性があるわけですよね。
と書いていても、あまり具体性がありませんので、ぜひオリジナルの
ブログ投稿をどうぞ。コード例もあります。
がる
がるです。
えと…私の文章の書き方が悪かったようで、なにやら誤解を生んでいるように思いましたので、まずはその点についてお詫び申し上げます。
「結構イケてる(ように見える)Webサービスのストリーム系APIを作っている現場のトップ、しかもRubyにもJavaScriptにも精通していて、わざわざWeb層を3年かけて自前実装してきた人」が「振り回されている」とは、別段考えておりません。
# どのあたりからそのように読み取られたのかは、すみません今だによくわかってはいないのですが。
先ほどの私のコメントで念頭にありましたのは、タイトルにもあります「Node.jsはコールバック・スパゲティを招くか」と、文中にあります「一方で、非同期処理を自然に記述する方法としてコールバックを多様したプログラミングモデルは、別の問題を引き起こすという指摘があります。コールバックが数百もネストし始めたときに増す複雑さにより、コードの見通しやデバッグ、メンテナンスが難しくなると、PostRankのCTO、Ilya Grigorik氏は自分たちの経験に基づいて論じています。」のあたりです。
現実問題として「見通しも悪くかつメンテナンス性も悪いcallback系の実装」を何度か拝見しておりましたので、そのあたりを想起してました。
上述のようなことがありますので、おっしゃるとおり割合に「一般論」として、軽い話題として書いたつもりだったのですが。
# 別段、Node.jsを、あるいはJavaScriptを使っている「から」コールバックスパゲティがおきるんだ、とは思ってもいませんし。
あるいは、話の進展をさせるのであれば「なぜコールバックスパゲティはおきるのか」「あるいは、コールバックスパゲティは本当に問題なのか?(いやまぁメンテナンスが非常に困難になるので、少なくともその一点において十分問題だとは思っているのですが)」「どうやったらその問題を軽減できるのか」というあたりのお話に展開できるのであれば、あるいは「興味深い話になるのかなぁ」などとも思っていたのですが。
お気に触る内容、あるいは書き方だったようで、またその結果、気分を害させるようなことになってしまったようで、大変に失礼をいたしました。
よろしければ、コメントの削除など、していただければと思います。
西村賢
がるさん、
詳細なコメントありがとうございます。
コールバック・スパゲティの実例を元に議論しないと、
まあ意味がないですよね。というか、私にはよく分かりません。
ともあれ、私のほうこそ早とちりだったらスミマセン。
また、お気軽にコメントくださいませ。
ささだ
いくつか指摘させて下さい.
* Fiber と(Scheme などで言われる)継続は異なるものです.
* Fiber や継続のようなものを並行処理の primitive として扱うことも出来ます.
* Fiber を利用するのは Enumerable ではなく Enumerator です.また,Enumerator のすべての機能で Fiber を利用するわけではありません.
西村賢
ささださん、
ご指摘ありがとうございます! 本文を修正しました……。
アラファイブ
西村さん、 いつも新鮮な情報をありがとうございます。
そのコールバック・スパゲティの問題ですが、考えますにそれが問題に
なるのは、
PCなどで使う業務ソフトを複数人数で作っている場合で、
唯一で共用であるイベントループの使用に関する思惑が各人で
絡み合ってしまう場合か、
イベントループの使用に関して、過去の思惑と今回開発時の思惑が
かみ合わない場合かだと思います。
いずれにしましても、イベントループが唯一で共用である点が問題だと
思います。
イベントループについて実例をつまびらかにしろと言われると言えないの
ですが、もし仮に、Node.jsが“複数の”イベントループを持てるので有る
ならば議論せず解決が出来るかとも思うのですが、
Node.jsのイベントループは1つでしょうか、複数でしょうか?
(自分で開発環境を建ててみれば良いのですが、さすがに荷が重いです。)
西村賢
アラファイブさん、
node.jsのイベントループ周りの大まかな流れについては、edvakfさんの
概説が参考になるかもしれません。
http://d.hatena.ne.jp/edvakf/20101207/1291556433
src/node.ccは2000行強ありますが、V8との入出力、それに必要な
ユーティリティ関数、イベントループライブラリの違いを吸収する
抽象化レイヤとのやり取りなんかがずらずらとあって、そういうのを
のぞくと、Start関数の最後の100行にnode.jsの骨格がある気がします。
V8のコンテキストは1つだけで、evented IOは3つのスレッドプール、
で、event_loopは1つだけ起動しているように見えます。
イベントループが複数あると、むしろ複雑になる気がしますが、
どうなんでしょうね。
osiire
なんか流れ着いてきてコメントしたくなったので。
コールバックの嵐なプログラムは、継続モナドでスッキリ書けて合成できるので、スパゲティがかなり解決すると思っています。
実際、継続モナドで書いた例が次のもので、>>=の部分がイベントのネスト部分になります。
http://sourceforge.jp/projects/ngms/svn/view/trunk/webui/src/main/js/exec.ml?view=markup&root=ngms
このプログラムはOCamlですが、このコードからjavascriptを生成してブラウザ上で動かしてます。ご参考まで。
西村賢
osiireさん、
コメントありがとうございます。
確かにGoliathの作者も、「Haskellerならモナド使えばいいって
いうかもしれないけど」と指摘していました。
継続モナド……。うーん、サッパリ分かりません。
勉強して出直します! ><