AWDwR "Chapter22 Deployment and Scaling"

この章、英語わかりにくひ…。

Picking a Production Platform

まずはWEBrickApachelighttpdの3つのWebサーバについて。

WEBrickRubyにバンドルされているピュアRubyのWebサーバで、特に速くはないしスケーラブルでもないけど実行させるのがメッチャ簡単。何千ものユーザに同時に利用されるようなアプリケーションでなければ最初の選択としてはWEBrickが適している。それにWEBrick上で開発したアプリケーションは変更することなしにApachelighttpdに移行することができる。

Apacheは万能でまあまあ速くオープンソースのWebサーバとしては独壇場なので、Railsアプリケーションを実行させる環境としては最も人気がある。Apacheの場合はmod_fastcgi(Apache2.0ではmod_fcgid)を使ってFastCGIで動作させる。しかしApacheの2.x用のモジュールはいくつか問題がある。これらの問題は全てのRailsアプリケーションに影響するわけではないけどちょっと心配。勇気があればApache2.xのmod_fastcgiを使ってみよう。大勢のユーザに公開するならApache1.3.xの方がいいかも。

lighttpdはモジュールはそんなに多くないしドキュメントやチュートリアルもあまりないしApacheのようなサポートもないんだけど、でも興味あるかも。httpdは速い。特に静的なコンテントの場合は超速いし、高負荷時にも安定している。さらに動的コンテンツ用にFastCGIの特別なランタイムがあるし、なによりも素晴らしいのはロードバランシング機能がビルトインされていること。なので簡単にアプリケーションをスケールできる。

でもlighttpdは安定性に問題があるので使うのをちょっと躊躇するかも。公開前に徹底的なパフォーマンステストを行なうことをアドバイス


次にアプリケーション実行方式は何を選択すべきか。単純な答。FastCGIを使え!

WEBrickサーブレットのアプローチをとっていて、1スレッドで各リクエストを処理する単独のプロセスを持つ。高負荷の用途に向いていない理由の一つはActionPackがスレッドセーフじゃないこと。

RailsCGIを使うのは我慢大会のようなもの。リクエストのたびにRubyインタプリタが起動するからリクエストを処理するのに数秒かかるのはザラ。

じゃあ、なぜわざわざCGIをとりあげるのかというと、まず第一に全てのWebサーバでデフォルトでサポートしているから。初めてApacheをセットアップするときにまずCGIから始めるというのは良いアイデアで、CGIでうまく動くことを確かめてからlighttpdに移行したり、また問題が発生した場合はCGIに戻してデバッグしたりできる。

第二はRails自体のコードを拡張する必要ができたとき。FastCGIサーブレットRailsをキャッシュしちゃうので変更した場合はサーバのリスタートが必要だけど、CGIならば変更しても次のリフレッシュで結果がすぐに反映される。


FastCGIを使うとRailsを超高速で動作させることができる。FastCGIはスタートアップ時にRubyインタプリタRailsフレームワークを初期化し、最初のクエリ時にデータベースの接続を確立してその後は保持してくれる。また、プロダクション環境においてはアプリケーションのコードがキャッシュされる。

さらにFastCGIプロセスはWebサーバプロセスから独立しているので、例えば静的なコンテンツの処理に100個のWebサーバのプロセスを持っていても動的なコンテンツを処理する10個のFastCGIプロセスがあればよい。メモリの消費は非常に重要な問題で、静的なコンテンツを扱う場合はApacheインスタンスは5MBしか使わないのに、Rubyインタプリタを動作させるためには簡単に20-30MB消費してしまう。100個のApacheインスタンスと10個のFastCGIでは800MBしか使わないのに、mod_rubyを使った100個のApacheは3GB以上使ってしまう。

A Trinity of Environment

developmentからproductionに変更した際の最も重要な変更はDependencies.mechanismの値が:loadから:requireになること。:requireになるとモデルやコントローラやその他のクラスを一旦ロードしたら再びロードすることはしなくなる。

