AWDwR "Chapter22 Deployment and Scaling" つづき
帰省先から書いてます:-)。
Scaling: The Share-Nothing Architecture
Railsアプリケーションをスケールする際のコンセプトがShare-Nothing Architecture。Share-Nothing Architectureにより状態の管理をWeb層やアプリケーション層でなくデータベースやネットワークドライブのような下位層で行えば良くなり、アプリケーションを複数のサーバ上で実行することが簡単になる。このShare-Nothing Architectureを実現するためにはセッション、キャッシュそしてアップロードファイルなどのデータをどう扱うかを考える必要がある。
まずセッションだけど、セッションデータはデフォルトではテンポラリディレクトリにファイルとして保存されている。しかし全てのサーバが同じセッションデータにアクセスしなければならないので、共有されているネットワークドライブに保存するようにするというようなことをしなければならない。しかしこれにはもっと良い方法があって、それはActiveRecordStoreというデータベースにセッション情報を保存する方法。これを使う場合はconfig/environment.rbまたは各環境の設定ファイルに以下のように設定する。
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager] = CGI::Session::ActiveRecordStore
他にもDRbやmemcachedに保存する方法がある。
一方、キャッシュはセッションと違っていろいろなサイズのファイルができるし、1ユーザ当たり多くのファイルができるので、データベースに保存するのは適さない。しかし、複数のサーバを使用する場合はローカルにキャッシュのファイルを保存することはできないので、ネットワークドライブを使用する必要がある。ファイルベースのキャッシングはconfig/environment.rbまたは各環境の設定ファイルに以下のように設定する。
ActionController::Base.fragment_cache_store = ActionController::Caching::Fragments::FileStore.new("#{RAILS_ROOT}/cache")
このキャッシングはアクションのキャッシングとフラグメントのキャッシングのみで有効で、ページのキャッシングはpublicディレクトリに保存する必要がある。なので、複数のサーバを使用する場合はpublicディレクトリにネットワークドライブをシンボリックリンクする。
Finding and Dealing with Bottlenecks
パフォーマンスチューニングの第一のそして最も重要なルールは計測なしに行わないこと。そして第二のルールは計測なしに行わないこと。パフォーマンスを改善するにはボトルネックに注目する。このボトルネックを見つける最も簡単でアプリケーションに影響を与えない方法はproduction.logをtailコマンドで見ること。これによりアプリケーションが内部で何をやっていて、どのくらいの時間がかかっているかを知ることができる。
ボトルネックが見つかったら、今度はチューニングの基準を計測し、それに基づいて調整を行なう。Unixには2つのパフォーマンス計測ツールがあって、その一つがab(Apache HTTP server benchmarking tool)で以下のようにして使う。
ab -c 9 -n 1000 http://www.foo.com/controller/action
このabは同じに1アクションしかテストできないので、もっと現実的なストレステストを行いたい場合はsiegeを使いましょう。
Case Studies: Rails Runing Daily
以下では実際の3つのアプリケーションが直面した問題を紹介するよ。
Basecamp by 37signals (www.basecamphq.com)
Basecampは何万人ものユーザに使われているプロジェクト管理アプリケーションで、RailsはこのBasecampで生まれた。
Basecampはデュアル2.4GHz Xeon、2MBメモリのマシン上で15個のFastCGIプロセスと50から100個のApache 1.3.xプロセスが動作している2つのWeb/アプリケーションサーバによって、1日約40万リクエストを処理している。しかし、マシンのロードは通常0.5から1.5程度。
MySQLのサーバは他の2つのアプリケーション(Ta-da ListとBackpack)で共有されていて、最大50万行のテーブルを持っている。このMySQLは3つのアプリケーションから利用されているが、ロードは0.1から0.3の間で、ボトルネックにはなっていない。
43Things by the Robot Co-op (www.43things.com)
43thingsは自分の夢や目標をシェアするサービス。
キャッシュは主にmemcachedに保存される。セッションもmemcachedに保存されるので複数サーバ間でセッションを共有できる。また処理の重いデータベースの検索もActiveRecordのオブジェクトとしてmemcachedに保存している。
43thingsはデュアル3GHz Xeon、2MBメモリを持つ、2台のアプリケーション/Webサーバと1台のデータベースから構成されており、1日約20万リクエストを処理している。それぞれのApache 1.3.xプロセスは25個のFastCGIプロセスを走らせている。サーバのロードが0.3を超えることはめったになく、CPUのアイドル時間も通常80%を超えている。
Mortgage processing engine (www.rapidreporting.com)
(RapidReportingって何やってるの?)
アプリケーションはデータベースにPostgreSQL、Webサーバとしてlighttpdを使用していて、IPトンネリングによるバーチャルサーバの後ろで1アプリケーションサーバ当たり10個のFastCGIプロセスが動作している。こうすることによってWebサーバを再起動することなくFastCGIを追加したり取り除いたりできる。
読了! うっ、間に合わなかったよ…。
AWDwR "Chapter22 Deployment and Scaling"
この章、英語わかりにくひ…。
Picking a Production Platform
まずはWEBrickとApacheとlighttpdの3つのWebサーバについて。
WEBrickはRubyにバンドルされているピュアRubyのWebサーバで、特に速くはないしスケーラブルでもないけど実行させるのがメッチャ簡単。何千ものユーザに同時に利用されるようなアプリケーションでなければ最初の選択としてはWEBrickが適している。それにWEBrick上で開発したアプリケーションは変更することなしにApacheやlighttpdに移行することができる。
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がスレッドセーフじゃないこと。
RailsでCGIを使うのは我慢大会のようなもの。リクエストのたびに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環境に変更したい場合は以下のようにする。
- WEBrickの場合 ./script/server --environment=priduction
- Apache/CGIの場合 httpd.confのvhostの設定、または.htaccessに"SetEnv RAILS_ENV production"と設定する。
- Apache/FastCGIの場合 httpd.confのFastCgiServerの定義に"-initial-env RAILS_ENV=production"と設定する。
- lighttpd/FastCGI fastcgi.serverに"bin-environment" => ("RAILS_ENV" => "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()というメソッドが便利で、例外情報と環境情報をメールで開発者に送信してくれる。
アプリケーションを公開後にバグが見つかった場合、アプリケーションを止めないようにパッチをホットデプロイしたいと思うけど、そのためにはシンボリックリンクを使うとよい。新しいバージョンをリリースしたい場合は、まずアプリケーションのファイルをあるディレクトリにおいて、その後でそこにシンボリックリンクを張る。具体的には以下のようにする。
- 最新バージョンをreleases/rel25のようなディレクトリにチェックアウトする。
- current→releases/rel24のシンボリックリンクを削除し、current→releases/rel25のシンボリックリンクを張る。
- 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")'
少し残っちゃった。間に合うか?
AWDwR "Chapter21 Securing Your Rails Application"
2005年もあと5日。Chapter21はセキュリティについて。
SQL Injection
フォームなど外部からのデータを直接SQL文の中で使ったりすると悪い人に任意のSQL文が実行されちゃうよというのがSQLインジェクション。例えばこんな風にしていると危ない。
Email.find(:all, :conditions => "owner_id = 123 and subject = '#{params[:subject]}'")
この場合もし悪い人がサブジェクトに' or 1 -- 'と入力しちゃうと全データが表示されちゃう。
このSQLインジェクションを防ぐためにはSQLメタキャラクタの\や'をちゃんとエスケープしましょう。Active Recordのattributes(), save(), find()で条件やリミットやSQL文を追加しない場合はActive Recordが危険な文字をエスケープしてくれるから大丈夫。しかし条件やリミットやSQL文を含めた場合は外部からのデータにSQLメタキャラクタが含まれないように気をつけること。そのためにはSQLステートメントの一部に#{...}を使うのではなくバインド変数を使うようにしましょう。
Email.find(:all, :conditions => ["ownerid = 123 and subject = ?", subject])
また?ではなくハッシュを渡す方法もあります。
Order.find(:all. :conditions => ["name = :name and type = :type", {:name => name, :type => type}])
それからRailsが自動的に作るメソッドもSQLインジェクションに対しては安心。
Email.find_all_by_owner_id_and_subject(owner.id, subject)
Cross-Site Scripting (CSS/XSS)
外部からのデータをそのまま表示しちゃうと、例えばJavaScriptを使ってセッションIDが盗まれログインされてしまうよというのがクロスサイトスクリプティング。
クロスサイトスクリプティングを防ぐにはHTMLメタキャラクタの<と>をHTMLエンティティの<と>に変換してしまえばOK。RailsはHTMLメタキャラクタをエスケープするh()メソッド(html_escape()のエリアス)があるのでそれを使いましょう。とにかくビューでは全ての変数に対してh()メソッドを使うこと。
また、テンプレートの中でHTMLを含んだ文字列に置き換えたい場合はsanitize()というメソッドを使いましょう。
Avoid Session Fixation Attacks
悪い人が入手したセッションIDをユーザに使わせることによって不正にログインしてしまうのがセッションフィクセーション攻撃。セッションフィクセーション攻撃を防ぐ方法には2つあって、一つがセッションを生成したときのIPアドレスを覚えておいて、それが変わったらセッションをキャンセルしてしまうようにするというもの。もう一つは誰かがログインするごとに新しいセッションを生成するようにしてしまえばよい。
Creating Records Directly from Form Parameters
ユーザを登録するアクションはよく以下のようにするんだけど
def register User.create(params[:user]) end
もし悪い人が以下のようなフォームを作ってデータを送信しちゃうと
<form method="post" action="http://www.foo.com/user/register"> <input type="text" name="user[name]"> <input type="text" name="user[password]"> <input type="text" name="user[role]"> # ロールは本来なら管理者のみが設定できるもの </form>
ロールを任意に設定されてしまうことになってしまう。
Active Recordはこれを防ぐ方法を2つ持っていて、1つがattr_protected()メソッドで保護したい属性を指定する方法。指定された属性はcreate()やnew()でparamsからモデルにいっぺんに設定することはできなくなる。この属性に値を設定したい場合は以下のように直接指定するようにすればよい。
user.role = params[:user][:role]
逆にattr_accessible()メソッドで設定してよい属性を指定する方法もある。この場合はそれ以外の属性は保護されるようになる。
Don't Trust ID Parameters
find()でIDだけで他の条件を指定せずにDBからデータを取り出すようにすると、悪い人がIDを推定して入力することにより任意のデータを取り出すことができてしまう。これを防ぐには条件付のfind()を使うようにしましょう。例えば以下の例ではユーザIDがログインユーザのものだけという条件をつけている。
def show id = params[:id] user_id = session[:user_id] || -1 @order = Order.find(id, :conditions => ["user}id = ?", user_id]) end
delete()やdestroy()も同じ問題があって、さらにこれらには:conditionsパラメタがないのでデータのオーナをチェックするとかwhere節を指定するとか自分でちゃんと確認しないとダメですよ。
他の方法としては関連を使うものがあって、これはuser has_many ordersと指定していれば以下のようにできる。
user.orders.find(:params[:id])
Don't Expose Controller Methods
コントローラ内のパブリックメソッドであるアクションはユーザに直接実行される場合があるので、そのこともよーく考えてアクセス権限等が守られるように注意すること。例えば以下のようにオーナのユーザIDを確認する。
def read @email = Email.find(params[:id]) unless @email.owner_id == session[:user_id] flash[:notice] = "Not Found" redirect_to(:action => 'index') end end
File Uploads
ユーザにアップロードさせたファイルをユーザにダウンロードさせる場合、そのファイルが.rhtmlや.cgi等の実行形式のファイルだったりすると、ダウンロード時に実行され任意のコードを実行される恐れがある。これを防ぐ方法としては、まずアップロードされたファイルを直接アクセスできるディレクトリに置かないようにし、アクションでそれらのファイルを表示するようにする。その際、注意する点としては
- 指定されたファイル名が存在するファイル名またはDBに登録されているファイル名かどうかを確認すること。その際../../etc/passwdのようなファイル名は許可しないようにする。
- ダウンロードしたファイルをブラウザで表示させる場合はHTMLシーケンスをエスケープすること。またバイナリデータをダウンロードさせる場合はブラウザで表示しないようにContent-Typeに適当なタイプを設定すること。
Don't Cache Authenticated Pages
ページキャッシングはセキュリティフィルタをパスしてしまうので、セッション情報でアクセス制御を行ないたい場合は、アクションキャッシングまたはフラグメントキャッシングを使いましょう。
オブLOVE夜会行ってきたよ
「オブLOVE夜会第2弾「イマドキのJavaScript」」行ってきましたよ。
- 会場までの道のり寒かった…。
- JavaScriptで作ったプレゼンツール(これ。「s」「w」押してみ、押してみ)衝撃。しかも1時間で作ったって…。
- JavaScript歴8ヶ月衝撃。しかもプログラミング歴2年って…。
- OO的JavaScriptが解説されているドキュメント探してたのでオススメはありがたし。早速読もっと。
- prototype.jsソース嫁 & script.aculo.usソース嫁。読みます。
- Giza衝撃。Giza.LoggerのRemoteスゴッ。
- 飲み会ではあまり席を移動せずorz。
- スーツじゃ浮くかなと思ったけど割とスーツ率高し。おじさんじゃ浮くかなと思ったけど割と平均年齢高し:-)。
- 今回の勉強会の目標「id:secondlifeさんにあいさつすること」達成。
どもども皆様ありがとうございました。
AWDwR "Chapter20 Web Services on Rails"
Chapter20を書いた人&Action Web Serviceのコード書いた人は南アフリカの人だって。Chapter18書いた人はオーストリアだし、DHHはデンマークだし、グローバルだね。
Action Web Serviceは長いんでAWSと省略するよ。
What AWS Is (and What It Isn't)
AWSではSOAPやWSDLのW3Cの仕様を全部実装したりXML-RPCの機能を全て提供するつもりはなくて、その代わりWebサービスで使いたい機能にフォーカスしている。このAWSを使うと
The API Definition
AWSを使う時に問題となるのは、Rubyのメソッドは任意の型を返すことができるけれど、受け取り側がRubyのようなダイナミックな言語じゃない場合は想定していない型が返ってくるとエラーになっちゃうこと。そこで型をちゃんと定義しておくためのAPI定義クラスというのを用意しておきました。これも以下のようにジェネレータを使うと自動生成できる。
ruby script/generate web_service Backend find_all_products find_product_by_id
こうするとスタブAPI定義のBackendApiというクラスとスケルトンコントローラのBackendControllerというクラスとそれから機能テスト用のBackendApiTestというクラスができる。
コントローラでは、wsdl_service_name()メソッドでWSDLで使われる名前を設定し、web_service_scaffold()で開発時にWebブラウザからWebサービスを呼べる機能を追加する。
API定義はapi_method()メソッドを使って行なう。引数や戻り値の型の指定はシグナチャで指定。api_method()メソッドは引数の型を指定する:expectsと戻り値の型を指定する:returnsの2つのオプション受け取り、:expectsを省略した場合はリモートから引数付で呼び出されるとエラーとなり、:returnsを省略した場合はリモートへは何も返さない。
で、定義の仕方なんだけど、まず基本型は以下のようなシンボルで指定する。
- :int 整数型
- :string 文字列型
- :base64 Base64でエンコードしたバイナリ
- :float 浮動小数点型
- :time タイムスタンプ型。RubyではTime
- :datetime タイムスタンプ型。RubyではDateTime
これら以外にもActionWebService::StructやActiveRecord::Baseのオブジェクトをシグナチャとして使うことができる。
で、シグナチャの書き方の例
- [ [:string] ] 文字列の配列
- [:bool] ブーリアン型
- [Person] Person型
- [{:lastname=>:string}] lastnameという名前つきの文字列型
- [:int, :int] 2つの整数型
Dispatching Modes
リモートからのリクエストとメソッドを結びつけるのがディスパッチなんだけど、デフォルトのディスパッチモードはDirect Dispatching。これを使う場合は特に設定は不要。Direct Dispatchingの場合、API定義は直接コントローラに結び付けられ、APIメソッドはコントローラのパブリックなインスタンスメソッドとして実装する。この方法はシンプルなんだけど、一つのAPI定義しかコントローラに結び付けられないのが欠点。Layered Dispatchingは一つのコントローラに全てのAPIを結び付け、Delegated DispatchingはいくつかのAPIを一つのコントローラに結びつける。このディスパッチングモードはコントローラ内でweb_service_dispatching_mode()で指定できる。
Using Alternate Dispatching
Layered Dispatchingを使う場合はリクエストと呼び出すメソッドのマッピングをweb_serevice()メソッドで定義しておく。Delegated Dispatchingもweb_service_dispatching_mode()に:delegateを指定する以外は同じ。
Method Invocation Interception
AWSにはリクエストの前または後で起動されるコールバックを登録することができるbefore_invocation()とafter_invocation()というのがあって、ActionPackのフィルタと同じ感じで以下のように使う。
class BackendAuthController < ApplicationController before_invocation :authenticate protected def authenticate(name, args) ... end end
before_invocation()メソッドで呼ばれるメソッドにはインターセプトしたメソッド名とその引数の配列が渡され、after_invocation()で呼ばれるメソッドはそれ以外にインターセプトしたメソッドの戻り値が渡される。またbefore_invocation()メソッド、after_invocation()メソッドには適用するメソッド、適用しないメソッドを指定する:onlyと:exceptというオプションがある。
Testing Web Services
Railsのテスト機能をAWSでも使えるよ。ジェネレータで機能テスト用のスケルトンが生成されるのでそれを書き換えて使いましょう。invoke(method_name, *args)メソッドはWebサービスを呼び出すメソッド。ただしこのinvoke()メソッドはDirect Dispatchingのコントローラに対してのみで、Layered Dispatchingの場合はinvoke_layered()メソッド、Delegated Dispatchingの場合はinvoke_delegated()を使いましょう。
で、WSDLを取得するには以下のURLにアクセスする。
http://www.foo.com/CONTROLLER/service.wsdl
また、XML-RPCの場合はエンドポイントURLを知る必要があって、Direct DispatchingとLayered Dispatchingの場合のエンドポイントは
http://www.foo.com/PATH/TO/CONTROLLER/api
Delegated Dispatchingの場合のエンドポイントURLは
http://www.foo.com/PATH/TO/CONTROLLER/SERVICE_NAME
となる。
オブLOVE夜会当選したよ
行きますよー。