入門書の次に読むRailsデプロイ
とあるきっかけから、監訳者の一人の橋本さんを通してオライリーさんから
Railsデプロイ[rakuten]をいただいた。Railsプログラミングがひと通りできるようになった、常用しているマシンの上で動作させてきた、自分以外の人々に向けたサービスをこれから始める。本書はそんな人々を助けてくれるだろう。
プライベートに運用するアプリケーションを除くと、普段は自分がログインすることがほとんどないようなマシンがアプリケーションを動かすための場になる。そのようなマシンは一つだけではなく、用途ごとに複数のマシンを並べて動作させることが多い。このようなマシン環境下でアプリケーション群を正常動作させ続けるには、いつも使っているマシンでアプリケーションを運用するのとは異なった視点・手法・作業が求められる。
たとえば、アプリケーションを動かすのに必要なハードウェア環境…… はともかくとしても、ソフトウェア環境を整えなければならない。それはRubyのインストールから始まるかもしれないし、もしかするとRubyをインストールするための環境作りから始まるかもしれない。また、HTTPサーバを設定しなければならないだろうし、複数あるマシンがうまく連携できるようにもするだろう。アプリケーションのリリースは複数のマシンが対象となる。マシンの用途が違えば作業も違う。そして、マシンを増やすことがあればそのたびにそれらすべてを行う必要がある。いきなりなにもかもやろうとすると大変だ。
本書を読むと、手元で動かす→他のある程度整備されたホストで動かしす→VPSを借りて動かす→専用ハードを借りて……、といった具合にじょじょに規模を拡大していく工程をひと通りながめることができる。その内容は単に「Railsアプリケーションをリリースする」というだけではなく、複数のマシンでサービスを運用するための機能分散の方法や負荷分散をするためのリバースプロキシの設定など広い範囲をカバーしている。
そのうえ、複数のデータベースを運用するためのMySQLの設定とアプリケーション側での対処、チューニングのためのベンチマークやプロファイルの取り方、キャッシュの使い方とキャッシュ乱用への注意など、アプリケーションをサービスとして運用するのに必要な領域についても実践的に解説する。そのような点から、アプリケーションを作れるようになり、さあこれからデプロイしようといったストーリーの入口で本書は活躍するだろう。
少々残念なのは、今この時期にPassengerが扱われていない点。もちろん出版時期などからいたしかたないところではあるのだが、カバーしている範囲の広さからすると実におしい。また、Railsからはやや離れたところでの説明や表現にはいくつかひっかかる点もあった。一つあげると125ページの「Linux上でRailsアプリケーションを動作させるためには……最低限でもCやC++のコンパイラ……が必要」といった記述で、これはRMagickなどをRubyGemsで運用することなどを前提にしているのであるが、できるならコンパイラは避けたいところと考える人も少なくないはずだ。(他のものも含めて出版社の方に伝えておいた。)
一方、本筋とやや離れたところでちょっと感心したのは、Apache HTTPサーバの設定作業の中で、a2enmodやa2dismodなどに触れている点だった(本書ではUbuntuを主な環境としている)。この種の特定のOS環境(この場合はDebian/Ubuntu)に独特な手順というのはともすれば流されてしまいがちである。もちろん明確な方針があって別のやり方で運用するのは構わない。だが、マシンはいつか自分の手を離れるものであるとすると、理由がなければできるだけその環境の流儀に従っておいたほうがよいと思う。考えてみればRailsだってそうなのだから、環境整備でも同じようにしたってよいだろう。
Railsキャッシュ、FastCGIとMultiViews
Typoにしてしばらく気付いていなかったのだが、次のようなアクセスパターンで404 not foundが起きる。
- キャッシュをクリアする
- 「/typo/2008/10」にアクセスする
- 「/typo/2008/10/11/ほげほげ」にアクセスする
- 404「The requested URL /typo/2008/10.html/11/ほげほげ was not found on this server.」となる
何が起きているかはmod_rewriteのログを見ればすぐに分かる。
[example.jp/sid#8f135d0][rid#8f391e0/subreq] (3) [perdir /typo/public/] add path info postfix: /typo/public/2008/11.html -> /typo/public/2008/10.html/11/ほげほげ [example.jp/sid#8f135d0][rid#8f391e0/subreq] (3) [perdir /typo/public/] strip per-dir prefix: /typo/public/2008/10.html/11/ほげほげ -> 2008/10.html/11/ほげほげ [example.jp/sid#8f135d0][rid#8f391e0/subreq] (3) [perdir /typo/public/] applying pattern '^$' to uri '2008/10.html/11/ほげほげ'
問題はなぜこうなるか。FastCGIなんてどこででも使われているのだから、手元の環境の何かが間違っているのだろう。そうは思いつつも、このことに気付いたのが今朝出掛ける少し前だったので、さっくり動けそうなPassengerに逃げてしまった。
そして今もう一度、別の環境で再現させようとしたのだが、どうにも再現できない。どうしたものか、というところでMultiViewsの動作を思い出した。
問題は/typo/public/2008/10.htmlがあることにより、/typo/public/2008/10/へのアクセスがこのファイルに向けられてしまうところにある。拡張子が補完されてしまっているわけで、この動作がまさにMultiViewsのそれだということにようやく気付いた。そこで再現できなかった別環境でMultiViewsを有効にしたところ、現象が再現するのを確認できた。
というわけで、Typoのように階層的でかつ中間ディレクトリ名と同じベース名のキャッシュファイルが作られるようなときにはMultiViewsとの相性が問題となることがある。もっとも、そのようなところにRailsアプリケーションを配置していたのがそもそもの原因とも言えるが。
ともあれ、運用はFastCGIでもPassengerでもどちらでもいいので、しばらくこのまま様子を見てみようと思っている。Passengerも動かしてみようとは思っていたところでもあるし(とか、いいつつ、typoからまた戻ったり、また別のものにしたりするかもしれないが)。
Typoインストールメモ
まずTypoのコードを入手する。いろいろなも味に依存しているのでRubyGemsを使うのが楽。
$ GEM_HOME=/tmp/GEM gem install typo
Typoではサイトをセットアップするときに必要なものをすべてコピーするので、インストールしたgem群への依存関係は一応なくなる。ただし、typoコマンドを通じてバックアップなどの管理操作ができる。そしてそのような操作のためにはインストールされたgemが必要となる)。
$ sudo -u www-data \
env GEM_HOME=/tmp/GEM \
/tmp/GEM/bin/typo install path/to/install/dir \
db_user=dbuser \
db_password=dbpass \
db_name=dbname \
web-server=external # FastCGIの場合
これでTypoが動作する環境ができるが、.htaccessは作ってくれないのでこれを作っておく(FastCGIで動かそうと思うので)。また、Rails 2.x系ではw3mなどでアクセスすると406エラーになるので回避コードを入れておく。
$ rails /tmp/t $ sudo -u www-data \ cp /tmp/t/public/.htaccess path/to/install/dir/public $ rm -rf /tmp/t $ sudo -u www-data \ vi path/to/install/dir/putlic/.htaccess # 調整 $ cat <path/to/install/dir/config/initializers/w3m.rb
Mime::HTML.instance_eval { @synonyms << "text/*" }
Mime::LOOKUP["text/*"] = Mime::HTML
E
今回、tDiaryから移行したデータがあるので、一部の表現のために書いた互換プラグインを置いておく。
$ sudo -u www-data \
cp -a typo_textfilter_{asin,tdiarycompat} \
path/to/install/dir/vendor/plugin
最後にApache HTTPサーバ側のその他の調整をしてリロードする。
$ sudo vi /etc/apache2/sites-available/site # その他調整 $ sudo /etc/init.d/apache2 reload
ブラウザでアクセスすると最初のユーザ登録ができる。tDiaryからのデータ移行の都合のためspamまわりの設定を残して、その他の設定をしておく。その後でデータを流し込む。
$ sudo -u www-data \ path/to/install/dir/script/runner td2typo.rb tdiary.dump
終了後、残しておいたspamまわりの設定をし、動作確認をする。
この環境ではFastCGIをmod_fcgidで運用しているのだけど、外部リソースにアクセスしまくるページなどでIPCCommTimeoutにひっかかることがあるようだった。tDiaryをFastCGIで動かそうとしたときにはIPCConnectTimeoutを大きめにしなければならなかったのだけど、ここではCommのほうを大きめに設定した。
実はnet/httpのタイムアウトのところでエラーになっていたのを勘違いしてしって、ずいぶん遠まわりをしてからこのことに気付いた。いかんいかん。
Rails 2.0.2とRuby 1.8.7のString#respond_to?
ところが(なのかな?)Rails 2.0.2にも同じバグがあって、これも顕在化した。よって、次のようになるのは1.8.7を1.8.7-p17にしても変わりない。
$ ./script/console
Loading development environment (Rails 2.0.2)
>> "" + ActiveSupport::Multibyte::Chars.new("")
ArgumentError: wrong number of arguments (2 for 1)
from (irb):1:in `respond_to?'
from (irb):1:in `+'
from (irb):1
>>
つまりこれはActiveSupport::Multibyte::Charsで定義されているrespond_to?が引数を一つしか取らないからで、これを二つ取るようにすればよい。というわけで、config/initializers/fix_multibyte_chars_respond_to.rbというようなファイルを以下の内容で作ることで回避できるような気がする。
if defined?(ActiveSupport) &&
defined?(ActiveSupport::Multibyte) &&
defined?(ActiveSupport::Multibyte::Chars)
mc = ActiveSupport::Multibyte::Chars.new("")
begin
"" + mc
rescue ArgumentError
raise unless mc.method(:respond_to?).arity == 1
class ActiveSupport::Multibyte::Chars
def respond_to?(method, ip = false)
super || @string.respond_to?(method, ip) || handler.respond_to?(method, ip) ||
(method.to_s =~ /(.*)!/ && handler.respond_to?($1, ip)) || false
end
end
end
end
Rails 2.0.2とRuby 1.8.7のString#chars
Ruby 1.8.7からはString#charsが定義されるようになった。これがRails 2.0.2なんかではまずくって、たとえば"hello".firstでNoMethodError: undefined method `[]' for #<Enumerable::Enumerator:0xb6f3f5b4>などとなる。これはRailsのように積極的に組み込みクラスを改変しているものの定めといえる。
Rails 2.1.0を見てみると以下のようになっていて、Ruby 1.8.7のString#charsを削除し、Rails(ActiveSupport)が定義しているcharsを使おうとしている。
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module String #:nodoc:
unless '1.9'.respond_to?(:force_encoding)
# Define methods for handling unicode data.
module Unicode
def self.append_features(base)
if '1.8.7'.respond_to?(:chars)
base.class_eval { remove_method :chars }
end
super
end
[...]
Rails 2.0.2でも同じようにするには、Rails側に手を入れたほうがてっとり早いとは思うのだが、ひとまずそれはしないことにするならばconfig/initializers/remove_string_chars.rbを以下の内容で作っておくというやり方はどうだろう。
unless '1.9'.respond_to?(:force_encoding)
String.class_eval do
begin
remove_method :chars
rescue NameError
# OK
end
end
end
script/consoleやscript/server(アプリケーションはTracks-1.6)で簡単な動作確認をしてみたところ、ひとまず前述の例外を回避できているようだ(Ruby 1.8.7、1.8.7-p17および1.8.6で確認した)。ただしそれ以外の環境やアプリケーションでどうなるかは確認していない。
Passenger (mod_rails) 1.0.1の動作
ざっとドキュメントを読んでみたところでは、アプローチとしてはFastCGIと似ているみたい。つまり、Railsアプリケーションのために起動されるプロセスを永続化させることで速度を得る。
ただ、FastCGIと違う点が(もちろん)あって、その一つはフレームワークのコードと、フレームワークのコード-アプリケーションの組み合わせに応じてコードをキャッシュする(プロセスを確保しておく)ということのようだ。フレームワークのコードというのは、アプリケシーションが利用しているRailsのことで、コードの所在やgemのバージョンなどに応じてプロセス(framework spawn server)が作られる。その先にアプリケーションのコードまでを読み込んだプロセス(application spawn server)が作られ、そこから具体的なアプリケーション実行プロセス(インスタンス)が生成される。
これらキャッシュまわりのプロセスのツリーの構築はpassenger-spawn-server(spawn server)が行うのだが、その他の制御はmod_passengerが行う。つまり、しかるべきプロセスに対してアプリケーションを動作させるためのプロセスを生成させ、それをコントロールするのはmod_passengerの仕事となる。ただ、実際には、framework spawn serverとapplication spawn serverについてのタイムアウト処理については、それぞれspawn serverとframework spawn serverが行っているようだ(個々のインスタンスのタイムアウトはmod_passengerが処理する)。
他にはconfig/environment.rbのオーナーの権限でアプリケーションを動させること(ただし特権では動作せず、RailsDefaultUserの権限になる)や、リクエストURIとRailsアプリケーションのマッピングの簡略化なども特徴といえるようだ。
Mongrelなどに対しては、ヒマしてるプロセスを長居させなくてすむといった利点が挙げられている。また、「Ruby Enterprise Edition」を使うことでcopy-on-write的な点でメモリ効率も上げられるよーというようなことが書いてある気がするが、肝心のRuby Enterprise Editionが何ものなのかはまだ資料が出来ていないようでよく分からない。
複数のアプリケーションを同居させるときのマッピングの指定方法やmod_aliasとの相性問題(?)など、ドキュメントをながめただけでは詳細が分からない点もあるが、前述のことも含め、実際に動かしてみてみよう。
追記1: ソースを読んだりしてもう少し様子が分かった。RailsBaseURIというのはLocation+SetHandlerみたいなものなのね。
追記2(2007-04-16): mod_aliasとの相性というか、要するにDocumentRoot以下の構造とリクエストURIのパス部分の構造が一致しているのを想定しているということのようだ。なので、Passengerが動作するバーチャルホストでmod_aliasを使うとすぐにアウトということでもない。もう少しがんばれそうだが、今後どうにかしていくのかな?
Passenger (mod_rails) 1.0.1を動かしてみて
少しだけど実際に動してみることでもう少し分かったので、先の記事を少しだけど書き換えておいた。
他に気付いた点として、passenger-spawn-serverにはSIGHUPでリロードがかかるような感じのコードがあるのだけど、シグナルハンドラを設定する親クラスのコードではSIGHUPを常に無視するようになっていて、結局のところSIGHUPには無反応になっているようだというのがある。もうちょっと確認したほうが良さそうだけど、その通りだとすればどっちが位図した挙動なのか気になる。SIGHUPでリロードしてくれたほうがうれしいように思う。
あと、spawn server系を外部から、つまりkillコマンドとかで終了させると、しばらくはエラー表示が出るようになる。しばらくというのはアイドル時間をすぎるあたりまでということだと思うが、詳しくは見ていない。関連して、運用時にはエラー出力用のテンプレートを調整しておいたほうが良さそうだ。そのままバとバックトレースとか出ちゃう。
運用に際してといえば、たとえば高負荷時の挙動だとか、前述のプロセスが予期せず死んだときの挙動だとか、そういった部分での検証は必要ではあるものの、単に動かすという意味ではインストールさえできればわりとすんなりと動いてくれる。Apache HTTPサーバ2.xでなければならず、今のところprefork MPMにしか対応していない(と書いてある)点が少々ひっかかるが、それがすごく問題ということもないだろう。Mongrelなんかよりも手間はまずまず少なくてすむように思える。
もうちょっと調べてみてはいるので、いずれ期会があればまとめてみる気になるかもしれない。が、そんな機会はないかもしれない。メモだけなら簡単なんだけどねえ ;-)


