Block Rockin’ Codes

back with another one of those block rockin' codes

Node におけるスケールアーキテクチャ考察(Scale 編)

[追記]

途中までは Node での複数プロセス起動、プロセス間通信等について書かれていますが、後半は自分が前回の記事 を書くにあたって自分が考えてたことを少し強引に広げて書いた個人的な妄想が多く含まれ、Node におけると言っときながら、後半は Node 関係ない感じになってしまいました。
正直まだ分かっていないことが多いです。変なところをどんどん指摘していただけるとむしろ嬉しいです。



Node におけるスケールアーキテクチャ考察(SSP 編) - Block Rockin’ Codes の続きです。

もともと何となく結論があって書き始めたんですが、書きながら色々調べているうちによくわからなくなりました。
まだまだ調べたらないことがわかったので、とりあえず今わかっているところまで書きます。
結局何がいいたいのかよくわからない感じかもしれないけど、ゴールは SSP のバックエンドの Node をスケールさせることです。

おさらい

前回は、Node に合いそうなアプリケーションの実装として、 Ajax/WebSocket を用いた SSP なアプローチがようさそうだというところまで書きました。
つまり実質 Node がやることは、 RESTful JSON API の公開です。



Node は単一のプロセスでサーバを稼働させることができますが、同時にサーバの処理の限界がプロセスの限界である事を意味します。
そこで今回は、この RESTful JSON API を Node で実装し、かつそのサーバをスケールさせるためにどのような方法があるかを考え、
本当に SSP は水平分散させやすいのかを考えてみます。

Node の HTTP サーバ

簡単に振り返ると、 Node は単体で HTTP をパースすることが出来ます。
これは Node で書いたアプリケーションの実行に、 Apach や Nginex へのデプロイを必要としないということです。
シングルプロセスで稼働するために省メモリである事は前回述べました。


しかし、昨今のサーバハードウエアは、多くのマシンがマルチ/メニ―コア環境を持っています。
これら複数コアを "使い切る" という観点では、Node シングルプロセスモデルは不利になります。


こうした問題への解決策の一つに、プロセスの複数起動というアプローチがあります。


まず、 Node 本体の公式サイトには、マルチコアの有効活用に対する FAQ の回答として以下のように書かれています。(翻訳)

「将来のバージョンで、Nodeは現在のデザインととてもよくフィットする形で子プロセスをforkする ( Web Workers APIを使って) ことができるようになる予定です。」

子プロセスをforkしてプロセス間通信を実施するモデルです。


Node本体にこの実装はまだされていませんが、同じアイデアで WebWorker を使いプロセスを fork する実装に pgriess/node-webworker · GitHub がありあます。
また Tj 謹製の Cluster - extensible multi-core server management for nodejs も複数のプロセスを起動することができます。(いずれも詳細は後述)


プロセスを複数起動して、必要があればプロセス間通信でやり取りすることが、 Node のマルチコア環境の有効活用に対する一つの答えです。


プロセスの複数起動すると以下のようなメリットがあり Node のプロセスを複数起動するための方法もいくつか考えられています。

  • マルチコア環境の有効活用
  • 単一スレッドのイベント総量のオーバー対策
  • プロセスのフォールバック
  • ファームの実現

プロセス間通信

プロセス間通信のプロトコルにつては、いくつか選択肢があります。


例えば node-webworker では WebSocket を使っているし、
cluster では JSON-RPC を使用しているようです。


現状 Node で WebSocket と言えば Socket.IO ですが、 Socket.IO はクライアントにブラウザを想定したフォールバック機構(Flash, Long Pooling etc)を持っているため、
プロセス間通信には使用しません。プロセス間通信に WebSocket を使う場合は

の組み合わせが良さそうです。node-webworker でも内部ではこれを使用しています。
シリアライズされたデータを双方向通信する際に、クライアントとも共通して使える点で WebSocket は使いやすいかもしれません。


データ形式JSON で良いでしょう。他の言語では message-pack を使った方がデータサイズ等の点で良いかもしれませんが、
Node の場合は JS である以上 JSON がネイティブサポートされているし、汎用性もあるのでシリアライズした JSON で十分だと思います。


ちなみに先の Node 自体のプロセス間通信の実装の際には、TCP の使用を検討していると Ryan 本人は言っているようです。
(それ以上のことはわかりませんでした。)


現時点では独自にプロトコルを考えようとするより、既存のものを応用する方が良いような気がします。


ロードバランサとリバースプロキシ

プロセスを複数起動し負荷を分散させる場合は、フロントにロードバランサやリバースプロキシ等を置くことで、処理を各プロセスに割り振れます。


ここで重要になるのは、フロントに立つサーバが

  • WebSocket を通す必要がある
  • イベントドリブンが望ましい

です。


WebSocket は SSP には必須です。現時点では WebSocket を通さないプロキシ等も多いので注意が必要です。
そしてサーバの構成によりますが Node で実装したサーバがイベントドリブンでも、フロントサーバがそうでない場合、 Node の良さが生きない可能性がある点も注意が必要です。



