Skip to content

realtime polyglot app node ruby mongodb socketio

iwhurtafly edited this page Aug 18, 2012 · 9 revisions

リアルタイムのアプリ、または、プッシュベースのインタラクティビティを組み込んだイベント駆動型アプリは、 チャット、ラージスケールのゲーム、共同編集作業、低待ち時間での通知といった、ブラウザ内での動作の可能性を高める 新しい世代のベースとなります。たくさんのテクノロジーが、この流れをサポートしていますが、 以下の4つのテクノロジーが突出しています。: Ruby, Node.js, MongoDB そして Socket.IOです。

この記事では、これらの技術を使用したリアルタイムのアプリを構築するためのアーキテクチャとコードの両方を知ることが出来ます。

この記事のRuby data-writer componentのサンプルコードと Node.jsのWebアプリケーションはGitHub上で利用可能で、 動作に関しては、http://tractorpush.herokuapp.com/で確認可能です。

概要

サンプルのTractorPushリアルタイムアプリケーションは、MongoDbのキューにメッセージを格納するために Rubyベースのデータコンポーネントを使用し、次にそのキューがNode.jsのWebアプリに受信され、 Socket.IOを経由してユーザーのブラウザへ表示されます。 要するに全てのスタックは、プッシュ通知により動作します。

Overview of TractorPush components

このシステムは、システム全体を作り上げるそれぞれの独立したアプリケーション同士を結びつける糊として動作する 後方サービス(MongoDB)を共有します。 

メッセージキューとしてのMongoDB

キューは、相互運用性があるものの独立したプロセスを記述するためにパワフルなメカニズムとなります。 商用的に実行可能なソリューションとして何百はなくとも、何十も存在しています。 古くから存在し、広く実装されているIBM WebSphere MQに始まり、 より新しくオープンソースかつオープンスタンダードなRabbitMQZeroMQがあります。

MongoDBは、その柔軟なドキュメント保存の特性、多岐に渡る各言語へのサポート、 そしてtailable cursor の「プッシュ」の特徴により、 有用な多言語のメッセージキューとして役立ちます。 複雑で勝手気ままなJSONメッセージのマーシャリングとアンマーシャリングは自動的にハンドリングされます。 Safe-writeは、改良されたメッセージの耐久性と信頼性に対し効果があり、 tailable cursorは、MongoDBからNode.jsへの「プッシュ」を行うために使われます。

Ruby製メッセージキュー発行アプリの提供

Rubyの成熟したWebフレームワーク(Rails、 Sinatra)は、ユーザーが直面し、しばしばメッセージキューの源泉となる多くの Webアプリにとって理想的です。TractorPushは、これをRubyで書かれたdata-writerにてシミュレートします。

アプリケーションのクリエート

GitHubからアプリをクローンし、Heroku上にアプリをクリエートして下さい。

:::term
$ git clone https://github.com/mongolab/tractorpush-writer-ruby.git
Cloning into tractorpush-writer-ruby...
...
Resolving deltas: 100% (8/8), done.

$ cd tractorpush-writer-ruby
$ heroku create tp-writer
Creating tp-writer... done, stack is cedar
http://tp-writer.herokuapp.com/ | git@heroku.com:tp-writer.git
Git remote heroku added

MongoDBのアタッチ

メッセージキューを含むことになるMongoDBのインスタンスを生成するため、MongoLabのアドオンを設定して下さい。

:::term
$ heroku addons:add mongolab
----> Adding mongolab to tp-writer... done, v2 (free)
      Welcome to MongoLab.

MongoDBのCappedコレクションの設定

MongoDBのCappedコレクションは、 tailable cursorをサポートしています。tailable cursorは、MongoDBがリスナーへデータをプッシュすることを許可します。 もし、このタイプのカーソルが結果セットの終端に達した場合、例外を返す代わりに、新たなドキュメントを返却し続けながら、 そのドキュメント群がコレクションにインサートされるまでブロックします。

また、Cappedコレクションは非常に高いパフォーマンスを発揮します。事実、MongoDBは、オペレーションlog (または、oplog)を 保存するために、内部的にCappedコレクションを使っています。交換条件として、Cappedコレクションは固定となり (例えば、サイズが“capped”となる)、分割することが出来ません。多くのアプリケーションにとって、これは許容範囲です。

