昨日のネタではRackで簡単なアプリを作ってそれを複数立ち上げたThinで動かしつつ、表のApacheからmod_proxy_balancerで適当にプロキシしてやるって構成にした。Railsとかでもよくやるので慣れてるし、扱い易いので好きな構成だ。

ただ、今回の遊びでちょっとやってみたかったけことがある。何かというと、Passengerの導入。mod_railsとかmod_rackとか呼ばれてるアレ。スタンドアロンのサーバではなくてApacheやnginxに組み込んで使うタイプで、パフォーマンスもそれなりに良いし使い易いという話を聞いてたので気になってはいた。でもRails使わなくなってからなかなか試してみる機会がなかったので、この際ついでだ、とやってみることにした。

設定は簡単

インストールについてはPassengerのページでも見てもらうとします。別に何のことはない、gemからインストールできるし、俺が借りてるサーバ(OSはUbuntu 8.04)ではapt-getでさくっと入った。

んで。例えば、次のような構成のRackアプリができてたとする。もちろんこの状態でrackupすれば普通に動くのが前提。

/var/www/rackapp
         |
         +-- config.ru
         |
         +-- lib/
         |
         +-- view/

で、rackapp.example.orgってのでアクセスすると上のアプリに処理が移るようにしたいとする。まず、/var/www/rackapp以下にpublic、tmpってディレクトリを作る。publicはhtmlとかcssとかの静的ファイルを置く場所。tmpには、あとで説明するけど、restart.txtってのを置いておく。こうなる。

/var/www/rackapp
         |
         +-- config.ru
         |
         +-- lib/
         |
         +-- view/
         |
         +-- public/
         |
         +-- tmp/
              |
              +-- restart.txt

そこまで用意できたら次はApacheの設定。

<VirtualHost *:80>
ServerName rackapp.example.org
DocumentRoot /var/www/rackapp/public
RackBaseURI /
</VirtualHost>

これだけ。実にさっぱり。特に指定をしなければこれでRACK_ENV=productionでアプリが起動する。色々細かい指定はできるけど、まぁそれは必要になったときにいじればいいよねとりあえず。あと、アプリの修正をした場合はいちいちApacheを再起動しなくても、

$ touch tmp/restart.txt

とかやってrestart.txtのタイムスタンプを更新してやるとPassengerがアプリを再読み込みしてくれる。うわーいできた。なんだよマジ簡単じゃんよー、とか思ってたら甘かった。

な、何も出ないぞ…。

早速これでブラウザからアクセスしてみたら、全く何も表示されない。なんだこれ。config.ruでRack::Lintをuseしてるんだけど、ログ見たらそいつが何か警告出して処理を止めてる。うーん、だけど、全く見に覚えのない警告なので、困惑しつつRack::Lintを外してみる。案の定エラー出て落ちる。どうやら見てると、アプリのcallメソッドに渡されたenvからRack::Requestを作るときにこける様子。なになに、env['rack.input']にrewindメソッドが無い…?

色々調べてたら、Rack::RequestはrewindできるIOオブジェクトが必要なんだけど、PassengerはrewindできないIOを渡してくるらしい。Rackの開発グループでも議論になってる様子。これRackベースのWAFとか作ってると結構致命的だよなー。

rewindが無いと駄目なら、rewindできるオブジェクトにしちゃえ

とまぁつまりそういう話なわけですよ。Passengerが渡してくるIOオブジェクトがrewindできないなら、rewindできるIOオブジェクトに変換するなりラップするなりしてしまえばいいや、と。やっぱり同じこと考えてる人もいるみたいだし、そもそもその人が何を参考にしてるかというとRailsのActionControllerの実装だったりする。というわけで、それにならってとりあえずこんなミドルウェアを作ってみた。

# netakit/rewindable_input.rb
module NetaKit
  class RewindableInput
    def initialize(app)
      @app = app
    end
    def call(env)
      unless env['rack.input'].respond_to?(:rewind)
        env['rack.input'] = StringIO.new(env['rack.input'].read)
      end
      @app.call(env)
    end
  end
end
# config.ru
require 'netakit'
require 'netakit/rewindable_input'

use NetaKit::RewindableInput
use Rack::CommonLogger

map '/resource/kanojo' do
  run NetaKit::Resource::Kanojo.new
end

NetaKit、はfaultier.jpで動いてるアプリのnamespaceなので特に気にしない方向で。まぁこんな感じにしてやると、rack.inputがrewindできないIOだったときはStringIOに変換してからアプリに渡してくれるようになる。やってみたらこれでちゃんと動くようになった。まぁこのままだと入力を使う使わないに拘わらず(例えばRack::Responseを生成せずにenvを生で扱い、かつinputを読む必要のない処理だけをやるようなミドルウェアとかを通してるときにでも)毎回inputをreadしちゃってアレげなので、ちゃんとやろうと思ったら何かのオブジェクトでラップしてやって呼ばれたときに変換かけるようにする方がいいかも。例えばさっきのCloudKitだとこんな風に実装してるとか。まぁ、上に書いたのでも動くっちゃ動くのでお試し程度なら十分だけど。

お手軽感は確かに

というわけで意外に手間どったPassenger対応だったんだけど、rack.inputの問題を除けば簡単に設定できて中々良い。立ち上げるプロセス数の調整とかもApacheが勝手にやってくれるわけだし、静的ファイルはアプリ通さないで返すようにするのにも特に設定が要らないのも楽だ。動かすのがApacheだけなのも余計なこと考えないで済むしいいな。

そういえば、一応参考程度にApache BenchでPassengerとThin+mod_proxy_balancerとのパフォーマンス比べてみたんだけど、ぶっちゃけ殆ど差はない。と言っても/resource/kanojoにひたすらGETリクエスト送っても殆ど静的なレスポンスを返すだけなので本当に参考程度。同時アクセス数を増やしてみたら若干数値が違ってたけど、Thinの立ち上げてるプロセス数次第で変わってくるだろうし、どうせ実運用になるとPassengerの場合でももう一段フロント立ててごにょごにょやるだろうからなぁ。RailsとかMerbとかでがちっとアプリ組んで、mongrel、lighttpd、WEBrick、fcgiとか色々試してみないと何とも言い難い。個人で作ったものくらいだったらPassengerのがやること少なくてお手軽かも、くらいには思った。あとまぁレンタルサーバだったら各自アプリケーションサーバ起動させるとか許さないだろうけど、Apacheに組み込めるんなら入れといてくれるところとかありそう。まぁ正直どっちでもいいな。とりあえずしばらくPassengerで運用してみよう。

あんま関係ないけど

rack.input問題調べてる最中に偶然辿り付いたCloudKitがちょっと気になる。面白そうだし、atomserver作るのに参考になりそう。あとでいじってみよう。