Block Rockin’ Codes Twitter

2011-06-18

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

[追記]

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

正直まだ分かっていないことが多いです。変なところをどんどん指摘していただけるとむしろ嬉しいです。



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

もともと何となく結論があって書き始めたんですが、書きながら色々調べているうちによくわからなくなりました。

まだまだ調べたらないことがわかったので、とりあえず今わかっているところまで書きます。

結局何がいいたいのかよくわからない感じかもしれないけど、ゴールは SSP のバックエンドの Node をスケールさせることです。


おさらい

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

つまり実質 Node がやることは、 RESTful JSON API の公開です。


f:id:Jxck:20110619001452p:image


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 は多くの実践的なプロダクトを公開しているため、注目に値します。


f:id:Jxck:20110619001451p:image



静的コンテンツサーバ

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 を設計し、プロセスにはレンジに応じてリバースプロキシで分散する。
  • レンジごとのデータに対応したストレージが用意できるのであれば、サーバはたぶん何でもいい。

f:id:Jxck:20110619001448p:image

  • 長所
    • API サーバが対象とする範囲が狭いので、キャッシュが一番効きやすい。
    • たぶんどんな DB でもできる。

  • 短所
    • ID ごとに区切るため自分でそのデータの分散を管理しないといけない。
    • データの性質によっては各サーバへの負荷が一定とは限らない。

構成2
  • データをシャーディングする。
  • プロセスには全ての CRUD を用意しておく。
  • 処理は Loadbalancer で分散する。
  • シャーディングの管理は DB に任せる。

f:id:Jxck:20110619001449p:image

  • 長所
    • ほぼ Mongo を使う構成で、 DB の管理は mongo に任せられる。
    • その後のスケールもさせやすい。

  • 短所
    • プロセスが対象とするデータ範囲は実質全体なのでキャッシュ効率は下がる
    • mongos にあたる部分の SPoF 対策が必要。

構成3
  • データをレプリケーションする。
  • プロセスには全ての CRUD を用意しておく。
  • 処理は Loadbalancer で分散する。
  • データの同期は DB に任せる。

f:id:Jxck:20110619001450p:image

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

couch や riak にはそれ自体が RESTful な API を持っているのでそのままでもたたける。

しかし、 WebSocket や 認証やセッションを考えると、直で叩くより裏に置く方が良いのかなと思う。

上の中では構成2が現実的なのかな、その用途では Mongo が一番相性がいいのかも。


まとまらないまとめ

アプリを SSP で実装した場合のバックエンドの Node による RESTFul JSON API のスケールについて考えました。

最終的には、色々と複雑な構成になった気がしますが、スケールアウトを考えると構成が複雑になるのは、 SSP に限らずそうだと思います。

実際の大規模サービス等の運用の場面では、もっと複雑なことが行われているだろうし、今回はそういった点は目をつぶっています。

プロセスの複数起動、プロセス間通信、 WebSocket の共有、 NoSQL による Sharding / Repliaction など、一通りのネタはそろいました。

そしてこれ以上は実際にやってみないとよくわからない。しかしこれから試してみるべき方針はなんとなくわかってきた気がします。


あと、前回はキャッシュに色々こだわって、SSP の裏に実装する JSON 形式のリソースは、キャッシュが容易だろうと考えていたけど、

本当にそうかよくわからなくなってきた。。

キャッシュをふんだんに取り入れるのは、それだけキャッシュのライフサイクル管理が大変になるのも事実です。

この辺は、もう少し考え直そう。


終わりに

なんとも煮え切らない結果になってしまった。

自分がよくわかってないところが多すぎて、たぶんわかっている人から見るとしょうもない内容になっていると思います。

ずっと考えててもしょうがないので、いったんここで出します。

続きはもう少し経験を積みながら考えて行こうと思います。

なんかちょっとでもフィードバックがあったらもらえるとうれしいです。


この記事は、なにかわかったら修正していくかもしれない。

そして、まとまってきたら、別で書くかも。


kzyskzys 2011/06/18 17:20 Redis はあまり詳しくないのですが、少なくとも memcached, Tokyo Tyrant では Consistent Hashing はクライアントライブラリ側の機能だったと思います。なので、個々のサーバーにことさらに書く必要はないかと思いました。原理上、あらゆる KVS で使えますよね。