TractorPushアプリケーションは、メッセージを保存する際、MongoDBのCappedコレクションに依存しています。 heroku addons:open mongolabコマンドで、MongoLabアドオンのダッシュボードを開いて下さい。 "Add"を押下し、新規コレクションを追加して下さい。

Add MongoLab collection

この新規コレクションをmessagesと命名して下さい。また、詳細オプション画面を開け、 Cappedコレクションのサイズを8,000,000バイトと指定して下さい。(デモには十分なスペースとなります。)

Create capped collection

メッセージキュー発行アプリのディプロイ

我々は、Heroku上にメッセージ書き出しアプリを作成し、MongoLabのアドオンも設定済みの状態にしております。 次にHerokuへアプリケーションをディプロイしてみましょう。

:::term
$ git push heroku master
Counting objects: 20, done.
...
-----> Heroku receiving push
-----> Ruby app detected
...
-----> Launching... done, v4
       http://tp-writer.herokuapp.com deployed to Heroku

TractorPushは、MongoDBのデータベースへメッセージを書き出すシンプルでヘッドレスなRubyプログラムです。 Procfile を見てみましょう。 書き出しのプロセスが、workerとラベルされています。メッセージキューを開始させるため、workerプロセスを1dyno、 スケールして下さい。

:::term
$ heroku ps:scale worker=1
Scaling worker processes... done, now running 1

heroku psコマンドで、workerプロセスが実行されていることを確認して下さい。

:::term
$ heroku ps
=== worker: `ruby writer.rb`
worker.1: up for 47s

メッセージキューにおけるRuby

TractorPushアプリケーションは、MongoDBオブジェクトのマーシャリングとアンマーシャリングの柔軟性を デモンストレーションするために、3種類のドキュメントベースのメッセージを使います。: シンプル(または名前値と呼ばれる)、配列、そしてコンプレックス(またはネストされたドキュメントと呼ばれる)の3種類です。

このwriter.rb のスクリプトは、 上記3種類のドキュメントタイプの中から1つ、デフォルトレートである1秒毎に、MongoDBのコレクションへ書き出しを行います。

:::ruby
while(true)
  coll.insert(doc, :safe => true)
  sleep(rate)
end

:safeの書き出しオプションは、データベースがエラー無しでメッセージドキュメントを受け取り、応答することを保証します。

コレクションの中身とドキュメント数の増加を確認するために、`heroku addons:open mongolab`コマンドで、 MongoLabアドオンのダッシュボードへアクセスすることも可能です。

メッセージキューのタイプを表示するログを確認します。

:::term
$ heroku logs -t --ps worker.1
2012-03-23T14:56:35+00:00 app[worker.1]: Inserting complex message
2012-03-23T14:56:36+00:00 app[worker.1]: Inserting simple message
2012-03-23T14:56:37+00:00 app[worker.1]: Inserting simple message
2012-03-23T14:56:38+00:00 app[worker.1]: Inserting array message

`messages`コレクションがCappedコレクションであるため、コレクションのサイズが制限値を越えた場合、 古いドキュメントは破棄されます。

Node.js webアプリの提供

このシステムの消費者側は、Cappedコレクションからのメッセージを消費する Node.jsのwebアプリケーション となります。 ローカルにアプリをクローンし、Heroku上にアプリをクリエートして下さい。

:::term
$ git clone https://github.com/mongolab/tractorpush-server.git
Cloning into tractorpush-server...
...
Resolving deltas: 100% (8/8), done.

$ cd tractorpush-server
$ heroku create tp-web
Creating tp-web… done, stack is cedar
http://tp-web.herokuapp.com/ | git@heroku.com:tp-web.git
Git remote heroku added

アプリケーションリソースの共有

単一システムとして、2つの言語環境(Node.jsとRuby)を使用するために、この2つアプリケーションは、メッセージストアを 共有する必要があります。書き出しアプリとwebアプリ間でMongoDBのインスタンスを共有して下さい。 書き出しアプリからMONGOLAB_URI 環境変数をコピーし、Node.jsのwebアプリへセットすることで 共有可能となります。

