どういう経緯で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段落目