Rails小手先パフォーマンス改善テクニック

この記事は公開されてから1年以上経過しており、最新の内容に追従できていない可能性があります。

地道な改善も必要

ここで書いたけれど、一撃で超改善というのはなかなか難しく、一旦現状を打破するために小手先のやりくりでなんとかしたいときもある。実際に小手先のやりくりをいろいろしたのでその記録。

基本は愚直にやるしかない

  • ボトルネックを探して、そこを改善していく
  • なので、まずは計測が必要。改善したあともどういう変化が見られたかを確認していく
    • 1日の中で処理する量は変化するので、1日様子を見てからの判断でも遅くはない
  • あるWebAPIなエンドポイントのレイテンシが高くてなんとかしたい、みたいな場合、その処理が単純なDBクエリして結果を整形して返す、であればまあまあわかりやすいが、現実の問題でそういうケースはあまりない

ちりつもである

  • というわけで、どうみてもわかるような大きなボトルネックの改善だけでなく、小さなボトルネックの改善も積み重ねていくしかない
  • たとえば1つのクエリ、1つの処理が1msだとしても、でループして何度もするなら積み重なる
    • いろいろ待ちもあるので10回読んだら10msということはなく11msとか12msとかになる。ワーストケース、p99とかで見るともっといく可能性もある
    • 単純なN+1の場合もあれば、1ページずつ読む、ある一定期間を順番に計算する必要がある、などのケースでもこうなる

インデックスをつけたりクエリを見直したり

ActiveRecord周りに多くのちりつも改善チャンスが眠っている

exists? / present? を使いわける

  • exists? はActiveRecordに対してやると SELECT 1 が発行される、これはすでにクエリ済みの結果でも変わらない
  • present? は結果を取得して、その結果に対して配列として存在確認がされる
  • ActiveRecordの結果を使うことがあるなら present? をして、配列として扱ったほうがよい
  • 逆にクエリ結果がいらないなら present? ではなく exists? にしたほうがいい
  • empty? もそう。

Arrayのfilterやfindを使う

  • books.map {|book| book.categories.where(type: TYPE_MANGE) } だとN+1になる。これを回避するためにmapの中で filterfind などを使う
    • books.map {|book| book.categories.filter {|category| category.type == TYPE_MANGE } }
  • さらにこのままだと book.categories が毎回読み込まれるので、プリロードも必要
    • books = books.preload(:categories)
  • 各bookのcategoriesがメモリ上に読み込まれた状態になり、そこから探すようになり、MySQLへのクエリを行わなくなる
  • ちなみにfilterの別名である .select() とするとActiveRecordでSELECTが呼ばれることに注意。もしselectを使いたいならブロックにする .select { ... }

ちゃんとプリロードする

  • scopeを使っているとプリロードできないので、エイリアスしたアソシエーションか、Preloaderを使うか、自分で実装する必要がある
  • if association(:author).loaded? で読み込み済みかを判断できるので、これを使って処理の仕方を変えられる

必要なカラムだけ取る

  • 極端だけど Book.all.map(:id) じゃなくて Books.pluck(:id) とか
    • TEXTが無制限に入るフィールドとかも一緒に取るとまあまま時間がかかる
    • 絞ればカバリングインデックスで取れる可能性も出てくる
      • Books.select(:name).where(type: "foo") みたいなもの

instantiateのコストは低くない

  • Book.find_by(id: 1) をすると SELECT * FROM books where id = 1 というクエリが出て、Bookインスタンスを作る
  • このインスタンス化の部分のコストが実は低くはなく、大量のレコードをインスタンス化するとまあまあ時間がかかる
  • 生クエリを書けば当然インスタンス化はないが大変。けれども to_sql で実は生クエリに落とせる
  • 複数のテーブルからまとめてデータを取るのもこれで行ける
    • 結構アクロバティックな対応になるので、本当に遅くて困っているような部分で検討するとよさそう

Jbuilderは遅いのか?

