RedisキャッシュとRackミドルウェアでパフォーマンスを改善(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社エンジニアの「?」を「!」に。原著者の許諾を得て翻訳・公開いたします。RedisキャッシュとRackミドルウェアでパフォーマンスを改善(翻訳)パレートの法則によると(少々誇張あり)、Railsアプリにおけるパフォーマンス問題の95%は、エンドポイントのわずか5%で発生していることになります。本記事では、シンプルなRedisキャッシュ技法とカスタムRackミドルウェアを用いて、私のRailsアプリでエンドポイントのボトルネックを500%以上改善したときの方法について解説いたします。i7)でSiegeを使いました。productionのデータベースをコピーしたRailsアプリをproductionモードでローカル実行し、Pumaサーバーをワーカー2つ(各16スレッド)の状態でベンチマークを実施しました。Siegeの設定は次のようにしました。--concurrent=20達成できた改善結果を知りたい方はこの先をお読みください。パフォーマンス最適化前のRails最適化作業を開始する前の詳細なパフォーマンスベンチマークは次のとおりです。0.03このエンドポイントは、ReactフロントエンドとiOSモバイルクライアント起動時の両方でランディングページとして使われているので、ここを最適化によさそうな候補として決定しました。どのユーザーがリクエストする場合でもデータが同じだったので、あるバージョンのデータをキャッシュしてこれを全ユーザーに見せることができそうでした。1つ注意すべき点としては、このエンドポイントがオプションパラメータとしてdiscounted_byを受け取ることです。このパラメータの型は連続値(0.0〜100.0のすべての値が有効)なので、使われる可能性のあるクエリをすべてキャッシュするのは不可能です。(categoryのような)不連続型のパラメータを1つだけクエリで受け取るのであれば、使われる可能性のある結果をすべてキャッシュしておくことを検討してもよいでしょう。最終的に、クエリの実行結果をパラメータなしでキャッシュしておいて、リクエストにdiscounted_by値を含んでいないクライアントにはキャッシュ版を提供することにしました。Relicやpgheroで調査を始めるのがよいでしょう。Recordの遅いクエリをRedisでキャッシュするデータベースレベルの最適化には限度があります。データセットが肥大化して、ビジネスロジックの要請上、joinされたいくつものテーブルからデータをフェッチせざるを得ない状況では、キャッシュに頼らずにSQLデータベースで望みのパフォーマンスを達成するのは難しいかもしれません。プロジェクトでSidekiqを使っていれば、既にそこにRedisデータベースがあります。既存のインフラをそのまま使えれば、新たな依存関係(Memcachedなど)を追加するよりずっとシンプルになります。RedisはわかりやすいキーバリューストレージAPIを提供していますので、キャッシュのために特別なgemを追加する必要はありません。Goをお使いかと思います。その場合、Redisへのダイレクトアクセスを有効にする作業はファイルを1個追加するシンプルな作業だけで住みます。Cronを使っています。"CacheUpdaterJob"キャッシュを30分おきに更新する設定は、私のアプリ用です。クライアントになるべく新しいデータを提供する必要がある場合は、キャッシュを数秒おきに更新するだけでも、リクエストのたびにデータベースからデータをフェッチするよりパフォーマンスが向上します。discounted_byパラメータを含まないキャッシュ済みリクエストを返すRailsコントローラは次のようになります。endend修正後のバージョンは元のバージョンより最大で5倍高速化します。Recordオブジェクトのインスタンス化が不要になるためメモリ使用量も削減されます。データのサイズにもよりますが、JSONシリアライザ自身もパフォーマンス上の重大なオーバーヘッドになっている可能性があります。詳しい方法については、私のRailsのメモリ使用量を削減する方法に関する記事をご覧ください。RailsのRackミドルウェアを最適化するサーバーへの各リクエストは、以下のRailsミドルウェアをすべて通過してからアプリのコードに到達します。Rack::ETagデフォルトスタックを迂回して、カスタムRackミドルウェアからレスポンスを直接クライアントに返すことで、数msほど削ることができます。これを行うには、以下のRackアプリを追加します。endendこのRackアプリをミドルウェアスタックの冒頭に挿入するため、Railsを次のように設定します。CacheMiddlewareこれにより、リクエストの宛先が最適化済みエンドポイントであると考えられるかどうかと、オプションパラメータの値が存在しないかどうかがチェックされます。これに該当する場合、キャッシュ済みJSONをクライアントに返し、そうでない場合はリクエストをスタック上の次のミドルウェアに渡します。この手法は少々極端なので、使うのは非常に大きい負荷が発生している場合だけにしておきましょう。cookie、セッション管理、ログ出力といったRackが提供する機能のほとんどが無効になってしまいます。いずれにしろ、皆さんのユースケースによってはこの方法で数msを節約すれば、気になるホスティング料金の節約につながるかもしれません。カスタムミドルウェアを使った場合の詳細なベンチマーク結果を以下に示します。「キャッシュのみ」のバージョンと比べて、最大で20%の改善が見られます。0.00最後に本記事では簡単のため、キャッシュの自動無効化やRails組み込みのキャッシュサポートといった高度な手法については扱いませんでした。gemや公式のRailsガイドをご覧ください。本記事のベンチマークはローカルで実行されたものであり、ネットワークのオーバーヘッドは含まれていませんので、ご了承ください。500%の改善は、私がアプリ固有のコードで達成したものです。コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。enno.jpを運営。実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。Storageを使ってはいけない(翻訳)#Tags#会社紹介#自社サービス#勉強会#Rails#PHP#デザイン#実績#週刊RailsウオッチCONTACTお気軽にご相談ください。(翻訳)CONTACTTechRachoでは、パートナーシップをご検討いただける方からのご連絡をお待ちしております。お気軽にご意見・ご相談ください。