`MONGOLAB_URI`環境変数は、データベースへの接続ユーザー名とパスワードを含んでいることに注意して下さい。 内密に管理することをお願いします。
:::term
$ heroku config:add -a tp-web `heroku config -a tp-writer -s | grep MONGOLAB_URI`
Adding config vars and restarting app... done, v23
  MONGOLAB_URI => mongodb://heroku...eroku_app123456

`tp-writer`から`mongolab`のアドオンをリムーブする場合、または、アプリ自体を削除する場合、 たとえ`tp-web`から参照されていたとしても、データベースを元のように供給し直すことが出来なくなります。 リソースを共有しているような環境下では注意して下さい。

webアプリのディプロイ

プロセスのタイプが`web`としてディプロイされたアプリケーションは、自動的に1web dynoとしてスケールされます。

HerokuへNode.jsのアプリをディプロイし、heroku psコマンドでwebプロセスの状態をチェックして下さい。

:::term
$ git push heroku master
Counting objects: 30, done.
...
-----> Heroku receiving push
-----> Node.js app detected
...
-----> Launching... done, v3
       http://tp-web.herokuapp.com deployed to Heroku

$ heroku ps
=== web: `node app.js`
web.1: up for 40s

ブラウザ上でアプリケーションを開くために、heroku openコマンドを実行して下さい。 JSON形式のメッセージが、Rubyの書き出しアプリからNode.jsのwebアプリへ、そして最終的に開いているブラウザへ、 リアルタイムにプッシュされていることを確認出来るでしょう。

TractorPush screenshot

Node.jsでメッセージを消費する

Node.jsのwebアプリにあるapp.js内のreadAndSendファンクションは、Rubyのデータ書き出しコンポーネントにより、Cappedコレクションに送られたメッセージを 消費することを担っています。

:::javascript
function readAndSend(socket, collection) {
  collection.find({}, {'tailable': 1, 'sort': [['$natural', 1]]}, function(err, cursor) {
    cursor.intervalEach(300, function(err, item) {
      if(item != null) {
        socket.emit('all', item);
      }
    });
  });
  // ...
};

collection.findの呼び出しは、messagesコレクション内の全ドキュメントに渡る繰り返し処理を行うカーソルを返します。 'tailable'オプションは、このカーソルへ、もしデータの終端に達しても、追加データを待つよう明示します。 このような仕組みにより、リアルタイムでメッセージを受信する挙動を実装しています。

多種多様なキューのリスニングをデモンストレーションします。 追加のcollection.findの箇所は、 コンプレックスのメッセージタイプのみ同じ呼び出しを行うことが出来ます。

:::javascript
collection.find({'messagetype':'complex'}, {'tailable': 1, 'sort': [['$natural', 1]]}, function(err, cursor) {
  cursor.intervalEach(900, function(err, item) {
    if(item != null) {
      socket.emit('complex', item);
    }
  });
});

socket.emitを使うことで、それぞれのカーソルの繰り返し処理が、メッセージドキュメントを放出していることに注目して下さい。

:::javascript
socket.emit('all', item);
// and
socket.emit('complex', item);

これは、サーバー側から接続された全クライアントのブラウザへ、メッセージドキュメント(JSONオブジェクト)をプッシュします。 このクライアントプッシュ型の特徴を提供しているライブラリは、Socket.IOと呼ばれています。

Pushing messages to the browser with Socket.IO

The Node.js web application serves up a single index.html page that uses Socket.IO to open a connection to the server and create a listener attached to message types ‘all’ and ‘complex’.

:::javascript
var socket = io.connect('/');
socket.on('all', function (data) { ... }
socket.on('complex', function (data) { … }

True bi-directional messaging with [WebSockets](http://en.wikipedia.org/wiki/WebSocket) is not yet available.

In reality, the client is polling the server for more data as the server-side Socket.IO configuration forces the connection to utilize XHR-polling with a 10-second timeout.

:::javascript
io.configure(function () { 
  io.set("transports", ["xhr-polling"]); 
  io.set("polling duration", 10);
});

Once the browser has shown all available messages it will stop. As new messages are inserted into the database, the browser will be pushed the new messages and resume.

Conclusion

The four technologies covered in this article are just one of many combinations that support a componentized real-time app. More fundamental is the role MongoDB's data-flexibility and Cedar's polyglot capabilities play in eschewing monolithic applications for a more modular system design.

Clone this wiki locally