As Sloth As Possible

可能な限りナマケモノでありたい

タグ:Rack

どうやらUnicornというのが良いらしいという噂を聞きつけたので、どんなもんじゃろと試してみることにした。

Route 477 - 大規模Railsサイトのための新しいHTTPサーバ、Unicorn

Unicornてのは何者なのかと言うと、Rack及びRailsに対応したRubyのWebアプリ用のHTTPサーバ。詳しくは上の記事を読んで下さい。githubでも使ってるそうだ。あと、名前が格好良い(あんまり関係ない)。

まずはunicornの設定

と言っても、gem install unicornしてconfig.ruがあるディレクトリでunicornコマンドを叩けば、thinとかと同じようにサーバが起動する。rackup互換のオプションも付いてるので特に悩むこともないと思う。あとは普通にApacheとかでプロキシの設定してやるなりなんなりすればすぐ使える。

それだけだと大して面白くないので、折角だから上の記事に書いてあるように、nginxでソケットでっていう設定をしてみる。先にUnicornの方の設定。ちなみにサンプルに使ったのは例によって彼女が404のやつ。今現在はApache2+Passengerで動いている

# unicron.conf
worker_processes  4
working_directory '/var/www/rackapp'
listen '/tmp/rackapp.sock', :backlog => 1
listen 4423, :tcp_nopush => true
timeout 10
pid '/tmp/rackapp.pid'
preload_app  true
stderr_path '/var/log/rackapp.log'

before_fork do |server, worker|
  old_pid = "#{server.config[:pid]}.oldbin"
  if old_pid != server.pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end

  sleep 1
end

after_fork do |server, worker|
  addr = "127.0.0.1:#{4423 + worker.nr}"
  server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => 1)
end

中身はほとんどUnicornの設定例からコピペ。ちなみに、nginxを推奨してるだけあってnginxの設定に似せたDSLになってるけど、実態は見ての通りRuby。その気になれば動的に設定をごにょごにょみたいなこともできる。ログはThinみたいによしなにやってはくれないけど、その辺は設定でLoggerを渡すなりRackのミドルウェアを使ってなんとかするなりすればOK。

んでこれを適当なファイル名で保存したら、次のようにunicornコマンドを叩く。

$ unicorn -D -c /var/www/rackapp/unicorn.conf /var/www/rackapp/config.ru

-Dオプションでデーモン起動、-cオプションは設定ファイルの指定、最後の引数はrackupファイルのフルパス。ちなみにこれを/var/www/rackappじゃないところで実行してみたんだけど、最初は「working_directoryとか指定してるから、そこにあるconfig.ruを勝手に読んでくれたりしないかな」とか思ってたんだけどやっぱりダメで、その後相対パスで指定してみたらrequireでコケて「そんなファイルねーよ、寝言は寝て言え」と怒り出したので、絶対パスにしたら行けた。

この時点でhttp://localhost:4423/がちゃんと見られることと、unicorn.sockが出来てることを確認。よし次はnginx。

nginxのプロキシ設定

こんな感じ。

# nginx.unicorn.conf
upstream rackapp {
    server unix:/tmp/rackapp.sock;
}

server {
    listen       80;
    server_name  localhost;

    location / {
        proxy_pass  http://rackapp;
    }
}

これを大本のnginx.confのhttpのブロックのどこかでincludeしてやる。一応言っとくとこれ以外にも普通のnginxの細かい設定はしてるけど、上の例では省略してます。

ちなみに、ソケットでやるように設定してるからこうなってるけど、TCPで良ければproxy_passのところをhttp://localhost:4423とかにしてやっても普通に動く。

とりあえずこれで完成。あとはnginxを起動してやれば、ちゃんと見られる。素敵。

インターフェースが統一されてるって素敵よね

いやぁ、やっぱRackでインターフェースが統一されてるって、良いですよね。アプリ側あんまり弄らなくてもさくっと移行できるわけですし(っていう話をこないだ書いた)。あと、ついでに同じサーバで動いてたHTTP::Engineのアプリも、apache+modperlからnginx+fcgiに移行してみたけど、こっちもちょこっと設定変えたくらいでアプリ側は全然弄ってない。良いなー。こうして新しい環境が出てきたりしたときにはやっぱりRackやなんやらが真価を発揮する。折角なので次はPlackでH::Eのときと同じことをやってみようと検討中。

んで、結局Unicornは良いの?イイの?