Railsはローカルからのリクエストかそうでないかをちゃんと識別していて、ローカルからのリクエストの場合はエラーが起きるとデバッグ情報を表示するけど、そうでない場合はpublic/500.htmlを表示する。

production環境に変更したい場合は以下のようにする。

Iterating in the Wild

production環境ではローカルからのリクエストの場合のみエラー時にデバッグ画面が表示される。
ローカルじゃない場合でもデバッグ画面が見たい場合はActionControllerにあるlocal_request?()というメソッドを上書きする。これはローカルからのリクエストかどうかを返すメソッドで、デフォルトでは127.0.0.1からのリクエストのときのみtrueを返すんだけど、セッションにある値を持っているときのみtrueを返すとか、特定のIPアドレスからのリクエストの場合はtrueを返すようにする等すればデバッグ画面を表示する条件を自由に変更できる。

ActionControllerのrescue_action_in_public()というメソッドは例外が発生すると呼び出されるので、この機能を使えば例えば問題が発生した際にメールで管理者に通知するというようなことができる。その場合、ActionMailerのSystemNotifierというクラスのexception_notification()というメソッドが便利で、例外情報と環境情報をメールで開発者に送信してくれる。

アプリケーションを公開後にバグが見つかった場合、アプリケーションを止めないようにパッチをホットデプロイしたいと思うけど、そのためにはシンボリックリンクを使うとよい。新しいバージョンをリリースしたい場合は、まずアプリケーションのファイルをあるディレクトリにおいて、その後でそこにシンボリックリンクを張る。具体的には以下のようにする。

  1. 最新バージョンをreleases/rel25のようなディレクトリにチェックアウトする。
  2. current→releases/rel24のシンボリックリンクを削除し、current→releases/rel25のシンボリックリンクを張る。
  3. WebサーバとFastCGIサーバを再起動する。

最後のサーバの再起動で単純に再起動しちゃうと処理中のリクエストを中断させてしまう。Apacheはそれを防ぐgracefulという機能があるんだけど、FastCGIも同じようなことができて

killall -USR1 dispatch.fcgi

とすればよい。

また、データベースのデバッグをする場合、以下のようにconsoleを使ってモデルのクラスを直接操作できる。

ruby script/console production
p = Product.find_by_title("Pragmatic Version Control")

これはデバッグだけでなくマスタ情報の登録や変更等の管理用の機能としても使うことができる。

Maintenance

管理者がしなければならないことはログファイルの管理とセッションの管理の2つ。

Railsはデフォルトでログ出力にLoggerクラスを使ってるんだけど、これは簡単に使える反面、メッセージフォーマットやログフィルのロールオーバーや出力レベルなどに柔軟性がない。より柔軟なログ出力機能を使いたい場合はLog4RやSyslogLoggerを使いましょう。これらはLoggerとAPIが同じなので移行は簡単にできてconfig/environment.rbのRAILS_DEFAULT_LOGGERを書き換えるだけ。

Loggerクラスはログファイルのロールオーバーの機能を持っているけれども、FastCGIの場合それぞれがLoggerインスタンスを持っているので、同じファイルをロールオーバーしようとしたりして問題が発生する。そのためロールオーバーはLoggerクラスにさせるのではなくログファイルをロールオーバーさせるスクリプトを作ってcronで走らせるようにしましょう。

Railsのセッションマネージャはセッション情報を自動的に掃除はしてくれないんだって! ビックリ。だからファイルベースのセッションハンドラの場合はファイルがたくさんできちゃうし、データベースにセッションを保存する場合はレコードがたくさんできてしまう。なのでそれを防ぐために自分でセッション情報の掃除をするスクリプトを定期的に走らせる必要がある。ファイルベースの場合は例えば以下のようなスクリプトをcronで定期的に実行する。

find /tmp/ -name 'ruby_sess*' -ctime +12h -delete

またデータベースにセッション情報を保存している場合は以下のようなスクリプトを実行させるとよい。

RAILS_ENV=production ./script/runner 'ActiveRecord::Base.connection.delete("DELETE FROM sessions WHERE updated_at < now() - 12 * 3600")'


少し残っちゃった。間に合うか?