rubyの軽いWebアプリケーションフレームワークを試してみた

さくらのレンタルサーバで、CGI+rubyで簡単なアプリケーションを作っているのだけど、ライブラリはrubyに標準で添付している cgi ライブラリを使っている。1画面で完結するアプリなら、cgiライブラリでも良いのだけど、もう少し複雑なアプリを簡単に作るには、フレームワークを使いたいところ。
かといって、DBを使う程でもないアプリには、Ruby on Railsは少し大げさな気がするし、CGIしか使えないさくらだと、結構重そうだ。
というわけで、もう少し軽いWebアプリケーションフレームワークをネット上で探してみた。Camping、Rack、Ramazeがなかなか良さそうだったので、この3つのフレームワークを試してみた。

Camping

http://camping.rubyforge.org/files/README.html
ソースが4kb以下という軽量フレームワーク
http://code.whytheluckystiff.net/camping/wiki/KeepItToJustOneFile
1つのファイルに、アプリの全ての構成要素(MVC)を入れるというポリシーが面白い。


お決まりのHello Worldアプリはこんな感じ。Controllerだけの単純な構造なので、あまりCampingらしさがないかも。

#!/usr/bin/ruby
require 'rubygems'
require 'camping'

Camping.goes :HelloWorldApp

module HelloWorldApp::Controllers
  class Index < R '/'
    def get
      "Hello, World."
    end
  end
end

puts HelloWorld.run

でもソースが短いからと言って、CGI起動が速いわけでない。
「Camping.goes :HelloWorldApp」で、HelloWorldAppクラスの動的評価をしているせいか、今回試したフレームワークの中では、起動が一番遅いようだ。詳細は、後述の起動性能比較を参照のこと。CGIでない環境なら、問題にならないと思うけどね。

Rack

http://rack.rubyforge.org/
正確には、Webアプリケーションフレームワークというより、フレームワークを作るためのライブラリで、Webサーバとフレームワークをつなぐためのものなのだけど、最小限の機能を持ったフレームワークとしても利用できる。
http://rack.rubyforge.org/doc/files/SPEC.html
WebサーバとのI/Fは、メソッド呼出の引数1つ(Hash)と戻り値(Array)のみというシンプルさが良い。


Hello Worldアプリ(CGI用)はこんな感じ。

#!/usr/bin/ruby
require 'rubygems'
require 'rack'
class HelloWorld
  def call(env)
    [200, {'Content-Type'=>'text/html'}, ["Hello, World."]]
  end
end
Rack::Handler::CGI.run HelloWorld.new

戻り値が、ステータスコードとヘッダ、コンテンツのArrayになっているメソッドになっている。上の例では使っていないけど、引数envはHashになっていて、パスやクエリとかが入っている。Rack::Requestを使えば、リクエストパラメータを簡単に取得する事もできる。
難点は、やはりフレームワークとして使うには機能が不足気味な点か。このまま使っても良いんだけど、結局、自作フレームワークをRackの上に作る事になってしまいそう。
例えば、HTML生成用にErubis等のテンプレートを呼び出す場合、以下のようになるのだけど、Erubisを呼び出すコードをいちいち書かなくて済むように、フレームワークっぽいものを作ることになってしまいそう。

#!/usr/bin/ruby
require 'rubygems'
require 'rack'
require 'erubis'

class HelloWorld
  def call(env)
    [ 200, {'Content-Type'=>'text/html'},
      [Erubis::EscapedEruby.load_file("hello.eruby").evaluate] ]
  end
end
Rack::Handler::CGI.run HelloWorld.new

Ramaze

Ramaze • The Web Framework for Rubyists
利用できるテンプレートやORM等の自由度が高いフレームワーク
公式サイトへ行くと、ドキュメントやサンプルが豊富なのも好印象。


Hello Worldアプリはこんな感じになる。

#!/usr/bin/ruby
require 'rubygems'
require 'ramaze'

class MainController < Ramaze::Controller
  map '/'

  def index
    "Hello, World"
  end
end

Ramaze.start :adapter => :cgi

テンプレートを使うのも簡単。メソッド名と同じ名前のテンプレートファイルを view ディレクトリの下に置くだけ。Erubisを使うなら、以下のような view/index.rhtml を置くだけ。

<html><body><%= @content %></body></html>

アプリのソースの方は、以下のようになる。

#!/usr/bin/ruby
require 'rubygems'
require 'ramaze'

class MainController < Ramaze::Controller
  map '/'
  engine :Erubis

  def index
    @content = "Hello, World"
  end
end

