2011年12月31日土曜日

MongoDBでお気楽MapReduce

MongoDBにはMapReduceの仕組みが用意されてて楽しそうですよね。

MapReduce - Docs-Japanese - 10gen Confluence

ちゃんと日本語。

で、コンソールで入力しながらの開発は面倒なので、こんな時はMongo VUE。安くならないかなー。

Yet another MongoDB Map Reduce tutorial | MongoVUE

使い方、これじゃよく分からない。

How to perform MapReduce operations in MongoVUE | MongoVUE

こっちですね!この手順どおりに実行すれば、ちゃんとMapReduce!Sharding環境ならそれぞれのShardで分散処理してくれます。

せっかくなので、以前作ったMongoTraceListnerのサンプルを利用して、各URLの処理時間の平均をMapReduceで計算してみます。

public class PerformanceTraceModule : IHttpModule
{
  private string ItemKey = "_mongoDbTraceStart";

  public PerformanceTraceModule()
  {
  }

  public void Init(HttpApplication context)
  {
      context.BeginRequest += new EventHandler(context_BeginRequest);
      context.EndRequest += new EventHandler(context_EndRequest);
  }

  void context_BeginRequest(object sender, EventArgs e)
  {
      var httpContext = (sender as HttpApplication).Context;
      var startTime = httpContext.Items[ItemKey] = DateTime.Now;
  }

  void context_EndRequest(object sender, EventArgs e)
  {
      var httpContext = (sender as HttpApplication).Context;
      var startTime = (DateTime)httpContext.Items[ItemKey];

      Trace.WriteLine(new
      {
          Method = httpContext.Request.HttpMethod,
          Status = httpContext.Response.StatusCode,
          RawUrl = httpContext.Request.Url.ToString(),
          Milliseconds = (DateTime.Now - startTime).Milliseconds
      });
  }

  public void Dispose()
  {
  }
}

このHttpModuleを仕掛けて処理時間をMongoDBに入れます。

実行!何度も何度もリロードやら、About、Loginへのアクセスを繰り返す。

mr2

データも溜まって来ました。

mr1

ここで、MapReduceを実行。

Map

mr3

Reduce

mr4

Finalize

mr5

In & Out

mr6

結果

mr7

ただ、ここでmap/reduce/finalizeのコードに間違いがあっても、わかりにくい。どこのなにがエラーなのかサッパリ。そんな時はコンソールを見てみましょう。サービス化してたらログファイル。

mr8

finalizeで適当にエラーコードを入れて実行するとコンソールにエラーメッセージが出てます。開発中はMongoDB Shellでやるか、ローカル実行してコンソール確認しながらすすめるのがいいかもー。

Mongo VUEやShellで見るだけじゃなくて、ASP.NET MVCのアプリケーションから見れるようにするために、以下のようにしてみました。

private MongoDatabase GetDatabase()
{
  var serverName = ConfigurationManager.AppSettings["MongoDb:Server"];
  var databaseName = ConfigurationManager.AppSettings["MongoDb:Database"];

  if (string.IsNullOrEmpty(serverName))
      serverName = "mongodb://localhost";

  if (string.IsNullOrEmpty(databaseName))
      databaseName = "TraceListner";

  var server = MongoServer.Create(serverName);
  return server.GetDatabase(databaseName);
}

private BsonJavaScript GetMapReduceCode(string name)
{
  return new BsonJavaScript(System.IO.File.ReadAllText(Server.MapPath("~/MapReduce/" + name)));
}

public ActionResult MapReduce()
{
  var map = GetMapReduceCode("map.js");
  var reduce = GetMapReduceCode("reduce.js");
  var finalize = GetMapReduceCode("finalize.js");

  var collection = GetDatabase().GetCollection("MyTrace");
  var options = new MapReduceOptionsBuilder();
  options.SetFinalize(finalize);
  options.SetSortOrder("_id");
  options.SetOutput(MapReduceOutput.Inline);

  var result = from r in collection.MapReduce(map, reduce, options).GetResults()
               select new
               {
                  RawUrl = (string)r["_id"],
                  Count = r["value"].AsBsonDocument["Count"].ToInt32(),
                  Elapsed = r["value"].AsBsonDocument["Elapsed"].ToInt32(),
                  Average = r["value"].AsBsonDocument["Average"].ToDouble()
               };
  return Json(result.ToArray().OrderByDescending(mr=>mr.Average), JsonRequestBehavior.AllowGet);
}

HomeControllerに追加。

mr9

やったー。出たー。あとは、見た目よろしくしていけばOKでしょう。今回は気にしないけど。

こんな感じでMongoDBにデータ入れとけばMapReduceで集計をMongoDBサーバーに任せられていいですね!

っていう、結論じゃなくて、結局はV8でスクリプト動かしてるから、大量のデータの場合そんなに早くなかったりしてショック。対象データをすべてフェッチしてC#で処理したほうが早かったりも...。

速度的には思ったほどじゃないけど、これはこれで面白いので、PowerPivotとあわせて業務系の人が扱えるようになると、嬉しいですよねー。