あと、キャッシュの効率の議論がうまく飲み込めませんでした。これは DB から取得したデータをプロセスに保存して、一定期間は DB に問い合わせない、という感じを想定しているのでしょうか?

JxckJxck 2011/06/18 18:07 ありがとうございます。
そうか、クライアントライブラリの仕事ですね。確かに ライブラリレベルのサポートで、Riak の様に実装自体に入っている以外の場合はこういう記述は不適切ですね。修正します。(node で consistent hashing サポートのライブラリを調べてそれを書かないと意味ないですね、すいません。)


キャッシュはおっしゃる通り、DB から取得したデータを プロセスとひもづけた memcache 等にキャッシュできないかという考えです。(当初思ってたのがどんどんずれてきて、今のでは RDB の SQL で取ってきたデータを入れとくとかと変わらないです。)
一定期間問い合せしないか、もしくは更新は DB にいって、書き換えごとにキャッシュをリフレッシュするMaster-Slave みたいにするかは、実装次第かなと思っています。(後者は難しいかもですね。。)

kzyskzys 2011/06/18 18:37 Riak は自分でやるんですね! すごいなあ。

DB から取得したデータを memcached 群にいれるのであれば、その memcached 群は全プロセスで共有するのが一般的かと思います。で、そうすると構成1, 2, 3でキャッシュの効率に差はでないかなあと。

JxckJxck 2011/06/18 18:57 1のような絵にした最初の意図は memcache をプロセスにひもづけるようなイメージでした(複数台に分散したら、各プロセスのあるハードのローカルとか) 。Rev-Proxy でプロセス毎振れば、プロセスが担当するデータの範囲が限定されるから、そこでキャッシュすれば memcache に乗り切らないデータが後ろに有ってもキャッシュヒット率あがるかなという机上の空論でした。(実際は、データの分割を自分で管理しないといけないので、非現実的かもしれませんが。)

その意味で 2、3の絵はプロセスにひもづけても、担当が範囲が限定されないのでキャッシュ効率が下がるとしました。おっしゃるようにネットワーク上に memcache 置いて全プロセスで共有すると、対象が全体なので変わらないですね。


そしてここの話は Node 関係ないですね、プロセス間通信とネットワーク経由でキャッシュやDBアクセスができる以上、もっと単純なスケールアウトができます。なんというか SSP(一個前の記事) 考えた時こんなことを妄想していたので書いてみたんですが、もっと単純な分散構成をまず書くべきでした。全体的に中途半端ですいません。

kzyskzys 2011/06/18 19:21 長々とすみません。ありがとうございます。

memcached クライアントライブラリがやっているような分散がリバースプロキシの仕事になって、そっから下は分散しないイメージなのか。なるほど。ヒット率は全体でつかっても変わらなそうな気がしますが、通信がローカルで閉じるのは速度面の利点があるかもしれないですね。

JxckJxck 2011/06/18 19:26 いえ、ありがとうございます。

全体的に、できたらちゃんと検証して結果を書けたらなと思っています。
環境がなくて難しいんですが。。

makotoimakotoi 2011/06/22 08:54 興味深い記事をありがとうございます。TupleSpaceに関してはRubyのRindaがそれに該当するので、http://www.druby.org/ilikeruby/d208.html を読むと理解が深まると思います。
Rindaの場合はパターンマッチングが柔軟(keyの完全一致だけでなく、rangeやregex一致も可能)なので、Tuplespaceのnode.js版とか作ったら面白そうですね。
でも実際のWebSocketのプロセス間メッセージ共有はRedis PubSubが一番簡単そうですね。

JxckJxck 2011/06/22 19:31 makotoi さんありがとうございます。
Rinda は軽く見てみました。(むしろちょっと調べたところ逆に Rinda くらいしか主要っぽい実装が見つからなかった。。)
まだ理解は出来てないですが、見るところやはり PubSub でやってしまった方が楽そうですね。
とはいえ、TupleSpace ももう少し見てみたいと思います。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証