今回はNode.js + MongoDBの組み合わせを使ったシステムのアーキテクチャについて書いてみようと思います。コードの類いは殆どありません。
非同期ノンブロッキング、Webサービス全般
MongoDBが主の記事ですが、題名はNode.jsから始まってます。これは今回のアーキテクチャ考のスタートポイントがNode.jsだからです。Node.jsの特徴というと
- サーバーサイド javascriptでそれなりに高速
- 非同期ノンブロッキング主義
- 対話型インタープリタで色々試しながら開発できる
というのが個人的に気に入っている点なんですが、これってWebサービス全般を書くにはよさそうだなぁとか普段から考えていました。どうしてかと言うと、一般にWebサービスと言えば
- 不特定多数が不特定なタイミングで利用する
- 多数のユーザーに個別な処理がある
- 多数のユーザーでの共有な処理がある
- レスポンスは早ければ早いほど良い
といった特徴を持っていますが、
- 1と2はまさに非同期で、ノンブロッキングにすることで、限りある資源で最大限に処理を行えるし
- 3はきちんと識別をして共有する空間に対する処理を非同期ノンブロッキングで作ればよさそうだし
- 4は上記をやればそれなりに向上できるな
と言えるからなんです。
Webサービス全体で共有するObject空間
PHPとかでWebサービスを書いているとApacheにリクエストが有る度にPHPスクリプトが実行され、セッションの読み込み、DBへの接続、クエリの実行、ロジックの実行、HTMLの生成、レスポンスといった流れになっています。キャッシュなどがありますから、コードのコンパイル等は最小限になりますが、個々のリクエストは独立事象であり、個々にDBからモデルを作り、ロジックをあてがって出力を作っていますから、毎度セッションのファイルやDBからのデータを読んで解釈してオブジェクトを作って操作しています。
毎度読み込みしているのが遅いのかなぁ〜なんて考えたので、じゃ、メモリに載っけようと考えたとき、ヒラメキました。
共有すべきもの、個別に持つもの、全部メモリに展開してれば早いし、共有すべきものはメモリに載っている1つのインスタンスとされているなら、もしかしてロッキングとかいらないんじゃない?
javascriptはご存知の通り、Objectに後付けで何でも放り込めます。ですので、オンメモリのObjectに必要に応じてデータを付け加えたり、機能を追加するのも簡単です。このObject達を必要なだけオンメモリにして、ユーザーからの問合せに必要なやつに声をかけるようにするのです。
このObject空間はパフォーマンス重視で、HTMLの生成とかを外在化させるなら、こいつは内部APIサーバープロセスとして実装するのが良さそうです。TObjectという基底のObjectを定義するとしたら以下のような感じでしょう。
var TObject = function( om, oid ) {
var myself = this;
myself.className = 'TObject';
myself.objectManager = om;
if ( oid !== undefined ) {
myself.oid = oid;
} else {
myself.oid = myself.objectManager._genStrIdSync();
}
myself.meta = {};
myself.metaHide = {};
myself.body = {};
myself.status = {};
myself.statusHide = {};
myself.cache = {};
myself.__initMetaSync();
myself.__initMetaHideSync();
myself.__initBodySync();
myself.__initStatusSync();
myself.__initCacheSync();
myself.__initEventListenerSync();
}
util.inherits( TObject, events.EventEmitter );
/* 〜色々な処理〜 */
/* APIに公開するメソッドの例 */
TObject.prototype.getNameSync = function() {
var myself = this;
return myself.meta.name;
}
module.exports = TObject;
これやこれの派生クラスのオブジェクトを管理するObjectManagerは以下のような感じかな?
var META_DB = 'meta';
var BODY_DB = 'body';
var DB_SERVER_HOST = '127.0.0.1';
var DB_SERVER_PORT = 27017;
var ObjectManager = function( callback ) {
var myself = this;
events.EventEmitter.call( this );
myself.class_name = 'ObjectManager';
myself.parentCallback = callback;
// Object空間はoidをキーにしたdict
myself.objects = {};
myself.classNames = Objects;
myself.metaDb = new mongodb.Db(
META_DB,
new mongodb.Server(
DB_SERVER_HOST,
DB_SERVER_PORT,
{
auto_reconnect : true,
poolSize : 10,
socketOptions : {
timeout : 0,
keepAlive : 0,
encoding : 'utf8'
}
}
),
{ w : 1 }
);
myself.bodyDb = new mongodb.Db(
BODY_DB,
new mongodb.Server(
DB_SERVER_HOST,
DB_SERVER_PORT,
{
auto_reconnect : true,
poolSize : 10,
socketOptions : {
timeout : 0,
keepAlive : 0,
encoding : 'utf8'
}
}
),
{ w : 1 }
);
myself.metaDb_is_opened = false;
myself.bodyDb_is_opened = false;
myself.on( 'db_opened', myself.start );
myself.metaDb.open( function( err, client ) {
//var myself = this;
if ( err ) {
myself.exit( err );
} else {
myself.metaDb_is_opened = true;
myself.emit( 'db_opened' );
}
});
myself.bodyDb.open( function( err, client ) {
//var myself = this;
if ( err ) {
myself.exit( err );
} else {
myself.bodyDb_is_opened = true;
myself.emit( 'db_opened' );
}
});
}
util.inherits( ObjectManager, events.EventEmitter );
/* 〜色々な処理〜 */
/*
* メソッド: createObject
* API用 Objectを新規に作成する
*/
ObjectManager.prototype.createObject = function( className, params, callback ) {
var myself = this;
// まずは与えられたクラス名のクラスがあるかどうか
// 内部APIなので基本的に入力パラメータを信じる
var myclass = eval( 'myself.classNames.' + className );
if ( myclass == undefined || myclass == null ) {
callback( new Error( 'Class not found' ), {} );
return false;
}
// Objectをとりあえず作成
var obj = new myclass( myself );
var oid = obj.oid;
// paramsの内容で初期化
// paramsはkey + valueだが、valueはURLクエリパラメータなので基本文字列
// Name
if ( params.name !== undefined && params.name !== null ) {
if ( params.name.constructor == String ) {
obj.setNameSync( params );
}
}
// Dir
if ( params.dir !== undefined && params.dir !== null ) {
if ( params.dir.constructor == String ) {
obj.setDirSync( params );
}
}
// Owner
if ( params.owner !== undefined && params.owner !== null ) {
if ( params.owner.constructor == String ) {
obj.setOwnerSync( params );
}
}
// Objectを空間に配置
myself.objects[ oid ] = obj;
// ここでコールバックしておく
callback( null, Tool.copy( obj.meta ) );
// コールバック後にobjectをflush
myself.objects[ oid ]._flushChain();
return true;
}
/*
* メソッド: cmdObject
* API用 Objectのメソッドをコールする(準備をする)
*/
ObjectManager.prototype.cmdObject = function( oid, method, params, callback ) {
var myself = this;
// Objectがオンメモリにあるかどうか確認 無ければ読んでから、有ればすぐに次の処理へ
if ( myself.objects[ oid ] !== undefined ) {
myself.__callObjectMethod( myself.objects[ oid ], method, params, callback );
} else {
myself.__loadObject( oid, function( err, meta ) {
if (err) {
callback( err, {} );
} else {
myself.__callObjectMethod( myself.objects[ oid ], method, params, callback );
}
});
}
return true;
}
上記コードは今書いているものの抜粋です。一杯色々抜けてます。イメージが伝われば・・・。
express等で書いたAPIサーバーからObjectManagerを読み込んでインスタンスにすると、APIサーバー上にObject達の空間が出来上がり、ObjectManagerを通じてAPIからObjectに指示を出す事ができます。
MongoDBは永続化用と検索用
ところで上記のコードにMongoDBのクラスを呼んでいるところとかあったのにお気づきになられたでしょうか?
このObjectManagerではObjectの永続化にMongoDBを使っています。TObjectの初期化にありますが、実はObjectの内部を複数の「部分」にわけています。ここの例はまだ開発中ですからこれからどんどん変わる可能性がありますが、今はこんな意図があります。とりあえず参考までにご覧下さい。以下のMeta、MetaHide、BodyがMongoDBで永続化され検索に用いられます。
Meta
Objectの一覧や検索などの用途に使う領域です。必ずオンメモリになります。MongoDB上でもmetaというDBに保存されます。ユーザーにとって一番良く見える部分で、MongoDB上でもクエリで使われます。
MetaHide
Metaの隠れ属性版です。MongoDB上に載っけます。例えば、Objectのプロパティを明示せずに、キーワードだけで検索したい場合用の単語リスト等が格納されています。こうするとClassNameは分からんけどhogeを含む何かを探してみたいなことができます。Object取得時にはこっちは表示されません。
Status
Metaと似た様なものですが、MongoDBに載っけません。永続化する必要が無いObjectに関する概要の情報を入れます。例えばそのObjectに該当するユーザーのオンライン状態などを入れます。Metaと同様Objectを取得した時に一緒に入ってきます。MongoDBに載らないのでMongoDBでの検索はできません。検索対象にならないものをこっちに入れます。
StatusHide
Statusの隠れ属性版です。setTimeoutのtimeoutIdとかを持っていると幸せになれます。
Body
そのObjectの持つべきメインのデータです。簡単なものならMetaに全部入れてもいいでしょう。でも大きいものはこっちで。そのかわり検索対象になりません。
Cache
Bodyのオンメモリイメージ以外に何か持たせておきたいものを置きます。まだ用途は不明
最後に
以上、現在私が開発中のさるAPIサーバーの要点をかいつまんで紹介いたしました。MongoDBの良いところとしてJSONがほぼそのまま保存でき、構造の変化も自由自在といったところが挙げられます。Node.jsの非同期ノンブロッキングからの思いつきにMongoDBの特性がちょうどマッチしてこの形になっています。MongoDBのおかげで本当に開発が楽です。
さて、「アーキテクチャ」って言いましたが、今回私が説明したような事を思いついて形にするのがIT業界における「アーキテクト」のお仕事かなと思っています。大きくは問題の構造の解き明かし、中くらいでは利用するソフトの特性という構造の理解、小さくは個々の部品の構造。こういったものの解はそれこそ無数にありますが、それをそれなりの時間で選別し、提供する。そういったお仕事をさせて頂いております。
最初はexpressを使った例を書こうかなんて簡単に考えていたのですが、既に投稿されていたので、急遽変更しました。
引き続き開発・検証を進めていますので、そのうちなんかしら公開しようと思ってます。
ありがとうございます。それではまた。