Node におけるスケールアーキテクチャ考察(SSP 編)
*息抜きがてら書いていたら長くなってしまった。。
*当たり前ですが、あくまで個人的な考えです。
*ころころ変わるかもしれません。
Node の基本的な知識についての話は色々なところで出始めて、
じゃあこーいう場合はどうするの? みたいな話が出始めたりもするようになってきた気もします。
正直、自分にもまだ分からないことだらけです。
そもそも自分はそこまでスケールに関するアーキテクチャや、OS の低レイヤに精通しているとは言えないので、
これを期に Node は何が得意で何が不得意なのか、スケールさせるために考えないといけないこと、などを自分なりにまとめて、
ついでに、これまで学んできた周辺のアーキテクチャに関する知識も混ぜて、色々思考実験をしてみたいと思っています。
だから WebSocket にブラウザが対応してないとか、そんな複雑なサーバ群本当に運用できるのかとか、
そういう話は無しに、とにかく考えてみようと。
とりあえず今の時点では
- Node に合うアプリケーションと実装
- Node に合うスケールアウトとその方法
について書きたいと考えています。
後半がまだ書けないので、まず前半だけ出します。
特に大したことは書いてありません、今まで自分がやってきたことを、Node と合わせて考え直しただけです。
スケール云々の前提として、どういう方向でアプリが作れるかという話。
(色々書いて、理解が深まったらこのエントリを修正したり、part2的なエントリやら修正エントリを書くかも。)
Node の前提知識
Node っていうと必ず最初に出てくる、以下キーワード関連の基礎知識は省略。
あと
- CSJS
- Client Side JavaScript
- SSJS
- Server Side JavaScript
- CSDB
- Client Side DataBase(localStorage, IndexedDB etc)
- SSDB
- Server Side DataBase
- SSP
- Semi Single Page (Approach|Architecture)
Node の得意なこと、不得意なこと
Node は他の言語で行っている大抵のことができますが、特化しているのはサーバ周りの実装です。
特に Web アプリ等が手軽に実装できるのですが、本当に得意なのは何なのかを、もう少し考えてみます。
Node が最も得意とする点は、I/O の処理だと考えています。ここで言っている I/O は主に "ネットワークI/O" と "ディスクI/O" といった 比較的重い I/O。
対して最も不得意な処理は CPU バウンドな処理だと考えます。理由は Node がシングルスレッド上のイベントループで処理を実行するためです。
Node に I/O 処理が要求された場合、この処理は非同期に行われるから I/O している最中は後続の処理を継続できる。
しかし、I/O を伴わないCPU バウンドな処理は非同期には実行されないため、イベントループが止まってしまい、以降の処理がブロックされる。
(もちろん回避策は無くはないけど、基本の話)
これまで主流だった、同期 I/O で処理する場合、I/O の処理が重たい場合は後続の処理が止まります。
Web アプリ等の場合は、クライアントの I/O をすべて一つのスレッドに並べて処理することは難しいため、
多くの場合はスレッドを複数起動する事でこれを解決してきました。
スレッドを複数起動するとそれぞれがメモリを消費するし、スレッドを起動し続けても限界点として C10K なんて問題もあります。
Node は、沢山の I/O 処理が来ても非同期でノンブロックな処理ができるし、たった一つのプロセスで処理しているためメモリの消費が抑えられる、 C10K に対する耐性もある。
(ベンチは見つからないけど、このシングルプロセスが処理できるイベント(= I/O)の総量にも限界はあるはず。)
これが主に重たいネットワーク I/O が増えがちなリアルタム Web や、ファイルアップロード/ダウンロードとの相性が良いとされる理由の一つです。
CPU 処理を I/O に振る
これらをふまえると、Node では CPU 処理ではなく、I/O 処理が多発する場面での活躍が期待できます。
ネットワークの I/O とディスクの I/O に強い事は、ディスクから取り出したデータを HTTP でクライアントに渡すという Web アプリで効果を発揮すると言えます。
手軽に強力なサーバを実装できる、というのは Node の一つのモチベーションにもなっていて、事実 HTTP 周りの API はかなり充実しています。
正しく生かしていくためには、 I/O 処理 > CPU 処理 な特性を把握することが重要です、時には、これまで CPU で処理してきたものを、
I/O の処理に振ってしまうという考え方もありだと思います。
Node とテンプレートエンジン
たとえば、テンプレートエンジンを用いてデータを HTML や XML に加工しようとすると、そこには CPU バウンドなレンダリング処理が入る事になります。
これは従来の Web アプリでは基本的な考えだったけど、 特に haml, jade, stylus 等の抽象度の高いテンプレートエンジンはレンダリングにそれだけ演算を必要とするため、
性能面で見れば Node との相性がいいとは言い切れないと思います。
ここで、先ほどの CPU 処理を I/O に振る考え方を適応すると、シングルページなアーキテクチャが見えてきます。
つまり、Node はディスクから取り出したデータを JSON 等で配信するサービスを提供し、クライアントは非同期(Ajax, WebSocket)にそれを取得して画面を構築します。
画面のレンダリングをクライアントに振ること自体は、昨今のクライアント(PC, スマホ, テレビ etc)のスペックであれば大きな問題にはならなくなってきました。
レンダリングしたページの配信に比べると、Ajax や WebScket を多用するアプリケーションには、コネクション数の増加の問題が懸念されるかもしれません。
しかし、それはサーバから見れば、CPUバウンドなレンダリング処理を、多数のネットワークI/Oに置き換えた形になっています。Node には、その方が良いのではないでしょうか?
ところが全てを本当に単一のページで行うのは限界があると考えているため、ある有効な点でページは遷移させます。
また、実際は全てを Ajax やら WebSocket で行うことが非効率な場合も有るでしょう。多少は埋め込むことも有ると思います。(埋め込むと後で言うキャッシュの問題がでるけれど)
それでも従来に比べて圧倒的に遷移が少ないため、ここでは仮にセミシングルページ = SSP (アプローチ|アーキテクチャ) としておきます。
次はこの SSP の特徴をもう少し見てみます。
SSP=Semi Single Page(Approach|Architecture)
比較のために、サーバで画面(HTML)を動的に構築してレスポンスとして返すスタイルのアーキテクチャの特徴を振り返り、以下の二点に注目します。
- ページの構築で同期する。
- ページ自体をキャッシュしにくい。
画面というリソース形式の限界
Web ではサーバに対するリクエストをインプット、レスポンスをアウトプットととらえるのが標準的で、
細かいオーバーヘッドを無視した場合、アウトプットが複数の I/O を伴う場合の合計時間は、同期・非同期の場合以下のようなイメージになります。
- A,B,C
- ディスク I/O
- X
- レスポンスできる状態のリソースが完成した点
同期I/O sum(A,B,C,...)+render
A---- | B--------- | C---- | render---X -> HTTP response
非同期I/O max(A,B,C,...)+render
A---........ | B---------+ render---X -> HTTP response | C------.....
*あくまでイメージです。
HTML画面というアウトプットを伴う場合は、レスポンスの時点でレンダリングが終了している必要があり、
レンダリングの時点で必要なI/Oが終了している必要があります。(非同期なレンダリング等は除く)
これでも、非同期I/Oによってレンダリング終了までの時間が「短くなる可能性」があることは分かりますが、
先ほど言った「バウンドなレンダリング処理はしたくない」という考えから、これも Node を生かし切れているとは言えません。
ということで、アウトプットは HTML ではなく JSON にしてしまいます。
実は Node の多くの DB や File 関係モジュールの API は、アウトプットが大体 JSON なので、それぞれに URI を振って、RESTful JSON API を実装するためのコストは比較的低いです。
ベースとなる HTML ファイルは別途配信しますが、これはレンダリングを伴わず( haml や sass とかでもあらかじめレンダリングしておく)配信できます。
(renderd)HTML-* -> HTTP response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A----X -> HTTP response B---------X -> HTTP response C----X -> HTTP response
クライアントはこれらを順次 Ajax や WebSocket で受け取って DOM 操作で画面を構築することになります。
リソースのキャッシュ
レスポンスを画面にすると、構築にコストがかかるくせにキャッシュしにくいという問題が常に付きまとい、 Web アプリケーションフレームワークは長いことこの問題と闘ってきました。
多くの場合はセッションが保持する情報の一部(ユーザ名とか)が画面に反映されているために、キャッシュできるほど汎用性が無いのが問題です。
たとえば Rails3 では現在三つのキャッシュ機構を提供しています。
- ページキャッシュ
- アクションキャッシュ
- フラグメントキャッシュ
ページキャッシュが画面全体のキャッシュにあたり、先の理由からこれが活躍する場面は非常に限定的です。
一番多用するのは、一部だけをキャッシュしておくフラグメントキャッシュだと思いますが、これも実用的ではあっても、割と苦し紛れの方法な気がします。
RESTful JSON API で用意されたサービスが返すデータは、 DB から取り出した後、多少手を加えただけのもので、
これをキャッシュするのは非常に簡単だし、 HTML と比べると容量も小さい。各サービスを提供する Node プロセスに対して memcache を付ければそれだけで効果を発揮します。
この時点ではまだ、 DB が KVS であるかどうかも関係なく、URI がきちんと設計できているかによるでしょう。
また JSON でやり取りしていれば、送信先のクライアントで、IndexedDB 等を用いたキャッシュも視野に入れられます。
CSDB を用いたキャッシュは、そこに対して CRUD までしてしまうような CSJS を組むことで、システム全体でみた場合バッファの役割にもなります。
オフラインでの動作と、リクエストの減少、クライアントデバイスのリソース有効活用等、今後は CSDB をどう使うかがかなり重要になってくると考えています。
逆にシングルページの土台となる HTML, JS, CSS といった静的なファイルの配信については、 Apache や Nginx といった信頼性のあるサーバに譲る方がいいかもしれません。
また動的なデータを担う JSON を除いた .html, .css, .js といった静的ファイルは、文字通り静的リソースでありキャッシュ可能です。
squid 等のサーバのメモリに載せてしまえばいいし、If-modefied-since や cache-manifest を通じて、クライアントにキャッシュすることもできます。
キャッシュ可能性を高めることは、Web の高速化の常套手段です。
これはリソースからアプリケーションの状態を極力排除することで、より効率的に得られます。
SSP ではアプリケーションの状態はクライアントで管理し、クライアントがその状態を表現するために必要なリソースをサーバにリクエストします。
サーバがレスポンスするリソースの形式は「リソースの状態」を JSON という必要最小限の方法で表現しているため、キャッシュが効きやすいのです。
関連
- HTML5のapplication cacheがつかえない件 - (ひ)メモ cache-manifest については僕も勘違いしていました。
- Jxck's OutPut - Web SQL Database についてボヤく Web SQL DataBase があったら、クライアントでJOINしまくりだったのに。。
- Jxck's OutPut - キャッシュ主体のアプリケーション
リソースのビューとリアルタイム Web のクライアント
忘れてたのではっきり書いておきますが、こうした実装が必要なアプリの代表としては、リアルタイムな Web なのが自分の中で暗黙の了解になってました。
普通にトランザクションガチガチな CRUD をする業務アプリをこんな風に作る必要は無いでしょう。
そしてリアルタイムなアプリケーションは、クライアントがリッチになりがちです。
サーバは JSON API を用意すればいい、そしてキャッシュしましょうといういつもの話になるだけですが、
ここまでで、結果的には Node の得意な部分に合わせて、都合の悪い以下の部分をクライアントに押し付けた形になります。
- 画面構築
- アプリケーション状態管理
これによりクライアントでは、サーバから押し付けられた色々な処理を作りこまないといけなくなります。
これらのノウハウは CSJS な分野で色々たまっているかとは思いますが、簡単ではないですね。
ただ、経験的には、「既存のシステムの一部を Ajax 化」なんてやってだんだん複雑になっていくよりも、
最初から「全てクライアントでやる」と割り切ってスタートした方が、色々と覚悟ができていいような気もします。
現在こうした SSP を本気で構築するために必要な技術としては、以下のようなものがあるかと思います。
- WebSocket(NodeならSocket.IO)
- PushState(pjax)
- WebStorage(localStorage,IndexedDB)
- WebWorker
- backbone.js(CSMVC)
- underscore.js(utility)
- jQuery(DOM)
WebSocket ベースでデータをやり取りしたデータを IndexedDB に保存し、
backbone.js 上で処理し、DOM は今まで通り jQuery で操作しながら、
必要に応じて PushState を挟むみたいなのが最先端のトレンドになるんでしょうか?w
状態管理にセッションが皆無とは行かないとおもいますが、 Socket.IO は将来的に Express とセッションを共有できる予定なので、その辺を使って行くのが良いかと思います。
またここにあがった技術は、ブラウザの実装どころか、仕様すらままならないものもあります。しかしリアルタイムな Web を作っていく上で、レガシーブラウザの非対応はほぼ必須用件になるでしょう。それなら、クロスブラウザより先を考えた方が有益と思います。 Socket.IO は優秀なクロスブラウザライブラリをもつけど、それだけで解決する問題ばかりではないですし。
いずれにせよ、かなり規模の大きいJS開発になると思います。
Node はサーバ・クライアント両方がJSで書けるので、以下のようにノウハウや既存のリソースを再利用できたりします。
- ES5(ほぼ)対応
- 同じエディタが使える
- 同じユニットテストフレームワークが使える
- クライアント用のライブラリを移植できる。
- (バリデーション等)ロジックを双方で共有できる。
- 学習すべき言語を一つに絞れる。
ちなみに生産性を考えると CoffeScrpt も見えてきます。
(これはまだ賛否両論有るようです。)
このメリットは、規模が大きくなればなるほど、効いてくるかも知れませんね。
(そう思わない人ももちろんいるとは思います。)
パフォーマンスの考え方
もちろんですが、このスタイルのアプリケーションのパフォーマンスは AB(Apache Bench) といったツールで簡単には測ることができません。
ページの構成はクライアントで非同期に行われるため、何らかの仕組みで、最後の Ajax 通信の結果が DOM に埋め込まれるまでの時間を計る必要があるかもしれません。
しかし、 WebSocket を用いて、サーバから更新がプッシュされ順次更新されていく場合は、これも意味がなくなります。
そもそも、そこが目的で、ベースの画面をさっさと表示して、残りのデータが順次くる訳なので、
UI 設計に注意して「体感速度」を向上させ、それをうまく測らないといけません。難しいでしょう。
AB でテストするのは、各 API のレスポンスタイムで、ここは細かいチューニングが API ごとに可能かもしれません。
まとめ
全体的な欠点としては、キャッシュの更新や管理、そして SSP なアプリを実装する技術的な難易度です。
ここはもっと考えていかないといけないところですね。
また、SSP では、トランザクションを必要とするような処理が必要なアプリには向いていないでしょう。
その場合は、(画面遷移を伴う)マルチページ?な従来の方法を使うんだと思いますし、 Node でそれも不可能ではありません。
ただ、そうなった場合に I/O よりもレンダリング等の問題がネックになるのであれば、それは恐らく Node でやることではないのでしょう。
そうした処理は Rails 等に任せて行うのもよいかもしれません。
それらが共存するアプリの解決手段が Rails + EventMachine なのかもしれませんが、Node + Rails の組み合わせも検討対象かもしれません。
とにかく Node には以下のような特徴があります。
- 非同期でノンブロッキングな I/O
- シングルスレッド上のイベントループによる省メモリな処理
- ECMA5(ほぼ全)対応の JavaScript による記述
これらの意味をきちんと把握して、それを生かして初めてメリットがあります。
誤解を恐れずに言えば、自分は「Nodeは万能だ」などとはこれぽっちも思っていません。
(全ての言語、WAF、ライブラリがそうであるように)
あとがき
今回は、Node を生かすためには、 SSP なアプローチがどうもよさそうで、
そうするとレンダリングなくせるし、キャッシュもできるし、WebSocket でリアルタイムまんせーなアプリができそうだよね。
という、言われてみれば今更な話でした。
次回は、Node でそんなアプリを作る場合、サーバサイドの構成はどんなものが考えられるのか、を考えてみたいと思います。そっちが本当のスケールの話です。主にロードバランス、リバースプロキシ、マルチプロセス、Sharding した KVS を用いたスケールアウト構成についてです。
フィードバックや新たな発見が有れば、色々考えが変わるかもしれません。
今回はやや手応えが薄いですが、指摘、コメントは相変わらず大歓迎です。