Rails Web アプリケーションをもっと速く
こんなストーリーを考えてみます。
あなたは、
でも、
そこで起こるのはアプリケーションへの同時接続数増加によるサービス提供速度の低下です。ユーザ数が一万人を越えてしまうWebサーバに特有の問題は、
それでなくとも、
ユーザー数の増加が現在のアーキテクチャの許容以上の負荷になってしまった場合、
- 別の言語で書き直す
(PythonやPerlやPHPやC++) - インフラを外部にまかせてしまう
(Google App Engine、 Amazon EC2、 Microsoft Live Mesh)
といったものがあります。
まず最初の、
次の、
ここではそういった決断をふみとどまって、
Measure before optimizing~Railsを計測しよう
Railsアプリケーションが遅い!
Rubyコードの計測:ruby-prof
当然ながらRailsアプリケーションは、
それでは、
ruby-profileはgemパッケージとして提供されています。まずはgemコマンドでインストールを行います。
# gem install ruby-prof
次に、
% cd minicious % cp -R /usr/lib/ruby/gems/1.8/gems/ruby-prof-0.6.0/rails_plugin/ruby-prof vendor/plugins
次に、
# Create a flat printer
#printer = RubyProf::FlatPrinter.new(result)
#printer.print(output, {:min_percent => 1,
# :print_file => false})
#logger.info(output.string)
## Used for KCacheGrind visualizations
printer = RubyProf::CallTreePrinter.new(result)
path = File.join(LOG_PATH, 'callgrind.out')
File.open(path, 'w') do |file|
printer.print(file, {:min_percent => 1,
:print_file => true})
end
ここまで設定した段階で、
% script/server
起動した、
出力されたcallgrindを閲覧するためのビューワがKCachegrindです。
Linux系のディストリビューションに対してはパッケージが提供されていますが、
KCachegrindを実行してみます。
% kcachegrind log/callgrind.out
以下が実行例です。

ruby-profとKCachegrindによってボトルネックが可視化されました。
Rubyのプロファイリング結果に基づく、
リソースアクセス性能の計測:httperf
次に、
こういったアクセス性能の測定用のツールとしてApache HTTP Serverに付属するabコマンドがよく知られています。
ですがここでは、
httperf -helpを実行すると、
- --server
- サーバ名を指定します。
- --port
- ポート番号を指定します。
- --uri
- ルートからのURIを指定します。
- --num-conns
- 総コネクション数です。ここで指定した回数だけHTTPでのコネクションが行われます。
- --num-calls
- 1つのコネクションで何回リクエストを送るかを指定します。KeepAliveが有効の場合に指定します。
- --rate
- 一秒間に何件のリクエストが発生するかを指定します。本稿では、
このオプションが使いたいためにabではなく、 httperfを利用しています。
使用例
http://
% httperf --server=localhost --port=80 --uri=/user/links --num-calls=1000 --rate=100 httperf --client=0/1 --server=localhost --port=80 --uri=/user/links --rate=100 --send-buffer=4096 --recv-buffer=16384 --num-conns=1 --num-calls=1000 Maximum connect burst length: 0 Total: connections 1 requests 102 replies 101 test-duration 2.491 s Connection rate: 0.4 conn/s (2491.0 ms/conn, <=1 concurrent connections) Connection time [ms]: min 2491.0 avg 2491.0 max 2491.0 median 2491.5 stddev 0.0 Connection time [ms]: connect 0.1 Connection length [replies/conn]: 101.000 Request rate: 40.9 req/s (24.4 ms/req) Request size [B]: 70.0 Reply rate [replies/s]: min 0.0 avg 0.0 max 0.0 stddev 0.0 (0 samples) Reply time [ms]: response 24.7 transfer 0.0 Reply size [B]: header 316.0 content 1372.0 footer 0.0 (total 1688.0) Reply status: 1xx=0 2xx=101 3xx=0 4xx=0 5xx=0 CPU time [s]: user 0.90 system 1.54 (user 36.0% system 62.0% total 98.0%) Net I/O: 69.7 KB/s (0.6*10^6 bps) Errors: total 1 client-timo 0 socket-timo 0 connrefused
ここで見るべき指標は、
ウェブサイトの計測:Firebug
単一のリソース性能につづいて、
ユーザがアクセスする実際のウェブサイトはRailsが作成するHTMLファイルだけでなく、
この部分を包括的に測定する良いツールはあまりないのですが、
まずは、
インストール後Firebugを有効にし、
見るべきは、

