Block Rockin’ Codes

back with another one of those block rockin' codes

"リアルタイム Web" に関するプラクティスのアウトプット

追記

11/12/26
MLのスレッドへのリンクが間違っていたので修正。

introduction

WebSocket なんかをつかって、従来のステートレスな処理以外に、コネクションを継続するステートフルな処理が可能になりました。
これを利用すると、これまで実装が難しかったリアルタイムな表現を Web に持ち込むことができます。


そして、 WebSocket を用いたプログラムを作成する上で、Node.js と Socket.IO を用いる方法について、
今年はこのブログでも何度か紹介してきました。


今日は今年一年の集大成として、自分が色々試しながら得たリアルタイム Web に関する知識、技術などを、
ここにまとめてアウトプットしたいと思います。


今回お話しするのは、 東京Node学園 3時限目 : ATND で発表した下記内容の抜粋です。


Node Academy | "About SlideStream & Tips for Realtime"


この資料は、今年10月に開催した 東京Node学園祭2011 の自分のセッションで使った、
SlideStream というスライドツールに関する発表で使ったものです。
この発表では、

について話しましたが、この記事ではそれを元にして、主に「リアルタイム Web」をいかに開発するか?
に絞ってお話しします。長いです。

Caution

  • そろそろ、Socket.IO 初心者向けエントリは飽きたなぁ、くらいの人を対象にしています。
  • 「リアルタイム Web 」= 「WebSocket を多用した使った Web」 くらいの意味で取っていただいていいですが。
  • 実装に Socket.IO on Node.js を使っていることが案に前提になってしまっています。
  • 生の WebSocket や別の言語、ライブラリを使う場合は、必ずしも全てが当てはまるとは限らないです。
  • Socket.IO のセキュリティというあまり出てない話に触れています。これは読み手に実益/実害をもたらす可能性があるかもしれませんが、一切責任は取りません。
  • 脆弱性という言葉を使いますが、知る範囲で実例はありません。可能性の話をしていると思っていただき、自分で検証してください。
  • SlideStream は東京Node学園祭の前に割と急いで作ったので、この実装や、そこでした判断が「完璧な解」では絶対にありません。が話はその時の実装がベースです。
  • 間違っている可能性は大いに有ります。気づいたら教えて下さい。

SlideStream

前提として、 SlideStream について、簡単に紹介したいと思います。

SlideStream は

  • Presenter がスライドのページを送ると、 Viewer のページも一緒に進む。
  • Viewer がページを動かしても、他の誰にも影響しない。
  • Presenter のマシンのコマンドライン出力などが、リアルタイムに Viewer のスライドに表示される。(ライブコーディングならぬ、リアルタイムコーディングと言ってます)
  • HTML ベースなので画像、動画などを iframe, link などで埋め込み、他のリソースを共有できる。
  • ベースは deck.js

というものです。

構成は以下のようになっています。



Node-Ninja にデプロイし、プロセスは一つで、 Redis を一部使ってます。
他に特別なことはしてません。
他、詳細はスライドを参照してください。

Auth(enticate|orize) of Socket.IO

まずは、認証(Authentication)と認可(Authorization)の重要性の話です。

ページ遷移と Presenter の識別

最初の機能として、 Presenter (つまり自分) がスライドのページを送ると、 Viewer のスライドもページが移ります。
これは、自分がスライドを動かす時、そのイベントを deck.js の API から補足し、Socket サーバに送っているためです。
Socket サーバは broadcast で、そのことを全 Viewer に伝え、 Viewer のブラウザは deck.js の API を叩いてスライドのページを変えます。


これは、特に難しくも何ともないんですが、 Presenter - Viewer が一方向ということを忘れて実装すると、
接続してる全ての Viewer のローカルでのページ切り替えが、全て別のユーザに broadcast されて、
それはもう大変なページ切り替えの嵐で、地獄絵図必至です。


正しく Presenter のみの切り替えが伝わるようにするには、以下のようにします。

  • Socket サーバは、各クライアントのセッションを識別できるようにする。
  • その中で Presenter の識別だけは認証しておき、「この接続は Presenter だ」と分かるようにしていおく。
  • 全ての Viewer と Presenter はページ遷移を Socket サーバに送るが、 「broadcast するのは Presenter のメッセージだけ」にする。


これで地獄絵図は避けられます。自分はプレゼンテーションの前にひっそり自分だけ認証をしていました。(認証ページ)
これは単純な HTTP アプリの cookie ベースのセッション認証と同じです。


