こちらは「Ruby on Rails パフォーマンスアポクリファ」のサンプル章です。日本語での完全版が現在Gumroadで入手可能です。
Rails のバックグラウンドジョブの多くには、一般にパフォーマンスの問 題があります。
バックグラウンドジョブでは、しばしば大きなデータセットを処理しま す。そして、その際に User.all.each(&:send_daily_newsletter) のよ うな愚かなことをしてしまいます。
開発環境やテスト環境では、User.all が返すのは、おそらく数行、多くて も十数行でしょう。開発者の多くは、自分のローカルマシン上の seed デー タを極端に限定しています。
しかし、プロダクション環境では、User.all はおそらくかなりの数の行 を返します。開発するアプリケーションによっては、数十万行になるかもし れません。
10 万行を返す結果セットには、ほんのわずかばかり問題があります。それ は SQL クエリーが返すのに時間がかかるというだけではありません。Ruby アプリケーションにも取り返しのつかない影響を及ぼしてしまいます。
Active Record の大規模な結果セットは、メモリ使用量を増大させる可能 性があります。Ruby のメモリアロケーターは入り組んでいて、Ruby のプ ロセスが大量のメモリを使うと、ガベージコレクションされても OS に返さ ない傾向があります。これは Ruby のせいではなく、アロケーターのせいで す。今後数年のうちに Ruby 側で修正されるかもしれませんが、今のところ は対処せざるを得ません。
このような動作は、しばしば見落とされ、実運用で大失敗する可能性があ ります。さらに悪いことに、ほとんどの人はバックグラウンドジョブで何が 起こっているか、それが動作する限りはあまり気にしません。しかしなが ら、キューが一杯になったり、メモリが足りなくなったりすると文句を言う のです。
大量の結果セットを取得する傾向は、バックグラウンドジョブが遅くなっ たりメモリを大量に消費したりする理由の 1 つに過ぎないものの、よくある ことです。しかし Rails 5.0 以降では、この現象が発生したときに識別しや すくする設定項目が用意されています。その名も warn_on_records_fetc hed_greater_than です。
この設定について最初に書いたとき、GitHub を調べてみたら、この設定 を使っていたアプリケーションはたった 1 つだけでした。
これに整数に設定すると、ActiveRecord::Relation オブジェクトがそ の整数より大きな結果セットを返した場合に、ログに警告を出力します。
この設定は、大きな結果セットを取得する場合は ActiveRecord::Batch es の find_each などのメソッドを使用したほうがよいという注意喚起のた めのものです。
次のコードは、
User.all.each(&:send_daily_newsletter)
代わりに次のように書けます。
User.find_in_batches do |group| group.each(&:send_daily_newsletter)
end
今すぐ config/environments/development.rb にアクセスしてこの警 告を入れるのが合理的だと思います。1500 より大きい結果セットは少な くとも 2 つの完全なバッチで取得されるので、1500 あたりが妥当な値で しょう。
config.active_record.warn_on_records_fetched_greater_than = 1500
すると、その上限を超えると、ログにこのような警告が表示されます。
Query fetched 1501 User records:
SELECT "users". FROM "users"
ORDER BY "users"."id" ASC LIMIT $1
さて、これを application.rb あるいは環境別ファイル production.rb で有効にするかは迷うところです。この設定を行うと ActiveSupport::No tifications.subscribe("sql.active_record") がされます。つまり、 SQL クエリーごとの notification で購読されることになります。ローカル ベンチマークでは、クエリーあたりのオーバーヘッドは非常に小さくなって いますが、「Active Record のリクエストごとにオーバーヘッドを追加する」 というのは、必ずしもやりたいことではありません。
development モードでは常にオンにしつつ、バックグラウンドジョブを実 行するときだけ production モードでもオンにするのが便利でしょう。たと えば、config/initializers に以下のように記述します。
if Sidekiq.server? ActiveRecord::Base.
warn_on_records_fetched_greater_than = 1500
end
一度に読み込むレコード数には注意が必要です。