どういう経緯でHadoopを使うことになるのだろう?

Hadoopオライリー本(Tom White著, 玉川竜司、兼田聖士訳, オライリージャパン, 2010)、いわゆる「象本」のケーススタディを読んで、最初は「どういう経緯でHadoopを使うことになるんだろう?」と思いました。

つまり、大量のデータをバッチ集計する時に使うことは想像できたのですが、それはデータベースの構造などで解決できるのではないかと。結論から言えば、私がこの本をよく読んでなかったのが原因です。

単純な事例で考えてみた

例えば、会員の入会月別・男女別棒グラフを表示したいというケースがあったとします。次のグラフは、jqPlotのBar Chartを使い、AJAXでデータを取得・表示することにします。

あえて一番、間違った実装方法を示します。この方法が駄目なのは、入会者数が増えれば増えるほど表示が遅くなるからです。

class UsersController < ApplicationController
  def index
    counts = {}
    year = (params[:year] || Date.today.year).to_i

    start_dt = Date.new(year, 1, 1)
    end_dt = Date.new(year, 12, 31)
    # signed_atは入会日。Userモデルのbefore_validationで、現在日を代入している。
    stats = User.where(["signed_at >= ? AND signed_at <= ?", start_dt, end_dt])
    stats.each {|stat| counts[stat.sex] = counter(counts[stat.sex], stat.signed_at.month)}
    render :json => counts
  end
  # 省略
  private
  def counter(count, n, i = nil)
    count = {} if count.nil?
    if i.nil?
      count[n] = count[n].nil? ? 1 : count[n] + 1
    else
      count[n] = i
    end
    count
  end
end

TDDで開発していても、これで負荷テストをしていない場合は通ってしまいます。ダミーデータとして302,600レコードをデータベースのusersテーブルに用意し、ローカル環境*1で実行したみた結果、14秒弱かかりました。

Started GET "/users" for 127.0.0.1 at Fri Jan 14 10:53:28 +0900 2011
  Processing by UsersController#index as */*
Completed 200 OK in 13566ms (Views: 1.4ms | ActiveRecord: 1976.4ms)

別テーブルを用意してみる

このようなグラフを表示する場合は、入会時にUserモデルのafter_createフィルターで、usersテーブルとは別に統計情報をまとめる別テーブル、例えばuser_statisticsテーブルを用意し、それをUsersControllerのindexメソッドでfindした方がよいでしょう。

class User < ActiveRecord::Base
  # 省略
  after_create :update_statistics

  private
    def update_statistics
      stat = UserStatistic.where(["year = ? AND month =? AND sex =?", self.signed_at.year, self.signed_at.month, self.sex]).first
      if stat.nil?
        stat = UserStatistic.new(:year => self.signed_at.year, :month => self.signed_at.month, :sex => self.sex, :count => 1)
      else
        stat.count += 1
      end
      stat.save!
    end
  # 省略
end

この場合は、どんなに入会者が増えても最大で24レコード(12ヶ月・2つの性別)しかありません。

class UsersController < ApplicationController
  def index
    counts = {}
    year = (params[:year] || Date.today.year).to_i
    stats = UserStatistic.where(:year => year)
    stats.each {|stat| counts[stat.sex] = counter(counts[stat.sex], stat.month, stat.count)}
    render :json => counts
  end
  # 省略
end

この結果、4ミリ秒になりました。

Started GET "/users" for 127.0.0.1 at Fri Jan 14 10:54:38 +0900 2011
  Processing by UsersController#index as */*
Completed 200 OK in 4ms (Views: 1.5ms | ActiveRecord: 0.4ms)

ん?これでよくね?なにが起因してHadoopを使うことになるんだろう...

能動的な集計には対応できない

予め想定されているケースは、上記のように別テーブルを用意するなどして、統計情報をとっておけばいいわけですが、データを能動的に集計する場合は、これが通用しなくなります。例えば:

先月15日に発売した女性雑誌にサイトの広告を掲載したが、その宣伝効果を見たいので、先月と前年同月の日毎で女性入会者数をだして。

という要求にはこの場合は即座に応えられません。また、これだけのためにシステムを改編するのも大変です。しかも、「明日の朝の会議で使うから」などと18時頃に言われた日には...まぁ、よくある話ですが。

よく読めば書いてあった

象本の『1.3 他のシステムとの比較』に書いてありました。以下その抜粋。*2

・・・MapReduceはバッチ的なやり方であり、データセットの全体を分析する必要がある問題、特に非定型の分析の場合に向いています。・・・

ビジネスの現場においては、非定型な分析が繰り返し行われ、マーケット分析などに用いられています。仮にHadoop(MapReduce)のようなものが無かったら、連日徹夜して表計算ソフトのワークシートを切り貼りするなどして集計することになったことでしょう。しかし、それすらも通用しないデータ量に直面し、また「仕様上できません」などという言い訳も通用しなくなる前に、せめてHadoopで実験的な分析システムを研究・構築しておくのも、あながち損ではないかもしれません。

*1:ruby 1.8.7, Rails 3.0.3, MySQL 5.1.46, production環境, Mac OS 10.6.6, 2.33GHz Intel Core 2 Duo, 2GB SDRAM

*2:p.5、3段落目