WebSocket は、一旦 HTTP でリクエストした後、 upgrade を経て WebSocket にプロトコルを切り替えており、
Socket.IO はその時、 HTTP が持ってた情報を WebSocket に handshakeData というオブジェクトにして引き継ぐ仕組みを持っています。
ここに Cookie も入っているので、あらかじめ HTTP で「認証」しておけば、 WebSocket のコネクションも「認可」ができます。


SlideStream では以下のようになってます。

まず、 Express で認証しセッションデータに session.admin = true というフラグを立てておきます。
WebSocket への切り替え時は、それをそのまま handshakeData に渡しておきます。

io.configure(function() {
  io.set('authorization', function(handshakeData, callback) {
    if (handshakeData.headers.cookie) {
      var cookie = handshakeData.headers.cookie;
      var sessionID = parseCookie(cookie)['connect.sid'];
      // check the express session store
      sessionStore.get(sessionID, function(err, session) {
        if (err) {
          // not found
          callback(err.message, false);
        } else {
          // found
          handshakeData.session = session;
          callback(null, true);
        }
      });
    } else {
      return callback(null, true);
      // socket.io-client from node process dosen't has cookie
      // return callback('Cookie dosen\'t found', false);
    }
  });
});


ブラウザでページを切り替えると、 go イベントが発生するので、
それを補足し、同じ go イベントで Socket サーバに Presenter, Viewer 全員がページ(to)を送ります。

Socket サーバは、 go イベントを受け取ったら、セッションのフラグを確認し、 Presenter のものだったとき(admin フラグが有ったとき) だけ、自分以外に broadcast します(認可)。

socket.on('go', function(to) {
  if (!socket.handshake.session) return false;
  if (!socket.handshake.session.admin) return false;
  socket.broadcast.emit('go', to);
});


細かい実装方法は、以下の二つを参照して下さい。

Practice

Socket.IO については上述の方法で認証ができます。つまり同等の仕組みで WebSocket についても可能です。
WebSocket は通信の自由度が高い(Client/Server 共に push できる、クロスドメイン etc) なので、
その各セッション/イベントなどの粒度で「適切な認証/認可」を行うことは重要です。


例えば、上述の 「Presenter によるページ切り替え」がもっと重要な操作だった場合どうでしょう?
セッションを乗っ取られれば、 broadcast 対象にあたる全てのクライアント(ここでは Viewer)に、
不正なメッセージを送ることができます。


また、具体的な乗っ取りは、上記方法では Cookie の乗っ取りと等価になると思います(未検証)。
つまり、 Cookie に大してはおおむねこれまでと同等に、重要視して扱う必要があるということです。
(「WebSocket でコネクション保持だから、Cookie とかもうどうでもいい。」ではないということ。)


また、実装方法によっては、 SessionStrage にあたる部分の、参照が劇的に増える可能性があります。
今回は Redis を使っていますが、十分な速度が出ていたし、 Socket.IO / Express との相性はいいと思うので、お勧めします。

broadcast injection

Socket.IO では、イベント名にも気をつけた方が良いかもという話です。

WebSocket 時代の脆弱性(?)

入門向けサンプルだと、例えば以下のようなエコーサーバの例があると思います。

// Server
io.sockets.on('connection', function (socket) {
  socket.on('msg send', function (msg) {
    socket.emit('msg push', msg);
    socket.broadcast.emit('msg push', msg);
  });
});
// Client
socket.on('connect', function() {
  $('#msg').click(function() {
    socket.emit('msg send', 'data');
  });
  socket.on('msg push', function (msg) {
    display(msg);
  });
});


これは、ブラウザ上で操作していると、 $('#msg') である何かをクリックしないと発生しないように作ったつもりだと危ないです。
なぜなら、その画面で開発コンソールなどを開けば、以下のようにすると任意のメッセージを broadcast に載せることができます。

socket.emit('msg send', '悪意のあるメッセージ');


Socket.IO あまりよくわからなくても、「そりゃそうだろ」と思う程度かもしれませんが、
意識しないと、これも脆弱性になる可能性があります。
もちろん Chat のようにそれでも良い場合や、「送れたところでなに?」ということもあると思います。
でも、「broadcast 対象全員に任意のメッセージを送れる」状態には、気を付けないといけないと思います。

SlideStream の場合

