首页 > 解决方案 > 如何处理 Ruby/Rails 中的内存泄漏

问题描述

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

在我的应用程序中,数据以分层方式组织为一棵树,其中“X”级的每个节点都包含“X+1”级数据的总和。例如,如果级别“X+1”的数据包含城市的人口数量,则级别“X”的数据包含州的人口数量。这样,“X”级的数据就是“X+1”级(这里是人)的数据量相加得到的。

为了这个问题,考虑一个具有四个级别的树:国家、州、城市和社区,并且每个级别都映射到 Activerecords 表中(国家、州、城市、社区)。

数据是从填充树的叶子的 csv 文件中读取的,即邻域表。

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

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% 的城市后,我所有的 8GB RAM 都消失了。

我的问题是我能做什么。由于我正在使用“小型”原型,因此无法购买更好的硬件。

我知道一种使它起作用的方法:为每个城市重新启动应用程序,但我希望有人有更好的主意。“最简单”的方法是强制垃圾收集器释放特定​​对象,但似乎不是一种方法(https://www.ruby-forum.com/t/how-do-i-force-ruby-释放内存/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

谢谢

标签: ruby-on-railsrubymemory-leaks

解决方案


这并不是真正的内存泄漏情况。您只是不加说明地从表中加载数据,这将耗尽可用内存。

解决方案是批量加载数据库中的数据:

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 中,请确保选择聚合而不是实例化所有这些嵌套记录:

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

推荐阅读