さぁ…ベンチ取ってないのでなんとも。取っても「彼女が404」じゃあんまり参考にならないんだよなー。ただ、アプリをロードし終わってからforkするとかnginx側に気付かせない用に再起動できるとかその辺だけでも十分魅力的ではある。あと、再起動は早かった。流石。

表題通りですが、Ruby Freaks Loungeで記事を書かせていただきました。

Ruby Freaks Lounge:第23回 Rackとは何か

Rackってそもそも何なの、何でそんなの出来たの、みたいな話を簡単に。んー?誰だー、普段はこんなこととかあんなこととかばっかやってる癖に何真面目な記事書いてんだとか言った奴はー。先生怒らないから出て来なさいー。

RackってRailsだとかSinatraだとかに比べるとマニアックな部類(知名度がというよりは使い道が、ね)だと思うんだけど、予想外に反応が良くて嬉しい。技評の方は「Perl界隈でPSGIも盛り上がってるし、今そういう話題はタイミングが良かったのかも」と言ってたけど、本当にそうだなー。しれっとPSGIに触れたのはまたお前はRubyのことばっかで少しはPerlの勉強しろよ言われないため僕がPerl大好きだからですよ。ホント。ウソジャナイヨ。

ちなみに再来週になったら後編が掲載されてるはず。Rackのミドルウェアの話とかを予定してる(予定ってことはつまりまぁまだ書き上げてないんだけども…)ので、お楽しみに。

sinatraとActiveRecordとERBでBBS作ったのでソースを公開してみる - だるろぐに触発されて俺もBBSを作ってみようと、ここ数日Sinatraをいじっていた。Sinatraさんは最近バージョンアップしてた気がするけど、どうやらちゃんとRuby1.9.1でも動くようだ。素敵。

で、順調に行くかと思ったんだけど、どうにも書き込みのspecが通らない。そこで初めて、POSTやGETでパラメータにマルチバイトの文字列が入ってると何かおかしいことに気付いた。

最初に書いたspecとアプリ側のコードを抜粋。

# coding: utf-8
require 'rubygems'
require 'rack/test'
require 'routes.rb' #sinatraアプリ

set :environment, :test

include Rack::Test::Methods

def app 
  Sinatra::Application
end

describe '投稿するとき' do
  it 'は投稿に成功したら一覧にリダイレクトすること' do
    @props = { 
      'user'  => 'セト',
      'body'  => 'べ、別にアンタの論文が心配なんじゃないんだから!',
    }
    Entry.should_receive(:create).with(@props)
    post '/add', @props
    last_response.status.should be_equal(302)
  end 
end
# coding: utf-8
require 'rubygems'
require 'sinatra'
require 'haml'
require 'sequel'

post '/add' do
  Entry.create(params)
  redirect '/'
end

DB = Sequel.connect('sqlite://db/trabbs.db')
class Entry < Sequel::Model; end

実際にはget '/'とかEntryのスキーマとかindex.hamlとかも用意してありますがそれは心の目で補完しておくれ。んで、期待してるのはこれでセトがツンデレな投稿をしてそれがDBに記録され、その後トップにリダイレクトされることなんだけど、これがコケる。

1)
Spec::Mocks::MockExpectationError in '投稿するとき は、投稿に成功したら一覧にリダイレクトすること'
 expected :create with ({"user"=>"セト", "body"=>"べ、別にアンタの論文が心配なんじゃないんだから!"}) but received it with ({"user"=>"\xE3\x82", "body"=>"\xE3\x81\xB9\xE3\x80\x81\xE5\x88\xA5\xE3\x81\xAB\xE3\x82\xA2\xE3\x83\xB3\xE3\x82\xBF\xE3\x81\xAE"})

ありゃ。UTF-8の文字列がくるべきところに、ASCII-8BIT、つまりバイト列がそのまま来ちゃってる。んむ、えーと、実際のバイト列を変更せずにエンコーディングだけ変えるのはforce_encodingだったよね。ということでforce_encoding('UTF-8')してみる。

1)
Spec::Mocks::MockExpectationError in '投稿するとき は、投稿に成功したら一覧にリダイレクトすること'
 expected :create with ({"user"=>"セト", "body"=>"べ、別にアンタの論文が心配なんじゃないんだから!"}) but received it with ({"user"=>"\xE3\x82", "body"=>"べ、別にアンタの"})

あ、あれ?なんか足りなくない?これはどういうこと?

Rack::Utils#escape

ところで、上では端折ったけど、実はspecを実行する際にこんなwarningが出ている。