SlideStream であった、ちょっと具体的な例を出しましょう。


SlideStream では、 Presenter (自分)がローカルの Emacs で入力した文字を、
ファイルから読み取り Socket サーバに渡し、 broadcast してリアルタイムコーディングしていました。
入力は browser ではなく、 ローカルに立てた Node のプロセスから送っていたので、
実際は Socket.IO(Socket サーバ) と Socket.IO-Client(自分の Mac ) の間でのプロセス間通信です。


ところがこの時実は Socket.IO-Client のリクエストは Cookie を持たないので、前述した認証が出来ませんでした。


すると単純には、以下のようなコードになります。(例)


まず、 codeStream という EventEmitter から、 Emacs に書いたコードが流れてきます。
これを、 code というイベント名で、 Socket サーバに送ります。

// Presenter Local Process
var socket = require('socket.io-client').connect('http://' + host + ':' + port);
socket.on('connect', function() {
  codeStream.on('code', function(data) {
    socket.emit('code', data);
  });
});


Socket サーバは、 code イベントで得られたデータを、 Viewer に broadcast します。

// Socket Server
socket.on('code', function(data) {
  socket.volatile.broadcast.emit('code', data);
});


ブラウザは code イベントから結果を受け取り、表示処理をします。

// Viewers Browser
socket.on('code', function(data) {
  codeRender.rawRender(data);
});


なんとなくわかるでしょうか?

そしてこの場合、 Viewer はブラウザから

socket.emit('code', 'm@(^_^) pugya~~~');

で、リアルタイムコーディングをみごと台無しにすることができます。


認証ができないけど、放っては置けないのでどうしたかというと、 Socket サーバのイベントはソースを見ない限りはわからない & 発表時はまだソースは公開していない(というかこれもあってしなかった) ので、イベント名を表からは推測しにくいだろうものに変えて、対応しました。

Practice

初心者向けサンプルを dis るつもりはありません(というか上のは自分の記事からです)。ただ、もちろんちゃんとやるなら気をつけるべき点は、そこには書かれていないこともあるでしょう。


あと、v0.6? くらいまでは、 Socket.IO は 'message' というイベント名しか使えませんでしたが、今は任意の文字列が指定できます。


まずなによりも、認証できるならして、認可によって制限することが大事です。
そうでもなく、上記のようになんらかの理由から認証/認可での制限が難しいとか、
たとえ Cookie を盗まれてもこのイベントだけは任意に叩かれるとまずい、
というようなものは、推測しにくいイベント名に変えましょう。
イベント名は、送る側と受ける側だけ同じであれば良いです。
上の例のように、たらい回しにするところを全て同じにしたら、簡単に推測できます。



あなたの作った MMO ゲームは、 socket.emit() だけで画面の端までワープできたりしませんか?



ただでさえ、クライアントが自由にクロスドメインから接続できて、 emit() だけで手軽にデータを送りつけられてしまうので、
重要な API はランダム文字列などを用いる方が良い場合が有るかもしれません。


もちろんソースを公開してしまうと元も子もないので、DB のパスワードとかと同じように、別ファイルに出して .gitignore などの隠蔽工作も場合によっては必要です。イベント名は単なる文字列なのでやりようはいくらでもあります。


今後は、このイベント名をうまく隠蔽する仕組みが恐らく色々出てくるでしょう。
フレームワークレベルでの解決は有りなケースだと思います。、


自分も後で述べる自作のフレームワーク内ではこの問題を扱いたいと思っています。

volatile & diff

話は変わってパフォーマンス系の話です。

例えば、リアルタイムコーディングで、書いたソースコード文字列を broadcast するとなると、ちょっと大きなソースになった時
「Diff を送って、ブラウザで復元すれば効率がいいのでは?」という考えが浮かびます。
JS には google-diff-match-patch というそのものズバリなライブラリがあるため、
実際自分もそれを考えたんですが、それはやめて、「一文字入力する毎に、ソース全体を送る」を選びました。


採用はしなかったけど、実装はしたので、その残骸が実は http://d.hatena.ne.jp/Jxck/20111112/1321079097 だったりします。
Both-sides Javascript 自体は便利なのでそっちで出しました。


やめた判断は、 Socket.IO の 「Volatile の性質」と、後述する「CPU と I/O のバランス」から来る物です。

volatile オプション

