この記事はユニファアドベントカレンダーの24日目の記事です!
adventar.org
いつか大きめのバッチ処理を書く必要があるかもしれないので以前から興味があった ActiveRecord::Batches#find_each
を使わないで Array#each
を使ってしまったら実際にどれだけメモリを食うのか手元のマシンで検証してみた。
実験
今回は取り敢えず 適当に作った127万ユーザーにメールを一斉送信するような処理を作って比較してみた。
Array#each
require 'objspace' namespace :memory_profile do desc 'Array#each' task each: :environment do puts '---------- Array#each ----------' puts "#{User.count} users" puts "Before: #{memory_usage} MB" User.all.each do |user| NewsMailer.deliver(user.email) end puts "After: #{memory_usage} MB" puts "User: #{memory_usage(User)} MB" end end class NewsMailer def self.deliver(_email) = nil end def memory_usage(klass = nil) ObjectSpace.garbage_collect bytes = if klass ObjectSpace.memsize_of_all(klass) else ObjectSpace.memsize_of_all end (bytes / 1024.0 / 1024.0).round(2) end
± bin/rake memory_profile:each ---------- Array#each ---------- 1278681 users Before: 14.86 MB After: 1662.31 MB User: 0.0 MB
Array#each
を利用すると10,000レコード当たり13MB程度のメモリ使用量ということがわかった。想像していたよりメモリ使わないのね。
ActiveRecord::Batches#find_each
namespace :memory_profile do desc 'ActiveRecord::Batches#find_each' task find_each: :environment do puts '---------- ActiveRecord::Batches#find_each ----------' puts "#{User.count} users" puts "Before: #{memory_usage} MB" User.find_each do |user| NewsMailer.deliver(user.email) end puts "After: #{memory_usage} MB" puts "User: #{memory_usage(User)} MB" end end
± bin/rake memory_profile:find_each ---------- ActiveRecord::Batches#find_each ---------- 1278681 users Before: 14.86 MB After: 15.85 MB User: 0.21 MB
ActiveRecord::Batches#find_each
は1,000件ずつDBからレコードを取得し各レコードを1つのブロックに yield してるので全然メモリ使わないのがよく分かる。(当然DBには1,279回のクエリを発行しています)
オマケ
desc 'Array#each with includes' task each_with_includes: :environment do puts '---------- Array#each (includes)----------' puts "#{User.count} users" puts "Before: #{memory_usage} MB" User.joins({ blogs: { posts: :tags } }) .includes({ blogs: { posts: :tags } }) .where(tags: { name: %w(Ruby Glisp) }) .each do |user| NewsMailer.deliver(user.email) end puts "After: #{memory_usage} MB" puts "User: #{memory_usage(User)} MB" puts "Blog: #{memory_usage(Blog)} MB" puts "Post: #{memory_usage(Post)} MB" end
± bin/rake memory_profile:each_with_includes ---------- Array#each (includes)---------- 1278681 users Before: 14.87 MB After: 2760.97 MB User: 77.51 MB Blog: 86.42 MB Post: 97.66 MB
Tag
で Ruby と Glisp についてのブログ記事を書いている32万ユーザーに絞り込んでみた。無駄に includes
を利用して N+1 問題に気を使ったつもりだが不要な Eager Loading は複数の巨大な関連付けモデルの配列を作ってしまい大量のメモリを消費してしまうのが見て取れる。
まとめ
潤沢にメモリが乗っている Worker インスタンスが使える場合やコレクションの大きさが数万程度だったりどうしてもDBへのリクエスト数を制限したいとかじゃない限りは普通に ActiveRecord::Batches#find_each
を使いましょう。
ユニファでは、一緒に働く仲間を募集中です!