/usr/local/lib/ruby/gems/1.9.1/gems/rack-1.0.0/lib/rack/utils.rb:13: warning: regexp match /.../n against to UTF-8 string

UTF-8に対してエンコーディングを無視して正規表現にマッチさせてる…うん、どうもこれが怪しい。さてこれはどこから出てるかというと、こんなコードから出ている。

# rack-1.0.0/lib/rack/utils.rb
module Rack
  # Rack::Utils contains a grab-bag of useful methods for writing web

  module Utils
    # Performs URI escaping so that you can construct proper
    # query strings faster.  Use this rather than the cgi.rb
    # version since it's faster.  (Stolen from Camping).
    def escape(s)
      s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
        '%'+$1.unpack('H2'*$1.size).join('%').upcase
      }.tr(' ', '+')
    end
    module_function :escape

    # Unescapes a URI escaped string. (Stolen from Camping).
    def unescape(s)
      s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
        [$1.delete('%')].pack('H*')
      }
    end
    module_function :unescape

    # 以下省略
  end
end

なるほどですねー。要するに、

  1. escape中でunpackするときに、String#sizeを使っている。1.8まではこのメソッドはバイト数を返してたけど、1.9では文字数を返すように仕様が変更されている。マルチバイトな文字列だと当然バイト数と文字数は一致しないので、途中で切れちゃうことになる。
  2. packして返ってくるのは単なるバイト列、つまりASCII-8BITのString。
  3. 1.8までは文字列がどんなエンコーディングであれ、unpackしてpackすればまた文字列に戻る。でも1.9ではunpackからpackの過程で元のエンコーディング情報は失なわれているので、明示的にエンコーディング情報を付加してやる必要がある

ということかしら。まず1に関してはString#sizeを使ってるところをString#bytesizeを使うように書き換えればいい。ちなみにRack::Utilsでは1.8でも1.9でも動くようにRack::Utils#bytesizeというメソッドが定義されているので、それを使うといい。というか、なんで使ってないんだろう。単に忘れてるんだろうなぁ…。

module Rack
  module Utils                                                                          
    def escape(s)
      s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/) {                                              
        '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase                               
      }.tr(' ', '+')
    end
  end
end

これをrackをrequireした後に書いてRack::Utils#escapeを上書きしてやると、ちゃんとescapeされるようになる。

次はASCII-8BITになっちゃってるのをforce_encodingしてエンコーディングを変える。これも本当はRack::Utils#unescapeを上書きして、その中でエンコーディングを推測してforce_encodingしておいて、アプリ側のフックでString#encodingを使って内部で使うエンコーディングに変換してやるのがいいかなぁとか思ったんだけど、それでいいのかなー。

んでもまぁ、今回はUAに応じてviewのエンコーディング変えるみたいなことはしてないし、UTF-8決め打ちで良いよね☆…というわけで、Sinatraのbeforeの中で、RUBY_VERSIONが1.9以上だったときはparamの文字列をforce_encoding('UTF-8')するようにしたった。良い子はちゃんとして下さい。

はい、これでちゃんとspec通りました。ふー。

…で。

そしたら、今度はRackをforkして直してpull request送ればいいんだよね。いいんだけど、やんなきゃなーとは思うんだけど、めんどいその辺のあんま自信ないので、誰か1.9でもちゃんと動くように直してRackの開発スタッフに教えてあげてもらえると助かります。助けてエロい人。

彼女のステータスを返す」のソースが読みたいというリクエストがあったので、少し書き直してgistに上げてみた。

gist: 112607 - GitHub

一応上の状態でrackupすれば動きます。hamlとrackの最新版が必要な他は特に何も要らないはず。ちなみに1.8.6、1.8.7、1.9.1では動くのを確認済み。んでPassengerで動かすときには、「PassengerでRackアプリを動かす」で書いたように、config.ruと同じ階層にpublicとtmpってディレクトリを作ってやって、コメントアウトしてあるRewindableInputWrapperを有効にしてやればいいはず。

ついでにRack::Testを使って書いたspecと、実際にGETしたりPOSTしたりしてどんなレスポンス返してるのか見るスクリプトも置いといたので、参考までに。

ぶっちゃけRackとHaml(とあとPassenger)で遊びたかっただけなので、「Sinatraのが楽じゃね?」「ってかそれApacheの設定だけでなんとかなりそうな…」とかいうツッコミは無しの方向で。ちなみにRackでオレオレWAFもどき作りにはそろそろ飽きてきたので、今はRamazeさんで遊んでたり。Ramazeさんいいな、気に入った。とはいえこのRackいじりは無駄ではなくて、こういうことやってからRamazeのソース読んでたら案外流れが掴めて良かった。