まず二つの用件を満たす既存のサーバとしては WebSocket のパッチ(未検証)をあてた Nginx があげられそうです。
Nginx なら重みづけしたロードバランスとドメインベースでのリバースプロキシが設定できますし、Nginx 自体に静的コンテンツを配信させることもできます。
実績も積んできた Nginx をフロントに置くことは信頼性の面で歩がありそうです。
また個人的な予想では WebSocket の仕様さえ安定すれば、 Nginx は早い段階で WebSocket に対応しそうな気がしています。


Node の実装では、 Cluster は複数プロセスを起動し、
各プロセスへの処理を振り分ける事でロードバランスができます。


リバースプロキシとしては、Nodejitsu の nodejitsu/node-http-proxy · GitHub は、WebSocket や HTTPS も含めて対応しており、 http://www.nodejitsu.com/ で実運用されています。
nodejitsu は多くの実践的なプロダクトを公開しているため、注目に値します。



静的コンテンツサーバ

RESTful JSON API サーバとは別に、土台となる静的コンテンツのサーバも必要になります。静的コンテンツサーバももちろん Node で実装することができますが、既存の実績のあるサーバに委ねるのも良いアプローチだと思います。


特に SSP の場合は、静的コンテンツの更新頻度が非常に低く、土台となる HTML ファイルでいえば、その数もおそらく少ない事が想像されます。
すると、クライアント、サーバ共に積極的なキャッシュ機構を組み込みやすくなります。


静的サーバを Node と別に用意する場合は、Apache より Node と同じイベントドリブンである Nginx を置くのは相性がいいようです。
そして Nginx のキャッシュ、もしくは Squid 等を前におきメモリキャッシュを生かすこと。if-modified-since, expire, cache-manifest などを用いたブラウザキャッシュも考慮に入れます。


メモ: Squid は CARP(Cache Array Routing Protocol)でバランス
http://d.hatena.ne.jp/hideden/20091101/1257061316

セッションの共有

スケールさせた複数のサーバでセッションを共有する必要がある場合は、プロセスが確保したメモリとは別に、セッションストレージを用意するアプローチが有効です。
この場合応答速度を重視して memcache, TokyoTyrant, Redis 等といった NoSQL を用いることが多いようです。

memcached
  • 揮発性
  • expire 対応
  • 一番実績があり運用ノウハウも多く出ている。
TokyoTyrant
Redis


それぞれ長所/短所今ありますが、今回の用途では Redis が良いのではないかと考えています。
これは十分な速度の上に(一定のタイミングでの?)永続機構があり、標準機能である Pub/Sub はプロセス間通信に使える(後述) という点を評価します。

Express ならミドルとして connect-redis が使用できるので session-store としての導入も楽です。

WebSocket 共有

スケールさせた場合、 WebSocket クライアントが別々のサーバにコネクションを張る可能性があります。
すると、例えば Socket.io の場合 broadcast() 相当のことができなくなります。
そこで、 WebSocket を共有できるようにするために、プロセス間でメッセージを共有させる必要があります。

これには二つのアプローチが考えられるようです。

  • Pub/Sub を用いたプロセス間通信
  • 共有メモリの実装


前者が Pub/Sub を用いて、各プロセスが自分に関連するメッセージを Subscribe できるようにする方法です。
実装としては Redis に備わっている Pub/Sub 機能を用いることができます。


後者はそのまま共有メモリ空間を設けて、そこで情報を共有する方法です。
実装方法としてはタプルスペース(TupleSpace)というアーキテクチャがあって、
それが良さそうなのですが、詳細はよくわかっていません。


おそらく難易度的にも Redis の Pub/Sub を用いる方法が良いでしょう。


またここで Express に Socket.IO を組み合わせる場合は、近い将来(Socket.IO v0.7以降)に Express.IO が公開されれば Express - Socket.IO 間でセッションを共有できます。

これはリアルタイムな更新を伴うアプリケーションを SSP で開発する際(つまりセッションを保って画面遷移もしたい)に非常に大きな力になるでしょう。


CPU 処理とファーム

前回の話とは逆になりますが、レンダリングといった CPU バウンドな処理も Node で行いたい場合は、
複数のプロセスを起動し、メインのプロセスからプロセス間通信等を通して、その処理を委譲してしまう方法があります。


この方法に関しては、2011 年の JSconf で Joyent の Tom Hughes-Croucher (Node 本/Oreilly の著者)が興味深い発表をしています。

「多層 Node アーキテクチャ
Multi-tiered Node Architectures - JSConf 2011


これは Node のプロセスを複数起動した「ファーム(コンテキストによってはプールと呼ばれるものと思う)」を構成し、重たい処理は専用のファームに振ってしまうという考えです。


