- 公開日
Roppongi.rbで「Rails高速化戦略」を発表しました
自分がオーガナイザーを務めた Roppongi.rb #1で「Rails高速化戦略」という題で発表してきました。スライドは下記になります。
発表内容をこちらのブログでも文章形式でざっとまとめてみたいと思います。
Rails (Ruby) 遅いよね
RailsないしRubyはプログラミング言語の中では速くはない言語であることは言うまでもないと思う1。 実際に「Rails/Ruby遅いよねって今まで思ったことある方どれくらいいますか?」と会場でも聞いてみたところ、予想では半数以上手を挙げてくれると思ったのだけど、実際は30人中3~4人くらい。あまりにも意外な結果だったので自分なりに理由を分析してみると2つあるかなと思う。
パフォーマンスを求められないから
例えば社内の数人が使うような管理画面の場合。この場合、パフォーマンスよりも機能性(ちゃんと検索・閲覧できるかとかCRUD操作ができるかとか)などが優先されると思う。数人だけが使うのでアクセススパイクもないし、パフォーマンスが問題にもなりにくい。
Railsをフレームワークとして使っていないから
すごくパフォーマンスを求められるWebアプリの場合、それが事前にわかっているならまずは言語選択レベルでRailsを選択しないかもしれない。今ならGoとかElixirとかScalaとか代替言語もあるのでそちらを選択した場合はRailsは使わないことになるのでRailsの遅さで困ることもない。
それでもやっぱりRailsだ…!
それでもやっぱりRuby好きのRubyっ子であれば、Rubyは使いたい… ということで、Railsの高速化をする上での戦略を紹介。
Ruby Version Up
まずはRubyバージョンアップ。Rubyバージョンの歴史はこんな感じになっている。
- 2013.2: Ruby 2.0
- 2013.12: Ruby 2.1
- 2014.12: Ruby 2.2
- 2015.12: Ruby 2.3
- 20xx: Ruby 3.0
去年matzの口からRuby3のコンセプトが発表された。その驚くべき内容が Ruby 3 x 3 。
via. Ruby3 challenges - RubyKaigi 2015 Keynote - YouTube
とあるRailsアプリの場合
会社で取り組んでいるプロダクトのRubyのバージョンは基本的に最新バージョンを使うようにしているものの、中にはレガシーな環境もある。下記はあるプロダクトでRuby2.0 から Ruby2.1に上げた例。結果としては、Ruby 2.0 => 2.1 Ruby Version Up だけで レスポンス速度が約2倍向上した。
このようにRubyバージョンアップによりアプリケーションコード変更ゼロでも2、速度改善が期待できる。古いRubyお使いの方は今すぐRubyのバージョンアップ!
What about Rails?
じゃあRailsはどうだろうか。下記はamatsudaさんのmatzのRuby 3x3 を受けての発表。
?
が付いていることで分かる通り3倍速くなるという発表というより、まだまだRailsは速くするために工夫の余地があるよ、というような発表。
下記は同じamatsudaさんが発表されたRails Upgrade Casual Talksでの資料です。
via. Rails Upgrade Casual Talks // Speaker Deck
たしかに色んな機能が追加されている中、Railsが劇的に速くなることは考えにくい。解決策は…?
歯を食いしばってRails/Rubyをバージョンアップ
Rails 遅くなってもRuby は速くなっているので、どちらも最新版をしっかり追っかけていけば、遅くなることなくRailsの機能拡張も追っかけていけるのでOK.
ボトルネックを潰す
Railsアプリをどうボトルネックを発見し潰していくか?
推測するな、計測せよ
ボトルネックは計測して数値で示すもの。ボトルネックを発見するためのサービス・ツールをいくつか紹介。
- New Relic: 無料で使えて導入もラクでよい
- rack-mini-profiler: 開発環境導入する。クエリやpartialレンダー時間を表示。
- rack-lineprof: Rubyのコードを行単位で計測したい場合に有効
ツールを使った結果ボトルネックになりやすい箇所というとRDBまわり。それを解決するgem・機能を紹介。
ActiveRecord Optimization
問題発見型
- bullet: Kill
N+1
issue! - activerecord-cause: Logs where ActiveRecord actually loads record
DBスキーマ最適化型
- flag_shih_tzu: Bit fields for ActiveRecord
- counter-cache: cacheing count query result
- counter_culture: Better counter-cache
クエリ効率化型
- activerecord-precount: Yet another counter_cache alternative.
- activerecord-import: bulk inserting data
クエリを意識してActiveRecord使いこなそう
ActiveRecordもといORマッパの良さってDBを意識しなくて済むところ。でも高速化を行う上でクエリは避けられない壁。DBを意識せずコードを書いている最近のワカモノはもっとクエリを意識しよう! 老害っぽい発言だ
パーシャルレンダリングを減らす
N+1 partial rendering
データN個分render
処理が走ってしまうのを、個人的に N+1 rendering と呼んでいる。データの数N+親のビュー1回で N+1
. 例えばこんなコード。
<!-- views/items/index -->
<% @items.each do |item| %>
<%= render item %>
<% end %>
<!-- views/items/_item -->
<tr>
<td><%= item.name %></td>
<td><%= link_to 'Show', item %></td>
<td><%= link_to 'Edit', edit_item_path(item) %></td>
<td><%= link_to 'Destroy', item, method: :delete %></td>
</tr>
この場合のログはこうなる。
Processing by ItemsController#index as HTML
Rendering items/index.html.erb within layouts/application
Item Load (0.3ms) SELECT "items".* FROM "items"
Rendered items/_item.html.erb (0.5ms)
Rendered items/_item.html.erb (0.3ms)
...snip...
Rendered items/_item.html.erb (0.5ms)
Rendered items/_item.html.erb (0.3ms)
Rendered items/index.html.erb within layouts/application (57.7ms)
Completed 200 OK in 80ms (Views: 77.1ms | ActiveRecord: 0.3ms)
Viewで80msくらいかかっている。
Collection rendering
上記の場合、Collectionレンダーの機能を使えばもっと効率的にrenderできる。
<!-- views/items/index -->
<%= render @items %>
<!-- views/items/_item -->
<tr>
<td><%= item.name %></td>
<td><%= link_to 'Show', item %></td>
<td><%= link_to 'Edit', edit_item_path(item) %></td>
<td><%= link_to 'Destroy', item, method: :delete %></td>
</tr>
この場合のログはこうなる。
Processing by ItemsController#index as HTML
Rendering items/index.html.erb within layouts/application
Item Load (0.4ms) SELECT "items".* FROM "items"
Rendered collection of items/_item.html.erb [29 times] (6.9ms)
Rendered items/index.html.erb within layouts/application (10.3ms)
Completed 200 OK in 29ms (Views: 26.4ms | ActiveRecord: 0.4ms)
ビューで25msくらい。だいたい上記の例と比べると1/3くらいになっている。
Rails caching
RailsのCacheの仕組みとして公式ガイドで3つ紹介されているのだが、ご存知だろうか。
- Fragment Cache: View fragment caching.
- Action Cache: Controller’s action caching (removed in Rails4).
- Page Cache: Static page caching (removed in Rails4).
1. Fragment Cache
- グローバルナビ・サイドバーなどの多く呼ばれる共通コンテンツに有効
- 重い処理が走るビューの一部分であればあるほど高速化が期待できる
- Advanced Usage: Russian Doll Caching
2. Action Cache
- Rails4で削除されてgem化: actionpack-action_caching
- Viewの手前のControllerのAction自体の処理が重い場合に有効
cache_path
でキャッシュキーをカスタマイズ可能- モデルのupdated_at を組み込んだり、PC/スマフォでキャッシュビュー出し分け可能
3. Page Cache
- Rails4で削除されてgem化: actionpack-page_caching
- キャッシュ対象となるControllerのActionの生成するHTMLをまるっと静的ファイルに吐き出す
- その静的ファイルをNGINXなどのWeb Server/Reverse Proxyでハンドリング
Railsのキャッシュ戦略
- Railsデフォルトの FragmentCache を使ってビューのレンダリングを高速化
- それでもダメな場合や Controller 自体の処理が重い場合なら、ActionCache/PageCache を検討
注意
- キャッシュしても根っこの問題は消えない
- キャッシュのライフサイクル管理
- 用法用量を守って正しくお使いください
キャッシュしても根っこの問題はバイパスされるだけでそれ自体が解決されるわけではないので、本質的にはその根っこの問題を潰すほうがキャッシュより優先すべき。キャッシュによって臭いものには蓋をしていないか。キャッシュによって大きなボトルネックが隠蔽されていないか。本質的な問題を潰した上でなお高速化したい場合にキャッシュを利用するのが筋の良いキャッシュ戦略だと思う。
またキャッシュを行うことでそのライフサイクル管理も必要になってくることはアタマに入れておきたい。どういう場合にキャッシュがexpireすべきなのか(あるいはexpireすべきでないのか)、updateすべきなのか、削除すべきなのか。この辺もきちんと考えた上でキャッシュに取り組みたい。
静的ファイル配信
NGINX
プロダクション運用においては実際Railsが静的ファイルまでサーブすることはなくて、下記のようにNGINXに静的ファイルをサーブさせることが多い。
CDN
さらに言うと、Railsの吐く assets:precompile の成果物は、CDNに乗せちゃって配信を最適化してやるともっとよい。
レイテンシに負けないプロトコル = HTTP/2
バンド幅大きくなってもページロード時間は大きく変わらない。光の速度はこれ以上速くならない。じゃあどうするか。解決策がHTTP/2.
via. ウェブを速くするためにDeNAがやっていること - HTTP/2と、さらにその先
下記のBEFORE/AFTERは画像の配信をHTTPからHTTP/2に変更した場合のリクエストをキャプチャしたもの。
Before HTTP/2
HTTP/2前の状態。リクエストが順番に走っていることが見て取れる。
After HTTP/2
HTTP/2後の状態。リクエストが見事に多重化されている。
こちらのページではHTTPSの画像ロードの速度の速さを体感できる。
ユーザーの体感速度 = サーバーサイドレスポンス + クライアントサイド・スピード
仮にサーバーレスポンスタイムを1ms
にしたとしても、十分に速くなったとはいえない。なぜなら最終的にユーザーが感じるであろうウェブページの体感速度はサーバーサイドのレスポンス速度とクライアントサイドでのページロードのスピードを足し合わせたものだから。サーバーが0msでレスポンス返しても10秒間クライアントサイドの画面が真っ白だったら、ユーザーにとってはそれは10秒待たされてるのと一緒。
Rails HelloWorld App の場合
Rails5をほぼ素の状態でHello Worldという文字列を出力するアプリをHerokuにデプロイしてGoogle PageSpeed Insightsで計測してみた。
結果は80点以下…
Should Fix
として報告されているのは、headタグ内にあるJS読み込みが Render Blocking してますよ、という内容のもの。Webの高速化はサーバーサイドだけで済むようなラクなもんじゃない。
AMP
AMPはWeb高速化のベストプラクティスを詰め込んだ仕様/制限のこと。詳しくは下記が参考になる。
またAMPに対応するとページが速くなる他にもおいしいことがあって、GoogleがAMPページをキャッシュしてコンテンツ配信を肩代わりしてくれるのだ。いうなればAMPのためのGoogle無料CDN。これでオーガニック検索のトラフィックはだいぶラクになるかも?
僕も自分の英語TipsブログをAMP化してみたが非常に高速にページが表示できている。(完全にAMP化はできていないのだけど) まだAMP試していない人は、AMPすげーはやいのでぜひその速さを体感してみてほしい。そしてWebの高速化にまっすぐ向き合ってもらいたいと思う。
その他の参考資料
- High Performance Rails (long edition) // Speaker Deck
- Railsパフォーマンス基本のキ // Speaker Deck
- デザイナーやディレクターも知っておきたい、ページ表示速度の高速化の基本 – Rriver
Roppongi.rb イベントについて
- #roppongirb hashtag on Twitter
- イベント発表資料: Roppongi.rb 資料一覧 - connpass
- Roppongi.rb #1 発表の密度が濃くて楽しかったYO! - 酒と泪とRubyとRailsと
ただしRubyバージョン差異による非互換性を解消するための変更は必要だけどね。 ↩