一覧にはそれぞれ、
最下行を見ると、
Railsパフォーマンスチューニング
これらの測定結果を検討し、
フロントエンドの高速化
Railsのパフォーマンスのうち、
ですが、
同記事の邦訳は
Steve Soudersが提唱する
- ルール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 をキャッシュ可能にする
これらのルールの検証を自動的に行う、
14のルールは重要な順に並んでいます。このうち、
ルール1. HTTPリクエストを減らす
このルールは、
HTTPリクエストの削減を行うための、
使い方は簡単です。stylesheet_
<html>
<head>
..略...
<%= stylesheet_link_tag "application", "scafolld", :cache=>true %>
</head>
...略...
<%= javascript_include_tag :defaults, :cache=>true %>
</body>
</html>
実際にどう展開されるかを、
>> helper.javascript_include_tag :defaults
<script src="/javascripts/prototype.js?1209407360" type="text/javascript"></script>
<script src="/javascripts/effects.js?1209407360" type="text/javascript"></script>
<script src="/javascripts/dragdrop.js?1209407360" type="text/javascript"></script>
<script src="/javascripts/controls.js?1209407360" type="text/javascript"></script>
<script src="/javascripts/application.js?1209407360" type="text/javascript"></script>
>> helper.javascript_include_tag :defaults, :cache=>true
<script src="/javascripts/all.js?1211121143" type="text/javascript"></script>
リンクされるファイル数が削減されることで結果的にHTTPメソッドの発行回数が減少します。
Firebug で
ルール2. CDN を使う(+ルール6: スクリプトは最後に置く「並列ダウンロード」)
CDNとは、
そういったCDNサービスプロバイダを利用する事が予算的に難しい場合でも、
それが、
production環境でしか有効にならないため、
config.action_controller.asset_host =
"http://asset%d.example.com"
上の例では、
このAssetServersのメリットは、
このAssetServersと関連する事項として、
クッキーを含んだリクエストは、
ルール3. Expiresヘッダを設定する
これは、
Expiresヘッダが付与されていない場合、
Apacheでmod_
ExpiresActive On
<FilesMatch "\.(gif|jpg)$">
ExpiresDefault "access plus 10 years"
</FilesMatch>
<FilesMatch "\.(js|css)\?\d*$">
ExpiresDefault "access plus 10 years"
</FilesMatch>
あまり上手い設定例ではありませんが、
<link href="/stylesheets/scaffold.css?1210726647" media="screen" rel="stylesheet" type="text/css" />
expiresヘッダの検証は、
ルール4. コンポーネントをgzipする
圧縮可能な、
Apache2 mod_
<Location />
SetOutputFilter DEFLATE
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
SetEnvIfNoCase Request_URI \
\.(?:gif|jpe?g|png)$ no-gzip dont-vary
Header append Vary User-Agent env=!dont-vary
</Location>
上の設定を適用後、
% curl -v --compressed http://localhost/user/links.xml * About to connect() to localhost port 80 * Trying 127.0.0.1... connected * Connected to localhost (127.0.0.1) port 80 > GET /user/links.xml HTTP/1.1 > User-Agent: curl/7.15.5 (i486-pc-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8c zlib/1.2.3 libidn/0.6.5 > Host: localhost > Accept: */* > Accept-Encoding: deflate, gzip > < HTTP/1.1 200 OK < Date: Sun, 18 May 2008 12:57:01 GMT < Server: Mongrel 1.1.4 < Status: 200 OK < X-Runtime: 0.00886 < ETag: "ac8f5fd586f76c0f9414fba9fdbee14a"-gzip < Cache-Control: private, max-age=0, must-revalidate < Content-Type: application/xml; charset=utf-8 < Set-Cookie: _minicious_session=BAh7BiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNo%250ASGFzaHsABjoKQHVzZWR7AA%253D%253D--8b433eee9710a010fb4879e70a25b36bfd7d7f23; path=/ < Vary: Accept-Encoding,User-Agent < Content-Encoding: gzip < Content-Length: 236 (以下省略)
miniciousに対して、
バックエンドの高速化、負荷分散とキャッシュ
前節までフロントエンドの高速化について概観しました。つづいてバックエンドの高速化についても、
負荷分散:Apache2 mod_proxy_balancer
Apache2以降、
また、
それでは、
Apache2のhttpd.
<Proxy balancer://minicious/>
BalancerMember http://192.168.1.1:3000
BalancerMember http://192.168.1.2:3000
BalancerMember http://192.168.1.3:3000
</Proxy>
ProxyPass / balancer://minicious/
ProxyPassReverse / balancer://minicious/
上の設定で、
複数サーバが用意できなかったため、
httperfで計測してみたところ、
RailsアプリケーションがCPU性能を使い切っていない場合、
キャッシュ
Railsアプリケーションの高速化の手段として、
Railsページキャッシュ
Railsはアクションの実行結果をファイルとして保存し、
class LinksController < ApplicationController
caches_page :index
def index
user = User.find_by_login(params[:username])
@tags = Tag.find_by_user_name(params[:username])
@links = user.links
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @links } end
end
end
# ...略
end
cache_
本対策の効果は劇的です。
本対策の問題点は、
- 同一のURIで複数の表現をとりうるサイトには利用できない
- キャッシュが破棄されるまではキャッシュの内容を表示し続けてしまう
サーバ側で状態を持たないというRESTの原則に従っていれば、
なお、 2の問題は、 いくつかの問題はありますが、 Apache2のモジュールである、 mod_ 性能については、 問題についても、 ただし、 それではApache2での設定例を解説します。あらかじめmod_ 検証した限りでは、 Railsはクライアントへのレスポンスに以下のヘッダを付与します。 ですが、 下の例では、 なお、 RailsWebアプリケーションの計測法と、 Rails2. 最後に本特集全体の参考文献を紹介して記事を終えようと思います。ありがとうございました。Apache2 mod_
CacheRoot /var/www/cache/ # キャッシュファイルの置き場所
CacheDisable /session # キャッシュしないURI
CacheDisable /login
CacheDisable /logout
CacheEnable disk / # キャッシュするURI
CacheStorePrivate On # CacheControl: private を無視
CacheIgnoreHeaders Set-Cookie # Cookie はキャッシュしない
CacheDefaultExpire 60 # 1分後にキャッシュ削除
CacheDirLevels 3 # キャッシュする階層
CacheDirLength 6 # キャッシュディレクトリの長さ
def handle_conditional_get!
if body.is_a?(String) && (headers['Status'] ? headers['Status'][0..2] == '200' : true) && !body.empty?
self.headers['ETag'] ||= %("#{Digest::MD5.hexdigest(body)}")
#self.headers['Cache-Control'] = 'private, max-age=0, must-revalidate' if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
# max-age と must-revalidate を除去
self.headers['Cache-Control'] = 'private' if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
if request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag']
self.headers['Status'] = '304 Not Modified'
self.body = ''
end
end
end
まとめ
参考文献