volatile オプションについては、ここの「揮発性メッセージ」を読んで下さい。
簡単に言えば TCPUDP の関係です。送達確認の有無です。ちなみに送達確認(ACK) についても同じ記事に書いてあるので、合わせて読んでおいて下さい。


さて、 volatile オプションを付けて diff を送ると、 diff の送達が欠損した場合、きれいに復元できない場合があります。
例えば、 「foo を書き、foo を消して、 bar にする」という処理が、 3 つの diff になったとします。

+ foo
- foo
+ bar

期待する結果はもちろん

bar

でももし volatile がついていたとして、真ん中の -foo が欠損したら、以下の結果になります。

foo
bar

もちろん、 volatile を付けなければ、恐らくリトライして正常にいくと思います。
いかなくても、送達(ACK)を確認しエラーを検知して全部取り直すとかやりようはあります。

volatile 有無の判断

いくつかの要因から volatile を使う方向で実装することにしました。


まず、リアルタイムコーディングはページ切り替えなんかよりよっぽどリアルタイム性が高く、
SlideStream では1文字1イベントで設計しています。
この場合、どこかで詰まると、コーディングの表示が止まる可能性があります。
つまり volatile を付けないと詰まった場合の制御も考える必要がでます。


逆に volatile を付けて、かつ毎回全文送ってしまえば、一文字欠損しても、
その次のイベントで全部のコードが来て、全部書き直せば、 Viewers には2文字の表示が速かったように見えるだけです。


また、当日、会場のネットワークがどの程度のものなのか、想定できなかったのもあります。


なによりも、「全部送って、丸っと書き直す」スタイルは、実装がとても楽だったこともあります。

Practice

今は、リアルタイムと一言で語られていますが、リアルタイムにも程度があります。
一分に一回のページ切り替えと、一文字一回のコーディングでは、イベントの頻度が違います。


後者の場合、例えばゲームでの座標移動や、マウスカーソルの動き、 Twitter のストリームなどを扱う場合、
それが「多少消えてもいい」ものであれば、 volatile を使うことが Socket.IO のドキュメントでも推奨されています。


volatile をつけなければ、送達の確認もできますが、上の例ではそれは適切ではないでしょう。


また、実装を読むと、 volatile は socket に書き込めなかったら捨てるという実装のようです。
(つけないと、その時 ACK のコールバックを実行して、リトライ/タイムアウトなど)


それがどの程度の頻度でおこるかは、当日のネットワークとクライアント PC の状態に依存すると考えられます。
つまり、その性質上、事前判断はむずかしく、現実的に 「その時 Socket に書き込めるかどうか?」を、
判断基準にするのは難しいだろうことを付け加えておきます。

Domain Specific implementation

これは、賛否わかれる問題かもしれませんが、率直にあったこと、思ったことを書きます。
今までのライブラリ、特に「汎用性」をもったものは、リアルタイムに不向きな可能性もあるということ。

謎のボトルネック

長いので簡単に話すと、 SlideStream を本番前に何人かに接続してもらって試したら、だんだんもの凄く重たくなってしまい、使い物になりませんでした。
原因を探るために、 node のログや、プロセスなんかを色々調べても、特に問題はない。
ローカルで試す分には出なかったのに、本番数日前に気づいたので、とても焦りました。
結論から言うと、重たかったのはブラウザの自滅で、サーバもネットワークも全く関係なかったのです。

シンタックスハイライトと従来のライブラリ

恥ずかしいですが、あったまま話すと、原因は意外にも、シンタックスハイライトでした。
今回、コードのシンタックスハイライトは、使い慣れた SHJS - Syntax Highlighting in JavaScript を使って、 BashJavascript を色付けしてました。
このライブラリは、 pre タグの中の文字列を一気に書き換えるので、volatile を選んだ今回の場合「コードが毎回全部送られてきて、それを毎回ごそっとハイライト」になります。
で、これをリアルタイムコーディング中もの凄い回数繰り返していると、ブラウザでメモリがもの凄く消費され、スライドがもの凄く遅くなっていました。
Node.js でリアルタイムということで、すぐにデータの転送量や、 Node プロセスを疑ってしまったのですが、Chrome のコンソールを開けば一発でわかることでした。


シンタックスハイライトがメモリリークしていたのかを調べて、直しても良かったんですが、自分にはとにかく時間がありませんでした。
しかし、 SHJS が他言語のハイライトに対応するために、オブジェクト指向の流儀でクラスを分割し、拡張性、汎用性を高めていることにより、
生成されるオブジェクトに無駄があることは明白でした。


