なんとなく、Railsアプリケーションの高速化について自分が知っていることを吐き出してみる
思いついたことから雑多に。変なことを言っていそうだったらコメントくれると嬉しいです。
まず、RailsやRubyが遅いせいでユーザ体験に影響がでるというのは、普通のWebアプリケーションであれば基本は無いんじゃないかと思います。GitHubもShopifyもRailsです。まず自分のコードや構成を疑うのが大体の場合正しいと思います。
計測して、必要になるまではやらない
時間は貴重なので、不必要な最適化をしないというのが結構重要だと思います。
当て推量で無駄なことをしてしまったり、最適化するの自体が楽しくなってしまって時間を使ってしまいがちです。
その上では
- 目標を立てて十分早ければそれでよいと考える
- 例えばクックパッドでは(平均?)レスポンスタイム200ms以下を目標にしているようです
- 計測する
が重要かと思います。
計測するには、とりあえずはありものの道具を使いましょう。
- サーバサイドであれば
- 開発環境にはrack-mini-profiler を入れましょう
- 本番ではAPM を使いましょう(僕はNew Relicを使っています)
- クライアントサイドではLighthouse で問題点と改善策がみれます
基本サーバサイドの改善の方が全ユーザに影響があるのでコスパがいいと思います。
クライアントサイドはユーザによって環境が大きく違ったり、指摘されているところが広告のせいだったりということが起きがちです。
APMを使っていれば大体以下みたいなことができる情報は提供されている思います。
- 平均レスポンス時間と、何が寄与しているのか
- レスポンス時間のパーセンタイル値を見て、分布が妙なことになっていないか
をみて全体の状況を把握。
何か問題があった場合は、どのactionのどの部分に問題があるのかを細かくみる。
- 最も時間を使っている順や、平均レスポンス時間が遅い順で問題がありそうな部分を効率よく探す
- その中でもどの部分で時間を使っているのか、DBなのか、Viewなのかということを見ていく
意外と雑に書けなくて大変
RDB周り
遅い時に大体ミスってるのはこの辺な気がします。
N+1クエリがあったらさっさとやっつけましょう
- rack-mini-profilerを使っていれば開発中でも気付きやすいです。使いたければbulletもあります。
- 本番に入れてしまってもAPM使っているとすぐわかると思います。
- 対処はActiveRecordのjoinsとpreloadとincludesとeager_loadの違い - Qiita
適切にインデックスを使いましょう
- インデックスを使えばいいと言われましてもどうやって?と思うならpgheroを入れてとりあえず提案にしたがっておきましょう
- インデックスについて理解したいならUSE THE INDEX, LUKE!がオススメです
- 全然必須では無いですが、アルゴリズムとデータ構造の初歩的な知識があると、なんとなくお気持ちがわかった気になれます。
- 興味があるならAlgorithms, Part I | Coursera、Part IIがオススメです
その他
-
select
で必要なカラムだけをとるようにしたり -
pluck
を必要なカラムだけかつ不要なActiveRecordのオブジェクト生成をさせなくしたり - Redisとかでデータ構造使って工夫できないか考えたり
- カウンターキャッシュを使ったり
- RedisみたいなインメモリのDB上にキャッシュすることで工夫できないか考えたり
Railsアプリケーションの高速化とはちょっとずれますが、サービスが成長してデータ量が大きくなってくるとダウンタイムなしにマイグレーションするのも結構大変になってきたりします。
そういうときはstrong_migrationsがオススメです。
Railsを使っていなくても、READMEを見ておくだけで助かることが結構多いんじゃ無いかと思います。
キャッシュについて
一口にキャッシュといっても、どのレイヤでキャッシュするのかで、めちゃくちゃバリエーションがあります。
基本的にやってることは、「余分に記憶するものが必要になる代わりに時間がかかる処理をスキップ 」です。
時間がかかる というのは相対的で、
Latency Numbers Every Programmer Should Know
を知っているべきとのことなんですが、
- インターネットを通って遠いところで往復するのはめちゃくちゃ遅い
- ディスクからの読み込みはメモリからの読み込みよりかなり遅い
ぐらいをなんとなく把握していると、大きな考え違いをしにくくなる気がします。
Railsでのキャッシュ
Caching with Rails: An Overview — Ruby on Rails Guidesに網羅されています。
- ディスクからの読み込みor長時間の計算が必要なところをメモリ上に置く
-
ActiveSupport::Cache::Store
を使いましょう - テンプレート周りが遅いならフラグメントキャッシュを使いましょう
-
- HTTPのキャッシュ
-
ETag
やCache-Control
ヘッダを理解する
-
個人的にはファイルシステムのキャッシュとか、RDBMS内でのメモリへのキャッシュとか、正直どうなっているのかわからないレイヤも結構あるので、考え過ぎるよりやってみて結果を見るほうが効率がいいこともあったりする気がします。
リクエスト内で時間のかかる処理をしない
最近はWebサーバには大体pumaを使うのかなと思います。
pumaはHTTPリクエストを処理できるワーカーをスレッドとして事前に用意しておく作りになっています。
configでスレッド数を動的変化させるようにもできますが、上限はありますし、増やすほどメモリを食っていきます。
1秒あたりにどれだけのリクエストを捌けるのか、試しに概算してみましょう。
もし、N=10ワーカーで、レスポンス時間が常にL=200msだとすると、
になります。
ある actionだけ遅くてレスポンス時間が1secだとしましょう。ある1秒のはじめにそのactionに一気に5リクエスト来たとします。するとその1秒間では5ワーカーはそのリクエストで手一杯で、残り5ワーカーで処理することになります。
と最大スループットが40%も落ちてしまいました。
つまり遅いactionへのリクエストがあると長時間ワーカーを占有されて最大スループットが一気に小さくなってしまうということです。また、そのリクエストだけを見てもユーザへのフィードバックも遅くなります(クライアント側で工夫していない限り)。
時間がかかる処理はリクエスト内で処理せず、Sidekiqやそれに類するものを使ってバックグラウンドジョブに逃しましょう。
遅くなりがちなところとしては
- 外部のWeb APIを使う処理
- 大量のデータを操作する処理
とかがあるかなと思います。
メモリ使用量に気をつける
アプリケーションがメモリとして使おうとしているもの( 仮想メモリ)に対して、物理的なメモリがいっぱいいっぱいになると、 OSは一部をディスクに読み書きしはじめます。ディスクの読み書きはメモリの読み書きよりかなり遅いのでしたから、これが高頻度に発生するようになると(スラッシング)一気にパフォーマンスが悪くなります。
メモリ使用量はAPMで見れるものもあると思います。New RelicであればRuby VMsのところで見れます。
- 長期的に見て上昇していないか→メモリリークしてそう
- スパイクがないか→一部の処理で大量のメモリを確保してそう
- 時間の幅を広く見てると、ならされて見れなかったりする
基本的に、メモリ使用量を含めたインフラの監視もしましょう。自分はnode_exporterを使っていますが、New Relic InfrastructureやらDatadogやらMackerelやら色々あります。使っているホスティングサービスに紐づいているものもあるかもしれません。
その他、発展的おすすめ
- ISUCONの過去問やブログ
-
Webフロントエンド ハイパフォーマンス チューニング
- フロントエンドのパフォーマンスに関する知識を網羅的に仕入れるのに良かったです。
-
Grokking the System Design Interview
- 難しいけどSystem Design Interviewを受ける気がなくてもオススメです
- Numbers Every Programmer Should Know + アルゴリズムとデータ構造 + αを使ってこうやって算数するのかというのがわかって面白いです(題材の規模がデカ過ぎるけど)