ActiveRecordがメモリを食って解放しない問題

More than 3 years have passed since last update.

Railsコンソルで以下のように確認しましょう

[1] pry(main)> ObjectSpace.each_object(ActiveRecord::Base).count

=> 0
[2] pry(main)> User.limit(100)
User Load (1.5ms) SELECT `users`.* FROM `users` LIMIT 10
[4] pry(main)> ObjectSpace.each_object(ActiveRecord::Base).count
=> 100
[5] pry(main)> User.limit(100)
User Load (0.3ms) SELECT `users`.* FROM `users` LIMIT 10
[7] pry(main)> ObjectSpace.each_object(ActiveRecord::Base).count
=> 200

作ったActiveRecordのインスタンスがどんどんメモリ上に溜まると分かりました。

溜まったインスタンスが使わなくても、時間をかけて解放されないです。そのため、プロセスのメモリ使用量がどんどん増加てしまいます。多量なデータを処理する場合、メモリ足りない問題が出るおそれがありますね。

実にみんなさんがご存知ですが、この問題はunicornというサーバーでよくありました。時間をかけてunicornのプロセスのメモリ使用量が増加するという問題です。この問題を解決するために、unicorn killerというgemを使ってunicornプロセスを再起度して、メモリを解放する工夫をよくやっていると思います。

なぜRubyのGCはActiveRecordオブジェクトのメモリを自動的に解放しないか、まだ分からないですが、対策として以下のように幾つか方法があります。


1. 必要ない場合、includesを使用しない

includesを使ったら、パフォーマンスが上がるが、紐付けるテーブルのレコードもロードして、メモリを大量に使ってしまいます。パフォーマンスとメモリ量を検討する必要があります。


2. 使用しないレコードの属性をロードしないようにする

属性が多ければ多くほどActiveRecordオブジェクトが使うメモリ量が大きいです。

処理に必要な属性のみをselectしてください。

 User.select(:id, :name).limit(100)


3. 手動でメモリ解放する

ObjectSpace.each_object(ActiveRecord::Relation).each(&:reset)

GC.start

メモリを解放する処理は少なくても時間がかかるので、この処理を追加したら、パフォーマンスが落ちる可能性があります。メモリ量とパフォーマンスを考慮する必要があると思います。