Ramaze.start :adapter => :cgi

MainControllerのindexを実行したあと、テンプレート view/index.rhtmlが実行される。このとき、インスタンス変数 @content がテンプレートに渡される。
ちなみにテンプレートエンジンは拡張子で判断するので、「engine :Erubis」は省略可能です。

起動性能比較

CGI起動でも軽いWebアプリケーションフレームワークを探すのが目的だったので、起動性能比較もしてみた。起動性能比較といっても、Hello WorldアプリにHTTPリクエストを100回投げて、その所要時間を比較するという単純なもの。
HTTPリクエストを投げるrubyスクリプトは、benchmarkライブラリを利用し、以下のものを用意した。

require 'benchmark'
require 'open-uri'

count = 100
count = ARGV[0].to_i if ARGV[0]

class Loader
  def initialize(n = 1, prefix = '')
    @prefix = prefix
    @n = n
  end
  def load(path)
    contents = ''
    @n.times do
      open(@prefix + path) do |f|
        contents = f.read
      end
    end
    # puts contents
  end
end

loader = Loader.new(count, "http://localhost/~NAT/cgi/") # prefixは環境に応じて適当に書き換える

Benchmark.bmbm do |r|
  r.report("cgi") { loader.load("hello_cgi.cgi") }
  r.report("cgi+erubis") { loader.load("hello_erubis.cgi") }
  r.report("rack") { loader.load("hello_rack.cgi") }
  r.report("rack+erubis") { loader.load("hello_rack_erubis.cgi") }
  r.report("ramaze") { loader.load("hello_rack_erubis.cgi") }
  r.report("ramaze+erubis") { loader.load("hello_ramaze_erubis.cgi") }
  r.report("camping") { loader.load("hello_camping.cgi") }
end

Hello Worldアプリは、ここまでフレームワーク毎に紹介したものを使った。
なおcgiを使ったHello Worldアプリは、下記のものを使った。

#!/usr/bin/ruby
require 'cgi'

cgi = CGI.new
cgi.out() do
  "Hello, World."
end

cgi+erubisは下記の通り。

#!/usr/bin/ruby
require 'rubygems'
require 'cgi'
require 'erubis'

cgi = CGI.new
cgi.out() do
  Erubis::EscapedEruby.load_file("view/index.rhtml").
    evaluate({:content => "Hello, World"})
end

手元のMacBook Pro(CPU:Intel Core 2 Duo 2.2Ghz, メモリ:2GB, OS:Mac OS X 10.5.4)で実行したところ、下記のような結果になった。

                    user     system      total        real
cgi             0.100000   0.030000   0.130000 (  1.850957)
cgi+erubis      0.100000   0.040000   0.140000 ( 10.470253)
rack            0.120000   0.050000   0.170000 (  8.563580)
rack+erubis     0.130000   0.050000   0.180000 (  9.949786)
ramaze          0.130000   0.050000   0.180000 (  9.896040)
ramaze+erubis   0.150000   0.070000   0.220000 ( 27.786989)
camping         0.150000   0.100000   0.250000 ( 53.961681)

totalの差がそれほどなくとも、realの差が大きい場合があるのだけど、この違いはなんなのだろうか・・・?
realに着目すると、erubisを使う場合だと、rack+erubisが速い。cgicgi+erubisの差と、rackとrack+erubisの差を比べると、rackの方が小さいのだけど、この違いもよく分からない。totalに着目すると、cgi+erubisとrack+erubisの速さが逆転するし・・・。
なんとなくだけど、realの結果が一番体感速度に近い気がする。体感的には、cgi+erubis ≒ rack+erubis < (性能2倍の壁) < ramaze+erubis < (性能2倍の壁) < camping という感じ。

分からないことだらけだが、とりあえずtotalとrealのいずれでも、cgiが一番速く、campingが一番遅い、といったところは言える。


私的総合評価

フレームワークの評価は、色んな側面から評価すべきだと思うので、簡単にどっちが良いとは言えないのだけど、下記に示すような個人的な好みと、私のやりたいことと照らし合わせると、ramazeが良さそう。

  • 個人的な好み
    • コード記述量は少ない方が良い
    • テンプレートには、使い慣れたErubisを使いたい
    • 新しく覚える事が少ない方が良い
  • 私のやりたいこと
    • さくらのレンタルサーバで使いたいので、CGIしか使えない。デーモンプロセス不可
    • CGIでもそこそこの性能が出て欲しい
    • 数画面程度の小規模なアプリを手早く作りたい

というわけで、しばらくramazeを使ってみようかと思う。