情報の共有はプロセス間通信で行い、プロセス間通信自体は依頼する側から見れば非同期処理なので、依頼側のメインループは止まらない。おもしろい考えだと思います。
(スライドに文字で載っている以上にいろいろな事を言っていたので、自分が聞き取れなかった説明もあります。ビデオが公開されたら、頑張って聞きたい。)


ちなみにこの発表では実装案として cluster について言及がありました。


SSP とスケール

では、 Node のアプリをスケールさせるのに必要そうなモジュールやらは一通りありそうです。次は実際に SSP をスケールさせる際の構成を考えてみます。


まず、再確認ですが SSP での node の立ち位置は、 RESTful JSON API の公開です。
この API はアプリケーションを成り立たせるために、リソースに対する CRUD をサポートする必要があります。


リソースの永続化は RDB よりも NoSQL の方が良さそうです。そもそも RESTful API でのリソース操作には RDB の細かい機能はあまり必要としません。
というかあまりそういう機能に頼らず、実装する方がスケールさせやすです。


昨今の NoSQL の持つ Replication や Sharding 機能を用いた分散構成を考えています。
また、 ドキュメント指向のストレージの場合、 JSON をほぼそのまま格納できることが多い点もメリットになります。

プロセスごとのサービス

複数のプロセスを起動して、そこに永続領域に対する RESTFul JSON API を公開したサービスをたてる形でサーバを構築すると、いくつか方法が考えられます。
自分のイメージとしては以下の3つのような感じになるのかと。

構成1
  • データを ID のレンジで区切ってサーバに配置する。
  • プロセスにはレンジごとの CRUD を用意しておき、 memcache 等でキャッシュ可能にしておく。
  • レンジに応じた URI を設計し、プロセスにはレンジに応じてリバースプロキシで分散する。
  • レンジごとのデータに対応したストレージが用意できるのであれば、サーバはたぶん何でもいい。


  • 長所
    • API サーバが対象とする範囲が狭いので、キャッシュが一番効きやすい。
    • たぶんどんな DB でもできる。
  • 短所
    • ID ごとに区切るため自分でそのデータの分散を管理しないといけない。
    • データの性質によっては各サーバへの負荷が一定とは限らない。
構成2
  • データをシャーディングする。
  • プロセスには全ての CRUD を用意しておく。
  • 処理は Loadbalancer で分散する。
  • シャーディングの管理は DB に任せる。



  • 長所
    • ほぼ Mongo を使う構成で、 DB の管理は mongo に任せられる。
    • その後のスケールもさせやすい。
  • 短所
    • プロセスが対象とするデータ範囲は実質全体なのでキャッシュ効率は下がる
    • mongos にあたる部分の SPoF 対策が必要。
構成3
  • データをレプリケーションする。
  • プロセスには全ての CRUD を用意しておく。
  • 処理は Loadbalancer で分散する。
  • データの同期は DB に任せる。


  • 短所
    • プロセスが対象とするデータ範囲は実質全体なのでキャッシュ効率は下がる
    • サーバ間の同期が常に必要。Eventually Consistent を許容しないと行けない。
    • リアルタイム Web のバックなのに、その都度サーバ間の同期がネットワークを走るモデルはどうなのか。


couch や riak にはそれ自体が RESTful な API を持っているのでそのままでもたたける。
しかし、 WebSocket や 認証やセッションを考えると、直で叩くより裏に置く方が良いのかなと思う。
上の中では構成2が現実的なのかな、その用途では Mongo が一番相性がいいのかも。

まとまらないまとめ

アプリを SSP で実装した場合のバックエンドの Node による RESTFul JSON API のスケールについて考えました。
最終的には、色々と複雑な構成になった気がしますが、スケールアウトを考えると構成が複雑になるのは、 SSP に限らずそうだと思います。
実際の大規模サービス等の運用の場面では、もっと複雑なことが行われているだろうし、今回はそういった点は目をつぶっています。
プロセスの複数起動、プロセス間通信、 WebSocket の共有、 NoSQL による Sharding / Repliaction など、一通りのネタはそろいました。
そしてこれ以上は実際にやってみないとよくわからない。しかしこれから試してみるべき方針はなんとなくわかってきた気がします。


あと、前回はキャッシュに色々こだわって、SSP の裏に実装する JSON 形式のリソースは、キャッシュが容易だろうと考えていたけど、
本当にそうかよくわからなくなってきた。。
キャッシュをふんだんに取り入れるのは、それだけキャッシュのライフサイクル管理が大変になるのも事実です。
この辺は、もう少し考え直そう。

終わりに

なんとも煮え切らない結果になってしまった。
自分がよくわかってないところが多すぎて、たぶんわかっている人から見るとしょうもない内容になっていると思います。
ずっと考えててもしょうがないので、いったんここで出します。
続きはもう少し経験を積みながら考えて行こうと思います。
なんかちょっとでもフィードバックがあったらもらえるとうれしいです。


この記事は、なにかわかったら修正していくかもしれない。
そして、まとまってきたら、別で書くかも。