HerokuでSinatraでmemcachedしようよ!
「NoSQL データベースファーストガイド」(著:佐々木達也)という本を読んでるよ。各種NoSQLのひと通りの説明とそれぞれにRubyを使ったサンプルがあって、僕のようなNoSQL知識ゼロ(NoKnowledge)の人にとってはとてもためになるよ。特にサンプルは各NoSQLの利用状況を想定して作られているから、実用的でうれしいよね。
NoSQLデータベースファーストガイド by 佐々木 達也
その中に音楽視聴ランキングサイトの楽しいサンプルがあるんだよ。それはGyaoの音楽ランキングに基づいて、YouTubeから対応動画を取ってきてリスト表示するというものだよ。一度アクセスしたデータはmemcachedを使ってサイト側で保持することで、サイトのレスポンスを良くすると共に、他サイトへの負荷を下げるという例だよ。
早速僕も同じようなサイトを作ってmemcachedの使い方を学ぶよ。本の例はRailsをベースにしてるんだけど、僕はRailsをよく知らないのでここではSinatraを使うよ。そして折角だからHerokuにpushもしてみるね。
memcached
環境はOSX Snow Leopardだよ。ローカルでmemcachedを使うには、brew install memcached
すればいいよ。very verboseモードでの起動はこうだよ。
$ memcached -vv
Herokuでmemcachedを使う場合はadd-onするだけでいいみたいなんだ1。
$ heroku addons:add memcache
5MBまでは無料だけど2add-onを利用するには、クレジットカード情報などによる登録が必要だよ。登録しないで上のコマンドを実行すると、登録サイトを教えてくれるからそれに従ってね。
最初にmemcachedは何かということなんだけれども、僕はこれをリクエスト間の共有メモリ空間と理解したんだ。普通Webサーバはステートレスつまり、ユーザからの各リクエストは別々のメモリ空間で処理されるんだけど、memcachedを使うと多数のリクエストが一つの共有メモリ空間を利用できるようになる。つまりmemcachedはWebサーバをステートフルにする、こう理解したんだけどあってるかな?
GyaoRankサイト
さて最終的にでき上がったものは以下にあるよ。
まあ見た目がちゃちいけど、サンプルだから許してね..調べたらGyaoでは音楽以外のランキングデータも配信していたので3、ここではそれらにも対応してみたよ。ヒット率がかなり悪いけど..
ファイル構成をまず示すよ。
├── Gemfile
├── Gemfile.lock
├── app.rb
├── config.ru
├── system_extensions.rb
└── views
├── index.haml
├── layout.haml
└── style.scss
Gemfileの中身は以下のとおりだよ。
source :rubygems
gem 'sinatra'
gem 'haml'
gem 'sass'
gem 'dalli'
gem 'nokogiri'
本の例ではmemcachedのRubyインタフェースにmemcache-clientというのを使ってるんだけど、HerokuではSASL4というセキュリティ上の認証ができるgemしか使えないようなんだ。だからSASLに対応したDalliという変わった名前のgemを使っているよ。余談だけどdalliとnokogiriを並べて書くと、僕にはdankogaiに見えてしょうがないんだよ!
SinatraでDalliを使うときは例えば以下のようにするよ。
set :cache, Dalli::Client.new(ENV['MEMCACHE_SERVERS'],
:username => ENV['MEMCACHE_USERNAME'],
:password => ENV['MEMCACHE_PASSWORD'],
:expires_in => 1.day)
data = settings.cache.get(key)
settings.cache.set(key,data)
つまりDalli::Clientオブジェクトをcacheという変数にセットして、getでkeyに対応するデータの取得をし、setでkeyにdataを保存する。データの保持期間はオブジェクト生成時のoptionで指定できる。optionを指定しないならmemcache serverの指定を含めて引数は不要だよ。
次にapp.rbだけど、ちょっと長いので分けて要点だけ説明するよ。
get '/:term/:category' do |term, category|
@term, @category, @date = term, category, Date.today
@videos = videos(term, category, @date.to_s)
@urls = %w(daily weekly newly).product %w(music movie drama anime owarai variety all)
haml :index
end
Gyaoではmusic movie drama anime owarai variety allの各カテゴリデータにつき、daily weekly newlyという期間データを用意しているんだ。だからgetではそれらをパラメータとして取って、これに応じたビデオのリストをvideosメソッドで取得するようにしている。また@urlsはプルダウンメニューで使っているよ。
videosメソッドを見るよ。
helpers do
def videos(*term_category_date)
key = term_category_date * '/'
if vdata = settings.cache.get(key)
vdata
else
vdata = get_videos(*term_category_date)
settings.cache.set(key, vdata)
vdata
end
rescue
get_videos(*term_category_date)
end
end
ここではmemcachedにアクセスするためのkeyとして渡された引数term, category, dateを / で連結したものを使っているよ。最初にmemcachedに同じキーのデータがあるか見てなければget_videosメソッドでGyaoとYouTubeにアクセスして、データを取得しmemcachedにセットする。rescueでmemcachedが死んでる場合にも一応対応したよ。
次にget_videosメソッドを見るよ。
helpers do
def get_videos(*term_category_date)
ranking = rank_data(term_category_date.first 2)
video_data(ranking)
end
end
ここではrank_dataメソッドでGyaoにアクセスし、内容を解析してランキングデータを取得し、video_dataメソッドでそのデータに対応する動画をYouTubeから取得しているよ。
で、具体的なこれらの処理の内容は次のとおりだよ。Gyaoのデータはrssライブラリを使って、YouTubeのデータはnokogiriライブラリを使って解析しているよ。やっていることは本の例と基本的には同じだよ。
helpers do
def rank_data(*term_category)
rss = open( URL(:rank) + term_category*'/' ) { |f| RSS::Parser.parse f.read }
rss.items.inject([]) { |mem, item| mem << item.title.scan(/[^「」]+/) }
end
def video_data(ranking)
ranking.thread_with(true) do |artist, title|
opts = ["vq=" + URI.encode("%s %s" % [artist, title]), "format=5"]*'&'
entry = Nokogiri::XML(open [URL(:video), opts]*'?').search('entry').first
data = { artist: artist, title: title }
if entry
q = { url: entry.xpath('media:group/media:content').first['url'],
type: entry.xpath('media:group/media:content').first['type'],
count: entry.xpath('yt:statistics').first['viewCount'] }
data.update q
end
data
end
end
def URL(code)
{ video: 'http://gdata.youtube.com/feeds/api/videos',
rank: 'http://gyao.yahoo.co.jp/rss/ranking/c/' }[code]
end
end
ただここではビデオの取得にスレッドを使っているよ。折角だからここではこの間作った Enumerable#thread_withを使ってみたよ。これはsystem_extensions.rbというファイルに置いてあるよ。
system_extensions.rbの中身はこうだよ。
# encoding: UTF-8
module Enumerable
def thread_with(order=false)
mem = []
map.with_index do |*item, i|
Thread.new(*item) do |*_item|
mem << [i, yield(*_item)]
end
end.each(&:join)
(order ? mem.sort : mem).map(&:last)
end
end
class Fixnum
def day
self##60*24
end
alias days day
end
module Kernel
def requires(*features)
features.each { |f| require f.to_s }
end
end
Fixnum#dayとKernel#requiresは app.rbでつかってるんだけどまあおまけだよ。
layout.hamlとindex.hamlには特に面白いところはないので説明は省くね。
NoSQLってなんかもっと難しいものをイメージしてたけど、全然そんなこと無いんだね。
(追記:2011-7-11)Dalli::Client.newの引数を修正しました。5
blog comments powered by Disqus