Skip to content

realtime polyglot app node ruby mongodb socketio

iwhurtafly edited this page Aug 19, 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と呼ばれています。

Socket.IOでメッセージをブラウザにプッシュする

Node.jsのwebアプリケーションは、単一のindex.htmlページを提供します。このページは、Socket.IOを使っており、サーバーへのコネクションを開き、 メッセージタイプが‘all’と‘complex’であることを示したリスナーをクリエートします。

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

WebSocketsを使った双方向通信は、現在利用することが出来ません。

実際には、サーバーサイドのSocket.IO設定が 10秒毎のタイムアウトでXHRのポーリングを利用するよう、コネクションに対し強要し、 クライアント側は、追加データをポーリングするようサーバーへ要求します。

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

一度、ブラウザが利用可能なメッセージを全て表示し切ると、一端停止します。新たなメッセージがデータベースへ追加されると、 ブラウザへそれらのメッセージがプッシュされ、動作を再開します。

結論

この記事で取り上げた4つのテクノロジーは、コンポーネント化されたリアルタイムアプリをサポートする 多くの組み合わせの1つでしかありません。より重要なことは、MongoDBのデータ管理の柔軟性とCedarの多言語対応が、 モノリシック・アプリケーションを避け、 モジュール化されたシステムデザインを採用する役割を担っていることです。

Clone this wiki locally