あとLast-Modifiedの日付とかもうほんと冗談なのでそういう小ネタに食いつくとか無しの方向で。食いつくなよ。絶対に食いつくなよ。絶対だかんな!

昨日のネタでは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作るのに参考になりそう。あとでいじってみよう。

日々着実にバカな方にバカな方に向かってるのを実感してるfaultierです、みなさんお久しぶり。

一昨日あたりに見かけた「彼女がいないことをステータスコード404で表わす」ってのが大変ツボに入ったので、今日はせっかくだから実際にそれを実装してみたよ。なんと以下のURLをGETすると「faultierの彼女」ってリソースがいまどんなステータスなのか返してくれるんだ。

http://faultier.jp/resource/kanojo

…まぁ俺に彼女がいるかどうかなんて瑣末なことはどうでも良くて、一応これGET/POST/PUT/DELETEに対応してて、それぞれ違うレスポンスを返すように作ってあるので、生まれついてのHTTPクライアントな感じの少年少女紳士淑女な方々はいろんなリクエストを送って「リソース:faultierの彼女」をいじってみるといいと思うんだ。まぁどう操作しようが何も起きませんが。何、あんま使わないからってサボってGETとPOSTしか実装してない?しょうがないやつだな、GETするときにURLの末尾に/putとか/deleteとか入れてみなよ。それっぽい動きするから。

みごとに400系のステータスコードばっかりになっちゃったけど、せっかく作ったので、そのうち「418 I'm a teapot」とかも実装する予定。ってそれも400系か。あ、あとちなみに、GETするときにmode=prevとかmode=nextってクエリを付けると「前の彼女」「未来の彼女」についての情報も返ってくる。どうでもいいですね。

追記:418も実装した。ついでに多少ステータスコードをいじった。そのリクエストにそのレスポンスは普通無いだろ、みたいのがあるとあれなので。あと、HEADを忘れてたのでHEADも対応した。

Rack可愛いよRack

ネタはネタとして置いとくとして、今回はこれをRackHamlを使って作ってみた。何でかというと、今Ruby版Atompub::Serverみたいの作ってて、RailsやMerbのプラグインもそのうち書くつもりではいるんだけど、まず単体でサクっと動くの作りたいなと思っていろいろいじってるところだったから。Hamlは興味はあったんだけど中々触る機会がなかったので、ついで。

なんというか、Rackは素敵だなぁ。この程度のものを作るのにちゃんとしたWAFを使うのもなんだかゴキブリ退治に対戦車ミサイル持ち出すみたいで気がひけるんだけど、かといってCGIってのもねぇ、みたいなときにもとても良い。簡単な認証とかロギングとかセッションとかは添付のミドルウェア使えば実現できちゃうし、WEBrickとかで簡単にローカル環境で開発サーバ作れちゃうのも魅力的。そんでもって深く考えなくてもWEBrik、thin、mongrel、fcgi、Passengerと色んな環境に対応できちゃうのも素敵だ(というかそれは話が逆で、そのためのインターフェースライブラリなんだけども)。なによりRack::Testが便利すぎる。こんなしょうもないアプリでもちゃんとSpec書いたんだぜ、あんまり簡単だったから。

ハムは食べたいけども

Hamlの方は、なんというか、結構微妙。いやまぁ、確かにすっきりシンプルなテンプレートが書けるんだけど、普段見なれてるHTMLから乖離しすぎててちょっと抵抗あるかも。正直学習コスト考えたら素のHTMLとRubyをそのまま書けるerbやその派生/改善版のテンプレートエンジンのがよっぽどとっつきやすい気がする。一人でやってるときはともかく、デザイナーやマークアッパーとの分業で仕事してると、これ出力がどうなるか想像しながらやんなきゃならなくて作業しづらいんじゃないのかなーとか思ってしまう。

とは言え不思議な魅力もあるのも確かで、文句言いながら書いてたけど数分後にはなんとなく慣れてしまった。趣味でなんか作るときにはしばらく使ってみようかなと思ってたりする。

ところで

一応言っとくけど、あそこからエントリーしても別にデータとか保存してないしメール送ったりもしないので、安心して彼女の名乗りを上げるといいよ!まぁ本当に名乗りたければあんなとこで慎しやかに名乗られても困るけど、いないよねそんな人。

↑このページのトップヘ