今回の発表はストーリーも概ね決まってて、ハイライトしたいソースも分かっています。
また、実はハイライトって、それっぽく色がついてると、それっぽく見えるものです(標準が無いですし)。


そこで、以下のように急いで作った正規表現で、SHJS の css だけ借りて、だいたい SHJS と同じようにハイライトするものを以下のように書きました。
見ると嘲笑されてしまいそうな、恥ずかしいコードですが、あえて張ります。

render.prototype.higlightJS = function() {
  this.cache = this.cache
    .replace(//g, '>')
    .replace(/"(.*?)"/g, '\"$1\"')
    .replace(/'(.*?)'/g, '\'$1\'')
    .replace(/(var|function|static|true|false)/g, '$1')
    .replace(/([A-Za-z0-9]*?)\(/g, '$1(')
    .replace(/^\/\/(.*)/g, '//$1')
    .replace(/[^:]\/\/(.*)/g, '//$1')
    .replace(/\/\*\*(.*)/g, '/**$1')
    .replace(/\*(.*)/g, '* $1')
    .replace(/(\d{2,4}?)/g, '$1')
    ;
};

render.prototype.higlightBash = function() {
  this.cache = this.cache
    .replace(/(ls|cd|tree|rm|\sexpress\s|npm|\snode\s)/g, '$1')
    .replace(/(create)/g, '$1')
    .replace(/(Jxck\$)/g, '$1')
    .replace(/(\d{4}?)/g, '$1')
    ;
};


思いついた順に replace をつなげただけのコードなので、中身はすぐ分かると思います。
しかし、これに置き換えることで、 SlideStream はあのリアルタイム感を得ることができました。

Practice

SHJS はもちろん悪く有りません。ただしこれは、 SHJS が
「ブラウザの最初の表示で一回だけ動作」することを想定している、
つまり従来の Web での範囲に最適化されたものだというだけの話で、
その範囲では、言語の定義を追加することも簡単な、拡張性のあるすばらしいライブラリです。


しかし、こうした既存のライブラリが、リアルタイムな場面では適さない作りになっている場合がある、
という可能性を痛感した場面でした。


もちろん SHJS の拡張性をそのままに、リアルタイムに有った形に改善することも出来るでしょう。
でも、ここではそれとは別にもう一つ、この拡張性を捨てればもっと速くなるかも?という解も出てくると思います。


ちょっと飛躍しますが、上手く言えないですが、

「従来の Web を、現状の全ての仕様を満たしたままリアルタイムにする」

というような要件も、今後出るかもしれません。
この記事に上がってること、上がってない他の要因含めて、自分は難しいと思います。
そこでは「何かを捨てる」というより、もっと積極的に「必要最小限にする」が重要になる場面がある気がしてます。


今回はまだ良いとしても、リアルタイムゲームくらいの世界になれば、



と、光の速さが問題に上がるくらい、「速さ」が重要になります。


例えば、今回の場合、「使う言語」「使うキーワード」が分かってました。それ以上に対応しても、遅くなるだけでした。
だとしたら、美化する訳ではありませんが、自分が書いた replace チェインは、あながち悪い方法では無かったということも、できなく無いのではないでしょうか?
(もっと速くできるとかは別として。)
高速化、これは今までもあった話です。でも High Performance Web Sites なんかに載っていたのは、
ブラウザの読み込みサイズを減らすとか、最初の表示をいかに速くするかとか、確かそういう話でした。
しかし、リアルタイムな場合、最初の表示も大事ですが、表示してからが勝負です。
とにかくリアルタイムに見せたい場面が、リアルタイムに見えることが重要だと思います。
そのためには、光の速さまで気を使って、恥ずかしい感じのコードになってでも最小限の実装の方がいいかもしれません。


自分はそれを踏まえて、発表のときはこのような話をしました。

important for realtime is looks like realtime

ちょっと間抜けな文な気がしますが、そういうことです。
さすがに replace チェインは、単に汎用性を失ってて、それ自体は規模の大きい開発の場面では「悪」であるでしょう。
しかし、「そのくらい割り切った実装も、速さを重視するには必要かも」くらいには思っておくべきだと自分は思いました。
スピード狂な方々が、存分に真価を発揮する場面と言えるかもしれません。

balance btw CPU Heavy & I/O Heavy

ここは、リアルタイムに限らず、 Node.js を使う上で理解しておくべき点です。
しかし、Non-Blocking I/O やイベントループの話は理解できてる前提で書きます。

ロジックとイベントループ

この話は、具体例として先ほどの Diff の件をとって説明します。
さっきの話を読んで、


「自分なら volatile がついていようと、復元も含めて diff でばっちりロジックを実装できるぜ!」


と思った方、それはすばらしいですが、あなたが組むロジックは、イベントループを止めませんか?


Node.js をシングルプロセスで動かして、 nextTick などのテクニックを使わずに、実直に複雑なロジックを組んだ場合、
そのロジックが CPU を占有している間、「イベントループが止まる」状態になる可能性があります。
つまり、そこをボトルネックとして、他の処理がブロックされます。
これが Node.js で CPU Heavy(= CPU をごりごり使う処理)を書かないように気をつけるべき理由です。


対して、 I/O Heavy(= Disk, NW への読み書きを、がつがつ行う処理) については、それがどんなに重たくてもブロックしません。
だから、 SlideStream のようにたった1プロセスで動いているスライドツールに、2-300アクセスがあって、 broadcast を繰り返してもちゃんと動きます。
一方自分の Mac は一文字入力する毎に Disk, NW にアクセスしても、ファンがうなり声を上げて恥を書くこともありません。
1プロセスの1スレッドなのでメモリも使わないし、C10K 問題もありません。(これは、ちょっとしたベンチマークを最後に載せます。)


これが Node.js のメリットであり、リアルタイムと相性がいいという理由の一つです。


そのメリットは、「ブロックしちゃうような処理とか書かないし!!」と顔を真っ赤にして言う人があらわれる理由の一つです。


Event-loop harassment Pan-da

さて、こうして「ブロックしちゃうような処理」を書いて、イベントループを止めてしまうことを、
元Joyent のチーフエバンジェリストで、今は Node.js のパフォーマンスのコンサルタントをやっている、 @ はこう表現しました。

「イベントループハラスメントパンダ」です。(元ネタは JSConf2011 のこの資料 P29)



元ネタはサウスパークの「セクシャルハラスメントパンダ」という日本ではおよそ通じないネタです。


Diff に話を戻しましょう。


Diff 取る処理と、それを戻す処理は CPU 演算です。
それを、 volatile を付けずに実現して、リカバリのロジックを追加していったとすると、
おそらくどんどん CPU Heavy になる可能性があります。


一方、全部まるっと送るよう単純化した処理は、ほとんどが I/O での受け渡しです。
そして、その「単純にするという最適化」は、イベントループを滑らかに回し、
よりリアルタイム性を増す場合があると言えます。


つまり Node.js は、


「下手に最適化しようとごりごり演算ロジックを書くほどに、イベントループを止めて遅くなる可能性がある」


ということです。


これは、実はブラウザでも同じです。WebWorker や setTimeout などを使う手もありますが、それもバランスです。
それを踏まえると、先ほどのハイライトのreplace() も、イベントループの停止を最小限に抑えているという評価も可能になってきます。

simple is fast, fast is realtime


発表では、こんなふうにまとめちゃったけど、その時はこういうことを話していました。



馬鹿みたいに単純な処理の方が速い場合が有ります。
というか、それを「馬鹿みたいに単純な処理」ではなく、


「CPU Heavy なのか? I/O Heavy なのか?」


という観点で見る必要が有ります。

特に、サーバは処理が集中するので、ロジックによっては、これは顕著に出る場合が有ります。
たとえば最近、Nodejs_jp の ML にあがったこれなんかその典型と言えるでしょう。


もちろん、全てを I/O でカバーするにも限界があるのも事実なので、
重要なのはバランスです。


サーバはクラスタを組むことも出来ます。将来的にスレッドのような仕組みも入る予定です。
でも、本来の Node.js の性質を知ることの方が先だと思います。


そうすれば、 フィボナッチが遅い理由も分かると思うし、
自分が Jxck's OutPut - Nodeにテンプレートエンジンはもういいかな と考えた理由も伝わるかもしれません。
発展させると シングルページっぽいアプローチ(SSP) がよさそうなことや、
このメリットが生かせないアプリは、 Rails とかでやれば良いのでは?というそもそも論もきちんとできます。


じゃあ、バランスってどこで見るのか?それは難しですが、次にもう一つ話をしたら一つのアプローチを紹介します。

Client <> Server

SlideStream の場合、メインのクライアントはブラウザです。
そしてブラウザの性能も、PC のスペックも、どんどん良くなっています。
その上、 HTML5 で出来ることが増え、Node.js の特性でもある、
「クライアントとサーバで同じ言語が使える=Both-Sides JavaScript」の助けもあって、
選択肢は広がりました。


「この処理は、クライアントとサーバどちらにやらせるか?」


は十分に考えて、「従来のサーバが全部やってブラウザは表示だけ」は幼稚園までなのはもちろん、
Ajax でデータを非同期に Pull して表示」も小学生までなのは言うまでもありません。
ロジックをきっちり持たせ、場合によってはクライアントだけで完結する時代ですしね。

Practice

これもバランスです。ここまでの話を総括すると、 Node.js ベースのアプリには、
このバランスを取る軸として大きく四つの選択肢があることがわかります。



今回 SlideStream で言えば、青い印あたりを狙ったことになります。
なるべく I/O に寄せています。主に Socket.IO の broadcast サーバです。
また、サーバでハイライトしてから、クライアントに送るという処理もできますが、
例え単純な replace でもサーバではやりません。クライアントにやらせます。


この表で、サーバオンリーでCPU演算ごりごり(赤い印)ってどうでしょう?
たぶんそういう要件なら、 「Rails でいいのでは?」という意味が伝わるでしょうか?
まさかとは思いますが、 Rails を dis る気はさらさらないです。というか Django でも Cake PHP でもいいです。


それぞれメリット/デメリットをちゃんと理解し、使い分けをちゃんとすればいいだけです。
そうすれば、誰かが顔を赤くしたりして残念なことになることもありません。そういうの見るのはあまり好きではないんです。
(蛇足ですが、一部がリアルタイムな Web の構成として、「Node+Railsで連携して両方のいいところ取る」というのはありだと思っています。)


この辺の考えがわかると、あとはアプリ毎に最適化するだけです。
ちなみに出しておいて言うのもなんですが、このマトリクスにもとらわれない方がいいです。例です。

ベンチマーク

SlideStream については、このスライドを https://node-ninja.com/ に上げて、
FirstServer さん協力のもと、大量のブラウザからアクセスするという、負荷テストをする機会がありました。


結果は以下です。あくまでも SlideStream の結果であって、それ以上のものを示す意図は有りません。



グラフは DTrace(Joyent の Bryan Cantrill が考案したトレースツール)が吐き出したデータをグラフ化したもので、
Node Ninja の Analytics 画面から見れる(一部は見れないものもある)ものです。


このグラフから、どういったことが読み取れるかについては、中の人である @ さんに色々教えてもらいました。
ほとんどその受け売りになってしまいますが、書きたいと思います。

テスト条件
  • 30分にわたって接続数を増やしていき、最終的には 1000 程度の WebSocket コネクションを張った。
  • その中で SlideStream を使ったリアルタイムコーディングのセッションを、リハーサルした。
  • Node.js のプロセスは一つ、バックに Redis を利用。利用していないが MongoDB も起動していた。
結果

結果は以下です。なお、このテストの間、リハーサルの Viewer には、スライドが問題ないリアルタイム感で見えていたことを確認しています。


グラフ詳細(各グラフは毎秒単位)

左上より

[TCP] Accepts / remote IP(TCP接続確立数 / IP)
単位時間毎の接続数なので、デモ中に接続がどんどん増えていっている。
[Network interface] VNIC bytes sent and received / sent and received(データ送受信バイト数 / 送信・受信)
約 500KB/sec ぐらいのデータが送られている。ほとんどがテキストデータなのでボトルネックになるような量ではなかった
[Filesystem] logical read/write operations / app name(論理的な読み書き数 / プロセス名)
ファイル I/O の回数は少なく、ボトルネックになるような量ではなかった

右上より

[CPU] thread executions / app name(CPUスレッドの実行数 / プロセス名)
mongod が定常的に何かを実行し、Node が上限に振れている。Redis はほとんど CPU を使っていない。跳ねているのはutmpd という監視プロセス
[CPU] aggregated CPU usage / zone name(CPU利用率 / 仮想マシン)
サーバ全体で定常的に 25% 程度使われている
[Memory] resident set size / zone name(メモリ使用バイト数 / 仮想マシン)
サーバ全体で 200MB 程度。(その中で Node+Redis が消費したメモリは 50MB 程度であったとのことです。)
考察

CPU, ディスクI/O, ネットワークI/O の負荷は定常的に低く、ボトルネックになるような状態には至っていない。
またメモリも 1000 コネクションで、サーバ全体が 200MB 程度なのに対し、 Node(+ Redis)が自体が消費したメモリは 50MB 程度だった。
MongoDB や MySQL を要するようなデータの永続化とそのキャッシュなどが、 SlideStream には無かったため、メモリや File I/O の負荷が少ないのは当たり前と言えば当たり前ではある。
しかし Apache の場合 1 コネクションで 5MB ほど消費する(親プロセスや読み込むモジュールなどによって大きく変わる) とのことで、そうした環境で SlideStream を動かし 1000 程度のリクエストがあったら、もっとメモリを消費したと考えられる。


少なくとも全体的にリソースの消費は対したことなかったと言えるのではないでしょうか。

Stream Based Application

最後に今後の話です。

今回作った SlideStream ようなツールは、ソースに重複が多く有ります。
これは、クラスっぽくメソッドが抽象化できても、
そこにイベントをうまく抽象化しきれていないあたりが大きいなぁと感じていました。


また、統一したインタフェースがあると、色々と捗ることは間違い有りません。
そこで、 Node.js のもつ Stream が活用できると思います。

Node.js の Stream はあまり話が出ていなかったので、アドカレで書きました。
こちらを読んで下さい。


Node.js の Stream API で「データの流れ」を扱う方法 - Block Rockin’ Codes


そこに書いたように、データの流れを統一したインタフェースで扱うための仕組みです。
SlideStream のソース中も xxStream というオブジェクトがいくつか出てきますが、
あの頃はまだ Stream の理解が浅く、インタフェースに則ってはいません。
しかし、やりたいことは割と同じ感じにはなっているので、今なら書き換えも難しくはないでしょう。


Stream 自体はまだまだ議論の余地のあるものですが、個人的には重要になってくると思っています。

ステートレスとステートフル

これまでは 「HTTP はステートレスである」という前提が、全ての Web アーキテクチャの根幹に有ったと思います。
Cookie ベースのセッションも、画面遷移という設計も、基本その前提の上でつくられ、
Web Application Framework (WAF)なんかもそれに最適化されていたと思います。


REST もすばらしい考え方です。


Ajax が出てきても、基本はそのリソースを非同期に取得するといった使い方から始まって、
まだその枠を出ない場面も多いです。


しかし、 WebSocket はコネクションを保持するステートフルなプロトコルです。
これで実装するアプリケーションを、従来の WAF の枠で考えることは、
ちょっと難しのかなと言うか、もし枠を拡大するなら、実装を変えないといけないかもしれない。


今の自分では、まだ結論は出せていませんが、ここで Stream は一つ重要なキーワードになると思います。

Practice

Stream を用いて、以下のように考えを転換します。


「全てのリソースは、 Stream である」


リソースは REST などの議論に出るあのリソースです。
それはチャットでも twitter Streaming でもいいし、「クライアント」「サーバ」という粒度もあり得ます。
とりわけ今回のように連続してデータを生成するものに当てはめると見えてきます。


そこで、全てを Stream として抽象化して扱う、というモチベーションで考えたのが拙作の Stream.IO です。


Stream.IO というものを作ってます。 - Block Rockin’ Codes


まだまだ、これからですが、ここからは今後リアルタイム Web を考察する上で、
重要なことを色々気づけると期待しているし、
成功すれば、 SlideStream 的なものが Stream ベースのアーキテクチャで簡単に作れるかもしれません。


まだまだ、実装はだせるところまで行ってないので、
これについては、また別で書きたいと思います。

conclusion or afterword

すっかり長くなってしまいましたが、
Node.js と Socket.IO を用いた SlideStream の実装から得られた、
「"リアルタイム Web" に関するプラクティス」のアウトプットでした。
Node.js と Socket.IO から Stream.IO に至まで、
今年1年間取り組んできたことを棚卸しする良い機会になったので個人的にはよかったです。


また、今回の考察には、多分に議論の余地が残っているし、色々間違えもあると思います。
指摘、コメントなどなるべくフィードバックをもらえると、自分も先に進む糧になると思うので、できればお願いしたいです。


そしてこうした記事が、まだまだ発展途上な 「リアルタイム Web」 について、
議論して行くたたき台となればと思います。