Rails Web アプリケーションをもっと速く
こんなストーリーを考えてみます。
あなたは、Railsを学び、アプリケーションを作成し、サービスをインターネットに公開しました。しばらくすると、最初のユーザができます。あなたはとてもハッピーです。そうするうちにユーザが二人増え、十人になり、百人になりました。あなたはハッピーです、ユーザーもみんなハッピーです。
でも、ユーザが千人になり、一万人になり…。といった場合、何が起こるでしょうか?
そこで起こるのはアプリケーションへの同時接続数増加によるサービス提供速度の低下です。ユーザ数が一万人を越えてしまうWebサーバに特有の問題は、C10K問題として知られています。
それでなくとも、残念ながらRailsは同様他種フレームワークと比べて、単位時間あたりの処理量が低いことで知られています。その理由は、RailsではRubyが遅くて、NativeThreadに対応できず、ERbが遅くて、Helperが遅くて、Routingが遅い。とさまざまです。
ユーザー数の増加が現在のアーキテクチャの許容以上の負荷になってしまった場合、考えられる抜本的な対策として、
- 別の言語で書き直す(PythonやPerlやPHPやC++)
- インフラを外部にまかせてしまう(Google App Engine、Amazon EC2、Microsoft Live Mesh)
といったものがあります。
まず最初の、別の言語で書き直す。これもよくある決断です。アーロン・シュワルツ(Aaron Swartz)はredditをCommon LispからPhytonに書き直しました。また、TwitterがRuby On RailsからPHPやJavaに乗り換えるという噂は事あるごとに流布されます。
次の、「インフラをGoogle App Engine等のCloud Computerにまかせてしまう」というのは今後のトレンドになりそうです。ただし、Cloud Computerの利用が一般化した場合、現在のWeb技術者が持っている、サーバ側のDB、ネットワーク、IOのチューニングの知識のいくらかは陳腐化してしまうでしょう。
ここではそういった決断をふみとどまって、現状の環境でもうちょっとがんばってみよう、という場合の、Railsアプリケーションのパフォーマンス測定・改善法について考えていきたいと思います。
Measure before optimizing~Railsを計測しよう
Railsアプリケーションが遅い!からといって、闇雲に最適化を開始するのはあまりおすすめできません。アプリケーションの最適化に入る前に、さまざまな視点から"計測"が必要になります。
Rubyコードの計測:ruby-prof
当然ながらRailsアプリケーションは、Rubyで出来ています。Rubyの性能を測定するための事実上標準のprofilerがruby-profです。
それでは、ruby-profの使用法について解説したいと思います。
ruby-profileはgemパッケージとして提供されています。まずはgemコマンドでインストールを行います。
次に、インストールされたパッケージの、ruby-prof/rails_plugin/ruby-profを、Railsアプリケーションフォルダのvendors/pluginsにコピーし、pluginとしてインストールします。
次に、コピーしたruby-prof/lib/profiling.rbを以下のようにコメントアウト、コメント除去します。
ここまで設定した段階で、ruby-profが有効になります。railsサーバを起動します。
起動した、サーバに対するアクセス結果がlog/callgrind.outとして出力されます。
出力されたcallgrindを閲覧するためのビューワがKCachegrindです。
Linux系のディストリビューションに対してはパッケージが提供されていますが、それ以外のOSについてはソースコードでの提供になります。そのためインストール手順については省略します。
KCachegrindを実行してみます。
以下が実行例です。
ruby-profとKCachegrindによってボトルネックが可視化されました。
Rubyのプロファイリング結果に基づく、パフォーマンス改善法については、Rubyの作者まつもとゆきひろ氏による「まつもと直伝 プログラミングのオキテ 第18回 プログラムを高速化する(その2)」等が参考になります。
リソースアクセス性能の計測:httperf
次に、Railsが最終的に作成する、HTMLやXMLやJSONといったリソースへの、HTTPリクエストによるアクセス性能を測定します。
こういったアクセス性能の測定用のツールとしてApache HTTP Serverに付属するabコマンドがよく知られています。
ですがここでは、より高機能のhttperfを利用してみます。
httperf -helpを実行すると、いろいろなオプションがありますが、本稿で利用するもののみ紹介します。
- --server
- サーバ名を指定します。
- --port
- ポート番号を指定します。
- --uri
- ルートからのURIを指定します。
- --num-conns
- 総コネクション数です。ここで指定した回数だけHTTPでのコネクションが行われます。
- --num-calls
- 1つのコネクションで何回リクエストを送るかを指定します。KeepAliveが有効の場合に指定します。
- --rate
- 一秒間に何件のリクエストが発生するかを指定します。本稿では、このオプションが使いたいためにabではなく、httperfを利用しています。
使用例
http://localhost/user/linksというURIに対して、1秒間に100回リクエストが発生し、1000回で終了する場合以下のようになります。
ここで見るべき指標は、一秒間にどれだけのリクエストをさばけるかの値が表示されている、「Request rate:40.9 req/s (24.4 ms/req)」です。上の例では、1秒間に40.9リクエストの提供が可能であることが表示されています。
ウェブサイトの計測:Firebug
単一のリソース性能につづいて、今度はウェブサイトの測定です。
ユーザがアクセスする実際のウェブサイトはRailsが作成するHTMLファイルだけでなく、HTMLにリンクされたJavaScriptファイル CSSファイル、JPGやGIFの画像ファイルといった総体です。これらのデータを取得し、ブラウザの中でHTMLをレンダリングし、画像を読み込み、CSSを評価し、JavaScriptを実行することで実際のWebサイトはユーザの手に届きます。
この部分を包括的に測定する良いツールはあまりないのですが、本稿ではFirefoxブラウザのアドオンでありJavaScriptのデバッグ・開発環境であるFirebugの機能を利用したいと思います。
まずは、Firefoxのインストールと、Firefox のアドオンである、Firebugのインストールを行います。
インストール後Firebugを有効にし、Webサイトにアクセスすることで、Webサイトのパフォーマンスが測定できます。ただし、Firebugは1回のアクセスしか測定しないため、少ない回数の試行では統計的な誤りが発生します。
見るべきは、Firebugの接続タブです。
一覧にはそれぞれ、リソース、サイズ、ダウンロードのタイミング、かかった時間が表示されています。
最下行を見ると、総リクエスト数、総ダウンロードバイト数、総秒数が表示されています。
Railsパフォーマンスチューニング
これらの測定結果を検討し、方針をきめ、対策を打ち、再度計測して、再度改良する。というサイクルを回すことで、Railsパフォーマンスチューニングはすすんでいきます。
フロントエンドの高速化
Railsのパフォーマンスのうち、バックエンドの性能、先の計測値でいう「Rubyコードの計測」への対応については、ケースバイケースの、自己発見学習による試行錯誤しか手段が無いのではないかと思います。
ですが、フロントエンドの高速化、先の測定値でいう「リソースアクセス性能の計測」「ウェブサイトの計測」への対応については、Yahoo!Inc.パフォーマンス担当責任者のスティーブサウダーズ(Steve Souders)による、「高速ウェブサイトを実現する14のルール 」が公開され反響を呼んでいます。
同記事の邦訳は「パフォーマンスチューニングBlog」に掲載されています。同サイトでは14のルールの邦訳以外にも、パフォーマンスチューニングの示唆に富んだ記事が数多く掲載されており必見です。
Steve Soudersが提唱する「高速ウェブサイトを実現する14のルール」は以下です。
- ルール1. HTTPリクエストを減らす
- ルール2. CDN を使う
- ルール3. Expires ヘッダを設定する
- ルール4. コンポーネントを gzip する
- ルール5. スタイルシートは先頭に置く
- ルール6. スクリプトは最後に置く
- ルール7. CSS expression の使用を控える
- ルール8. JavaScript と CSS は外部ファイル化する
- ルール9. DNS ルックアップを減らす
- ルール10. JavaScript を縮小化する
- ルール11. リダイレクトを避ける
- ルール12. スクリプトを重複させない
- ルール13. ETag の設定を変更する
- ルール14. Ajax をキャッシュ可能にする
これらのルールの検証を自動的に行う、YSlowというFirefoxアドオンがYahoo!から提供されています。ここで、YSlowもインストールしておきましょう。YSlowはFirebugに組込まれて動作します。
14のルールは重要な順に並んでいます。このうち、いくつかの効果の高い対策を本稿でも見ていきます。
ルール1. HTTPリクエストを減らす
このルールは、HTTPリクエストは少なければ少ないほどよい、まとめられるものはまとめて送受信しよう。転送量が同じであっても、HTTPリクエスト・レスポンスのオーバヘッドがあるため、ファイルを結合してしまった方が早い。ということです。
HTTPリクエストの削減を行うための、Railsの機能が、AssetCachingです。production環境でのみ動作します。
使い方は簡単です。stylesheet_link_tagもしくは、javascript_include_tagに:cacheオプションをつけます。「:cache=>true」を指定すると、指定したCSSや、Javascriptを連結したファイルを作成し、そのファイルをリンクします。
実際にどう展開されるかを、script/consoleで試してみます(整形しています)。
リンクされるファイル数が削減されることで結果的にHTTPメソッドの発行回数が減少します。
Firebug で「http://localhost/user/links」URIを測定してみたところ、:cache オプションの有無によって、Webサイトの描画処理の完了までに「900ms」→ 「138ms 」という予想以上の違いがでました。
ルール2. CDN を使う(+ルール6: スクリプトは最後に置く「並列ダウンロード」)
CDNとは、コンテンツデリバリーネットワークの略で、リソースファイルの分散提供を行う仕組のことです。Akamaiや、CDNソリューションズといったCDNを専門に行うサービスプロバイダが知られています。
そういったCDNサービスプロバイダを利用する事が予算的に難しい場合でも、Rails2.0では、自前CDNとも言える機能があります。
それが、AssetServersです。
production環境でしか有効にならないため、設定は、config/environments/production.rbで行います。
上の例では、%dで指定された箇所に0~3までの数値がランダムで入ります。
このAssetServersのメリットは、リソースの並列ダウンロードが可能になることです。HTTP1.1プロトコルの制限として、一つのホストに対して2つのコネクションしか同時に接続することができない、というものがあります。その制限に対し、AssertServersでRailsサーバと別のサーバ(別サーバで無く、サブドメインでもかまいません)を設定することで、ブラウザは各種のリソースファイルが別ホストに設置されていると判断して、並列にリソースのダウンロードを行います。
このAssetServersと関連する事項として、Rails2.0ではセッションがクライアント側cookieに格納されるようになりました。そのためクッキーのサイズは、Rails2.0以前より肥大化する傾向にあります。この変更の意外な盲点として、クッキーを渡したRailsサーバと同一のサーバへの全てのリクエストに対してその肥大化したクッキーが含まれる、という事実があります。
クッキーを含んだリクエストは、HTMLページへのアクセス時だけではなく、JPGやCSS、Javascriptといったリソースについても同様に行われます。こういったリソースファイルに対してクッキーを送ってしまうのは、単純にネットワーク帯域の無駄です。AssetServersの機能を利用することで、その問題の回避も行えます。
ルール3. Expiresヘッダを設定する
これは、「Expiresヘッダを付けて、クライアント側キャッシュを有効に行わせよう」というルールです。
Expiresヘッダが付与されていない場合、クライアントのキャッシュにあるリソースが有効期限切れかどうかをチェックする条件付GETリクエスト(If-Modified-Since、If-None-Match)が発生します。確認の結果、有効期限内であった場合、これは単純にネットワークのオーバーヘッドです。Expiresヘッダを付与することで、有効期限が切れるまで確認リクエストの発生を抑制することができます。
Apacheでmod_expiresモジュールを有効にしたうえで、以下の設定をおこないます。
あまり上手い設定例ではありませんが、jsとcssに対して「\?\d*」を付けています。これは、stylesheet_link_tag、javascript_include_tagヘルパーが、リソース名の後に、ファイルの更新日時から作られた数値を自動で付与するためです。
expiresヘッダの検証は、YSlowアドオンにて行えます。
ルール4. コンポーネントをgzipする
圧縮可能な、コンテンツの圧縮を行いましょう。というルールです。
Apache2 mod_deflateモジュールを有効にして、リソースをgzipしてみます。以下設定例です。
上の設定を適用後、実際にgzip化されているかどうか、HTTPクライアントのcurlで検証してみます。
miniciousに対して、httperfで検証してみたところ、20%程、秒間性能が向上しました。
バックエンドの高速化、負荷分散とキャッシュ
前節までフロントエンドの高速化について概観しました。つづいてバックエンドの高速化についても、何点か触れておきたいともいます。
負荷分散:Apache2 mod_proxy_balancer
Apache2以降、mod_proxy_balancerというソフトウェア的に負荷分散を行うリバースプロキシモジュールが追加されました。これを利用してRailsサーバの負荷分散を行う方法が広く知られています。
また、Rails2.0でセッションがcookieベースとなったため、負荷分散時のセッション同期の考慮が不要になり、よりいっそうリバースプロキシが使いやすくなりました。これもサーバに状態を持たないというRESTの原則のメリットと言えます。
それでは、Apache2での設定例を簡単に紹介します。
Apache2のhttpd.confに以下の様に記述します。
上の設定で、3つのRailsサーバが負荷分散のメンバとなり、Apache2 mod_proxy_balancerによって負荷分散されます。
複数サーバが用意できなかったため、Pentium4、シングルコアのPC上で、mongrelを別ポートで3プロセスたちあげる、という非常に簡易な方法で負荷分散の検証をしてみました。
httperfで計測してみたところ、上の条件でも15%程、単位時間あたりの性能が向上しました。シングルコアでかつ単一サーバでの検証のため、タスクスイッチのオーバーヘッドにより性能はほとんど改善しないのではないか、と思われていたため、これはやや意外な結果でした。
RailsアプリケーションがCPU性能を使い切っていない場合、シングルコア単一サーバでのマルチプロセスによる負荷分散も効果が上がると言う結果になってしまいましたが、複数サーバや、メニーコアであれば、もちろんさらなる効果が期待できます。リバースプロキシはRailsの構成として積極的に使っていきたい機能です。
キャッシュ
Railsアプリケーションの高速化の手段として、最も効果的なのが、Railsの利用を避けることです。つまり、Railsの実行結果から静的なファイルを作成し、Railsの世界に入らずに、クライアントに対してHTTPサーバからレスポンスを返してしまう、という方法です。
Railsページキャッシュ
Railsはアクションの実行結果をファイルとして保存し、Railsを介さずにHTTPサーバからレスポンスを返すためのページキャッシュの仕組を持ちます。古くからある機能で、非常にシンプルですが非常に強力です。
cache_pageメソッドで、キャッシュを行うアクションを指定しています。ページキャッシュの削除には、expire_pageメソッドを利用します。
本対策の効果は劇的です。「http://localhost/user/links」へのhttperfの実行結果は、目を疑うような、約60倍の性能を示しました。「45req/sec」→「2700req/sec」。
本対策の問題点は、大きくわけて2つです。
- 同一のURIで複数の表現をとりうるサイトには利用できない
- キャッシュが破棄されるまではキャッシュの内容を表示し続けてしまう
サーバ側で状態を持たないというRESTの原則に従っていれば、1の問題はクリアできます。ですが残念ながらminiciousでは、ログインしたユーザと、ゲストユーザでHTMLリソースの一部を変えてしまっているため、この問題をクリアできません。
なお、セキュリティ上の考慮は必要ですが、クッキーとJavascriptを用いての解決法がBruce Tateにより提案されています(現実の世界の Rails, 第二回「高度なページ・キャッシング」 )。
2の問題は、Railsコード内からexpire_pageメソッドによりキャッシュファイルの削除が可能であるため、データの作成、更新、削除時にキャッシュを削除することで、ある程度は解決できます。
いくつかの問題はありますが、単一のURIで一つの表現しかもたず、かつ、データの変更に過敏ではないページでは是非とも利用すべき機能です。
Apache2 mod_cache
Apache2のモジュールである、mod_cacheモジュールでもRailsのページキャッシュと同様に、Rails実行結果のサーバ側キャッシュが行えます。
mod_cacheでは、HTTP/1.1(RFC2616)準拠のHTTPヘッダによるコンテンツキャッシュを行うため、RESTスタイル的にはこちらの方が適切であると言えるかもしれません。
性能については、Railsページキャッシュとほぼ同様の「45req/sec」→「3000req/sec」、60倍から70倍の性能になりました。
問題についても、Railsのページキャッシュとほぼ同様ですが、Rails内からキャッシュの明示的削除が出来ない点については不利と言えます。
ただし、mod_cacheは制限として、Basic認証の行われているページと、POSTメソッドの実行時についてはキャッシュを行いません。この辺りの制限や、条件付きGETなどをRailsからうまく利用できれば、超速Webサイト構築の道があるかもしれません(本稿では上手い方法が見付けられませんでした)。
それではApache2での設定例を解説します。あらかじめmod_cache, mod_mem_cacheを有効にした上でhttpd.confに以下を記述します。
検証した限りでは、上の指定だけではキャッシュを行ってくれませんでした。
Railsはクライアントへのレスポンスに以下のヘッダを付与します。
Cache-Control:private, max-age=0, must-revalidate
ですが、mod_cacheモジュールのオプションにmax-age=0, must-revalidateの指定を無視するものは無いようです。そのため推奨できる手段ではありませんが、Railsのコードを直接編集して検証を行いました。
下の例では、Cache-Control:ヘッダから、max-age=0とmust-revalidateを除去しています。
なお、この部分のコードを見ると、RailsのETagヘッダがMessageBodyのMD5値であることも分かります。そのため、リバースプロクシで負荷分散を行う場合、HTMLやXMLの文面は全く同じである必要があります。特にstylesheet_link_tagやjavascript_include_tagがファイルの更新時間から作成する、CSSや、JavascriptファイルのURIに注意が必要です。
まとめ
RailsWebアプリケーションの計測法と、フロントエンドのパフォーマンスチューニング、バックエンドのキャッシュ方法について見ていきました。
Rails2.0のRESTサポートと、フロントエンド、バックエンドのパフォーマンスチューニングの有機的な連携が、非常に強力であることが垣間見えたのではないかと思います。
参考文献
最後に本特集全体の参考文献を紹介して記事を終えようと思います。ありがとうございました。