WEBアプリケーションでは応答性能が求められることが頻繁にあります。いろいろ調べると、10秒までは待てるようですが、なかには3秒が限界という結果もあるようです。待てる待てないは、人によりまちまちですが、速いほうが好感が持てるでしょう。この記事では、重たい処理をワーカープロセスに任せて、レスポンスをとりあえず返すというワーカー オフロード パターンを紹介し、Bluemix での実装方法を簡単にご紹介したいと思います。
ワーカー オフロード パターン
WEBアプリケーションは、http リクエストをアプリケーションに投げて、何らかの処理を実行後に http レスポンスを返却します。仮に、この処理に時間を要すると応答までの時間がかかることになります。
ワーカー オフロード パターンというのは、処理に時間がかかる部分を別のプロセスに任せ、とりあえずは応答を返すというものです。
(図は MQLightのドキュメントサイトから抜粋 - https://developer.ibm.com/messaging/mq-light/docs/ )
このパターンを実現するのにメッセージングがよく使われます。簡単には以下の図に示すように、メッセージシステムを利用して、処理依頼をワーカープロセスに出します(図ではワーカーアプリ)。
ワーカープロセスは(時間がかかる)処理を実行した後に、処理結果をメッセージシステムを通じて通知をします。通知されたアプリケーションは、結果を読み取ります。
処理結果については、たとえば、ユーザーが再度WEBを開いた場合には、この結果を知らせるようにすればよいのですね。統計処理や、数値計算などを任せて後で見るというのに利用できます。処理依頼から結果を見るまでのターンアラウンド時間は、従来の処理よりもかかるかもしれませんが、応答という意味ではユーザーのストレスが軽減されるでしょう。
Bluemixでの実装
Bluemix にはメッセージングシステムとして MQLight が提供されています。
- (注意)MQLight ではメッセージ数が上限を超えると課金されます。気になる場合は、Experimental のメッセージサービスを利用してください。
ここでは説明を簡単にするため、フロントエンドには Node-RED を利用しました。Bluemix 上の Node-REDスターターキット には mqlight ノードがあるので、すばやく利用できます。
Node-REDアプリケーションの実装
Node-RED はスターターキットを用いて作成し、コードをダウンロードします。MQLight サービスを利用するので、これを作成します。
例:
$ cf create-service mqlight standard mqlight
さらに、テンプレートの manifest.yml を少し書き換えます。なお、Cloudant DB は筆者のスペースにある、既存の "cloudantdb" を利用しているので、環境変数 ”NODE_RED_STORAGE_NAME” を設定しています。
applications:
- name: nodered
host: amntnodered
memory: 512M
instances: 1
domain: au-syd.mybluemix.net
disk_quota: 1024M
path: nodered
command: node --max-old-space-size=384 node_modules/node-red/red.js --settings ./bluemix-settings.js -v
services:
- cloudantdb
- mqlight
env:
NODE_RED_STORAGE_NAME: cloudantdb
Bluemixにデプロイした後に、フローを定義します。
フローはこちらから参照できます。
[{"id":"9e066960.61f998","type":"inject","name":"","topic":"","payload":"5","payloadType":"string","repeat":"","crontab":"","once":false,"x":135,"y":108,"z":"dfd55d9e.202aa","wires":[["1ca973b8.e3568c"]]},{"id":"c8286d2f.37d79","type":"mqlight out","name":"Request","service":"mqlight","topic":"mqlight/example/request","x":472,"y":108,"z":"dfd55d9e.202aa","wires":[]},{"id":"1ca973b8.e3568c","type":"function","name":"number","func":"var mqMsg = {\n number: msg.payload\n};\n\nmsg.payload = mqMsg;\n\nreturn msg;","outputs":1,"noerr":0,"x":294,"y":108,"z":"dfd55d9e.202aa","wires":[["c8286d2f.37d79","3686002c.c97a"]]},{"id":"ef742968.108bd8","type":"mqlight in","name":"Result","service":"mqlight","topic":"mqlight/example/result","share":"sample","x":157,"y":210,"z":"dfd55d9e.202aa","wires":[["a7c58829.583a78"]]},{"id":"a7c58829.583a78","type":"debug","name":"","active":true,"console":"false","complete":"false","x":432,"y":211,"z":"dfd55d9e.202aa","wires":[]},{"id":"3686002c.c97a","type":"debug","name":"","active":true,"console":"false","complete":"false","x":488,"y":53,"z":"dfd55d9e.202aa","wires":[]}]
なおアプリケーションはフィボナッチ数を計算処理依頼をするというものです。フィボナッチ数列の定義はいくつかあるようですが、今回は以下のように定義しました。
F(0) = 1
F(1) = 1
F(n) = F(n-1) + F(n-2)
フローの上段は、数値を MQLight に飛ばす処理です。下段のフローは、処理結果をメッセージで受け取り結果を出力する処理になっています。
ワーカープロセスの実装
さて、ワーカープロセスですがこれは単純にメッセージキューからメッセージ(数値)を読み取りフィボナッチ数を計算して、結果をメッセージとして流すという処理になっています。この程度の計算なら、ワーカーオフロードパターンでなくてもよいのでは?と思われるかも知れませんが、サンプルですのでご容赦を。
サンプルソースコードは、GitHub の Gist にアップロード しておきました。なお、
---- app.js -----
app.js 本体
---- package.json ----
package.json の内容
となっていますのでご注意を。
以下はソースの要点です。細かい部分は割愛しました。
// Setup messaging API info for Order system
var PUBLISH_TOPIC = "mqlight/example/result";
var SUBSCRIBE_TOPIC = "mqlight/example/request";
var SHARE_ID = "example";
var mqlight = require('mqlight');
// Create Client, and wait for message to process.
var mqlightClient = mqlight.createClient(mqlightService, function(err) {
mqlightClient.on('message', processMessage);
mqlightClient.subscribe(SUBSCRIBE_TOPIC, SHARE_ID);
});
ここでは、 MQLight のクライアントを作成し、サブスクライブするトピックを指定します。メッセージが来たら、"processMessage" で処理します。
function processMessage(data, delivery) {
var request = data;
var fibNum = fib(parseInt(request.number, 10));
var replyData = {
result: fibNum,
status: 'SUCCESS'
};
replyData = JSON.stringify(replyData);
console.log("Sending response: " + replyData);
mqlightClient.send(PUBLISH_TOPIC, replyData, {
ttl: 60*60*1000 });
}
processMessage() では、受け取ったメッセージ(ここでは数値)を解釈し、fib() 関数にて計算を実行した後に、PUBLISH_TOPIC にメッセージを送信します。
このアプリケーションは Bluemix に対してワーカープロセスとしてデプロイします。
ワーカープロセスは、ルートなしアプリケーション (--no-route)です。このアプリケーションをデプロイするための manifest.yml は以下のとおりです。
applications:
- name: worker
no-route: true
memory: 256M
disk_quota: 1024M
path: worker
command: node app.js
services:
- mqlight
ここで、no-route : trueに着目してください。この指定で、ルートなし(つまり URLを持たない)アプリケーションがデプロイされます。"cf push”コマンドで、アプリケーションをデプロイすると以下のメッセージがでます。
$ cf push worker
.
App worker is a worker, skipping route creation
.
出ているメッセージをみると、App worker is a worker, と出力されていますね。実際に、Bluemix 上でも worker (ワーカー)と呼ばれることがわかります。正常にアプリケーションが稼動できたら、動作を確認します。
動作確認
フィボナッチ数列は
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946
などの数列です。最初を0(ゼロ)とするのが最新のようですが、ここでは1から開始しました。
Node-REDの Inject に2を入れると右側の Debug に 1 と出ます。Injectに20 を入れると 10946 が返ってきました。正常に動作しているようですね。
なお、MQLight サービスにはダッシュボードがあるので、これらを眺めてもよいでしょう。
まとめ
今回は、エンドユーザーに対する応答を速くするためのワーカーオフロードパターンと、ワーカーオフロードパターンをどのように Bluemix 上で実装するかをご紹介しました。さまざまな処理を細かく分散させる、マイクロサービスの考えに通じるかと思われます。
実装例は1つのプロセスで実施しましたが、重たい処理依頼がたくさんくる場合には、Bluemix のオートスケール機能を利用してもよいでしょう。