リクエストスコープベースのインメモリキャッシュ

並列処理

  • 外部の遅いAPIを利用する場合だとかで並列に処理しても問題ないときは並列にすればその分速くなる
  • 集計のために多数の計算が必要だったりする箇所も、並列にできるなら速くなる可能性が高い
  • CPUとメモリをより多く使うことになるのでその部分の注意が必要

Rails.cacheを見直す

  • 基本は1レコード1キャッシュのような形にすると、キャッシュヒット率が高くてよい
    • キャッシュ参照がN+1になる場合もあるので、全件キャッシュしたり、プリロードしたり適宜判断したい
    • キャッシュN+1はDBのN+1より圧倒的に早いけれど通信もあるしインメモリで済ませるよりは遅い
  • ちなみにActiveRecordをそのままキャッシュすると、クラスがリロードされたりした際にキャッシュから復元できなくなるので、あまり良くない
    • Productionリリース時にキャッシュを一度クリアしていると問題はない
    • それが難しいほどの大きなサービスだと気をつける必要がある
  • ActiveRecordのafter_save/after_deleteを使って、データ更新と同時にキャッシュを更新することができる
    • データ操作があまりにも多いようだとキャッシュの恩恵も大きくなさそうなので、増える複雑さとのバトルになる
    • 一切変わらないマスター的なデータはキャッシュを使う必要もなくて、Rails起動時にインメモリに置く、でもいい

サイト案内

運営してるひと: @sters9

最近は Go, Ruby, Rails, Kubernetes, GCP, Datadog あたりをしていますがもっといろいろやりたい!

サイト案内

開発環境の紹介

プライバシーポリシー

tools.gomiba.co

サイト内検索

アーカイブ

2024/12 (1) 2024/09 (3) 2024/07 (1) 2024/06 (3) 2024/05 (1) 2024/04 (7) 2024/03 (4) 2024/01 (3)

2023/12 (1) 2023/11 (3) 2023/10 (1) 2023/09 (1) 2023/08 (2) 2023/05 (4) 2023/04 (4) 2023/03 (4) 2023/02 (2) 2023/01 (1)

2022/12 (2) 2022/11 (4) 2022/10 (3) 2022/09 (2) 2022/08 (4) 2022/07 (5) 2022/06 (4) 2022/05 (9) 2022/04 (8) 2022/03 (10) 2022/02 (21) 2022/01 (8)

2021/12 (11) 2021/11 (1) 2021/10 (4) 2021/09 (2) 2021/08 (1) 2021/07 (2) 2021/06 (5) 2021/05 (10) 2021/04 (1) 2021/03 (8) 2021/02 (12) 2021/01 (8)

2020/05 (2) 2020/04 (2) 2020/02 (2) 2020/01 (1)

2019/12 (3) 2019/11 (2) 2019/10 (5) 2019/09 (3) 2019/07 (6) 2019/06 (4) 2019/04 (3) 2019/01 (2)

2018/12 (6) 2018/10 (4) 2018/09 (6) 2018/08 (7) 2018/07 (16) 2018/06 (7) 2018/05 (7) 2018/04 (5) 2018/03 (3) 2018/02 (10) 2018/01 (6)

2017/12 (8) 2017/11 (6) 2017/10 (10) 2017/09 (12) 2017/08 (12) 2017/07 (3) 2017/06 (1) 2017/01 (4)

2016/12 (5) 2016/10 (3) 2016/09 (1) 2016/07 (2) 2016/06 (1) 2016/04 (1) 2016/02 (1) 2016/01 (2)

2015/12 (1) 2015/10 (1) 2015/09 (3) 2015/06 (1) 2015/01 (1)

2014/08 (2) 2014/07 (3) 2014/05 (1) 2014/01 (7)

2013/12 (2) 2013/11 (4) 2013/10 (1) 2013/09 (1) 2013/08 (3) 2013/07 (4) 2013/06 (5) 2013/05 (2) 2013/04 (7) 2013/03 (1)