如何处理 Ruby/Rails 中的内存泄漏

How to deal with memory leak in Ruby/Rails

我正在开发一个 Rails 应用程序来处理大量的 数据并停止,因为它使用了我计算机的所有内存,原因是 内存泄漏(未释放的已分配对象)。

在我的应用程序中,数据以分层方式组织,就像一棵树, 其中级别“X”的每个节点包含级别数据的总和 “X+1”。例如,如果级别“X+1”的数据包含数量 城市中的人口,“X”层包含城市中的人口数量 状态。这样,层级“X”的数据就是通过对 “X+1”级别的数据量(在本例中为人员)。

为了这个问题,考虑一棵有四层的树: 国家、州、城市和社区,每个级别都已映射 进入 Activerecords tables(国家、州、城市、社区)。

从填充树叶的csv文件中读取数据,即, 社区 table.

之后,数据按以下顺序从底部(社区)流向顶部(国家):

1) Neighbourhoods data is summed to Cities;
2) after step 1 is completed, Cities  data is summed to States;
3) after step 2 is completed, States  data is summed to Country;

我使用的原理图代码如下:

1 cities = City.all
2 cities.each do |city|
3   city.data = 0
4   city.neighbourhoods.each do |neighbourhood|
5       city.data = city.data + neighbourhood.data
6   end
7   city.save
8 end

树的最低层包含 380 万条记录。每次线 执行2-8,汇总一个城市,执行第8行后, 该子树不再需要,但它永远不会被释放(内存 泄露)。总结了50%的城市后,我所有的8Gbytes RAM 消失。

我的问题是我能做什么。买更好的硬件不会 因为我正在使用“小”原型。

我知道一个让它工作的方法:为每个城市重新启动应用程序, 但我希望有人有更好的主意。 “最简单的”是强制 垃圾收集器释放特定​​对象,但似乎不是一种方法 去做吧 (https://www.ruby-forum.com/t/how-do-i-force-ruby-to-release-memory/195515).

从以下文章中我了解到开发人员应该 以“建议”垃圾收集器什么的方式组织数据 应该被释放。也许另一种方法可以解决问题,但唯一的 我看到的替代方法是深度优先搜索方法而不是 我正在使用反向广度优先搜索,但我不明白为什么它应该起作用。

到目前为止我读到的内容:

https://stackify.com/how-does-ruby-garbage-collection-work-a-simple-tutorial/

https://www.toptal.com/ruby/hunting-ruby-memory-issues

https://scoutapm.com/blog/ruby-garbage-collection

https://scoutapm.com/blog/manage-ruby-memory-usage

谢谢

这并不是真正的内存泄漏。您只是不加掩饰地从 table 加载数据,这会耗尽可用内存。

解决办法是从数据库中加载数据in batches:

City.find_each do |city|
  city.update(data: city.neighbourhoods.sum(&:data))
end

如果 neighbourhoods.data 是一个简单的整数,您不需要首先获取记录:

City.update_all(
  'data = (SELECT SUM(neighbourhoods.data) FROM neighbourhoods WHERE neighbourhoods.city_id = cities.id)'
)

这将快一个数量级,并且内存消耗很小,因为所有工作都在数据库中完成。

如果您真的想将一堆记录加载到 rails 中,请确保 select 聚合而不是实例化所有这些嵌套记录:

City.left_joins(:neighbourhoods)
    .group(:id)
    .select(:id, 'SUM(neighbourhoods.data) AS n_data')
    .find_each { |c| city.update(data: n_data) }

根据您的模型关联设置方式,应该能够利用预加载。

例如:

class City < ApplicationRecord
  has_many :neighborhoods

class Neighborhood < ApplicationRecord
  belongs_to :city
  belongs_to :state

class State < ApplicationRecord
  belongs_to :country
  has_many :neighborhoods

class Country < ApplicationRecord
  has_many :states


cities = City.all.includes(neighborhoods: { state: :country })
cities.each do |city|
  ...
end

你根本不需要 rails,纯 SQL 应该足以完成你正在尝试的事情:

City.connection.execute(<<-SQL.squish)
  UPDATE cities SET cities.data = (
    SELECT SUM("neighbourhoods.data")
    FROM neighbourhoods
    WHERE neighbourhoods.city_id = cities.id
  ) 
  SQL