Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

MongoDBのCapped CollectionとTailable Cursorを使ったRealTimeAccess集計


Index

  • RealTimeAccess集計
  • Capped Collection
  • Tailable Cursor
  • まとめ

RealTimeAccess集計

RealTimeAccess集計をするためにMongoDBの利用を考えます。サーバーの構成は上図のようなイメージで各種ApplicationServerからFluentdでLogAggregatorにRealTimeでLogデータを転送し、LogAggregator MasterがMongoDBにFluentdで書き込んで行きます。ここで言うRealTimeAccess集計の機能要件を整理すると以下のようになります。

  • Access発生後、1分以内で集計結果をWebツール上で確認したい。集計区間も1分単位など。
  • 複数条件が指定可能で、柔軟なCross集計がしたい。
  • RealTimeAccess集計のSystem負荷を出来る限り抑えたい。

Access発生後の直ぐに確認ということでFluentdが必要、柔軟なCross集計のために1行のAccessLogをそのままMongoDBに格納という方法を採用します。以下ではこれを実現するためのMongoDBの機能であるCapped CollectionとTailable Cursorについて説明します。

Capped Collection

MongoDBにはCapped Collectionというデータ数とサイズの上限を指定できるCollectionがあります。cappedコレクション - Docs-Japanese - 10gen Wiki はてなブックマーク - cappedコレクション - Docs-Japanese - 10gen Wiki
Capped Collectionの良いところは指定した上限サイズに達した場合は古いデータを自動的に削除してくれるところです。よってRealTimeAccess集計のような短時間のみデータが必要なケースに向いています。更に自動削除によりエンジニア泣かせとされる深夜帯の定期削除バッチたるものが不要になります。また通常のCollectionより書き込みが速くなります。速度のパフォーマンス比較は以下の通りです。50万件のInsertでCapped Collectionの方が20%ほど処理時間が短くなります。

$ mongo
MongoDB shell version: 2.2.3
connecting to: test

> use capped_test;
> db.createCollection("cappedcoll", {capped:true, size:100000, max:10000});
{ "ok" : 1 }

> db.createCollection("normalcoll");
{ "ok" : 1 }
var db = new Mongo().getDB( "capped_test" );
var col = db.getCollection( "cappedcoll" );
// var col = db.getCollection( "normalcoll" );

for( var i=0; i<500000; i++ ) {
    col.insert( {"host" : "127.0.0.1", "user" : "-", "method" : "GET", "path" : "/", "code" : "200", "size" : "570", "referer" : "-", "agent" : "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4"} );
}
// cappedcollectionへの書き込み
$ time mongo insert.js
mongo insert.js  18.03s user 1.18s system 42% cpu 44.833 total

// 通常のcollectionへの書き込み
$ time mongo insert.js
mongo insert.js  22.05s user 3.89s system 47% cpu 54.172 total
Collection Write time QPS
Capped Collection 44.833sec 11152
Normal Collection 54.172sec 9229

Tailable Cursor

tailableカーソル - Docs-Japanese - 10gen Wiki はてなブックマーク - tailableカーソル - Docs-Japanese - 10gen Wiki
Capped Collectionを設定した時にTailable Cursorというtail -f コマンドのような機能が利用できます。 これによってMongoDBに新たに追加されたデータだけを取得することができます。以下がTailable CursorのJavascript Shellです。tail.jsというファイルで保存しmongoに流します。これを起動中に上で実行したinsert.jsを流し込むとtail機能が実現できます。

var db = new Mongo().getDB( "capped_test" );
var coll = db.getCollection( "cappedcoll" );
var lastid  = coll.find().sort({ '$natural' : -1 })[0]._id;

while(1){
    cursor = coll.find({ _id: { $gt: lastid } });
    // tailable
    cursor.addOption( 2 );
    // await data
    cursor.addOption( 32 );
    while( cursor.hasNext() ){
        var doc = cursor.next();
        lastid = doc._id;
        printjson( doc );
    }
}
$ mongo tail.js
MongoDB shell version: 2.2.3
connecting to: test

#新たに追加されたデータだけtailのように取得可能
{
	"_id" : ObjectId("520929fe342ac66d8f5148d0"),
	"host" : "127.0.0.1",
	"user" : "-",
	"method" : "GET",
	"path" : "/",
	"code" : "200",
	"size" : "570",
	"referer" : "-",
	"agent" : "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4"
}

柔軟なCross集計をしたい場合はfindのtail.js中のfind条件を指定してあげれば良いと思います。例えばpathは/index.htmlでcodeが200の場合等。

var lastid  = coll.find({path:/\/index.html/, code : "200"}).sort({ '$natural' : -1 })[0]._id;


NodeJSのSever-sent eventを使って一定期間Tailable Cursorで取得したデータをまとめ、ブラウザ側に集計データをPushしてあげれば奇麗なRealTimeAccessGraphが描けると思います。Graph描画について別の機会に書こうと思いますが、Mongooseとmorris.js辺りを組み合わせて書こうと考えています。
Mongoose - デベロッパーズガイド 日本語訳 はてなブックマーク - Mongoose - デベロッパーズガイド 日本語訳
morris.js はてなブックマーク - morris.js

まとめ

  • MongodbのCapped Collectionを使うとCollectionに格納できるデータ数とサイズの上限を指定できる。
  • Capped Collectionは上限を超えたデータを自動削除してくれるので、削除バッチ処理等が不要。
  • Capped Collectionは通常のCollectionよりもWrite処理が速い。50万件のInsertで20%処理速度が向上。
  • Capped Collectionを指定するとTailable Cursorが利用できるので新たにMongoDBに格納されたデータのみ取得可能。
  • Tailable Cursorにより新たに取得したデータを一定時間集計し、NodeJSのSever-sent eventを使ってClient側に送信してGraphを描く事ができる。