Ruby不释放内存

5
我有一些类似于以下代码的Ruby代码:
offset = 0
index = 1

User.establish_connection(..) # db1
class Member < ActiveRecord::Base
  self.table_name = 'users'
end 

Member.establish_connection(..) #db2

while true
  users = User.limit(10000).offset(offset).as_json ## for a Database 1
  offset = limit * index
  index += 1
  users.each do |u|
    member =  Member.find_by(name: u[:name])
    if member.nil?
      Member.create(u)
    elsif member.updated_at < u[:updated_at]   
      member.update_attributes(u)   
    end
  end 
  break if break_condition
end

我看到的是 RSS 内存(htop)不断增长,最终达到了 10GB。我不确定为什么会这样,但 Ruby 似乎从未释放内存给操作系统。
我知道有一长串问题与此相关。我甚至尝试将代码更改为这样(特别是最后三行)。即手动运行GC.start,结果仍然相同。
while true

....
...
...
users = nil
GC.start
break if break_condition
end

我在 Ruby 版本 2.2.22.3.0 上测试过。

编辑:其他细节

1)操作系统。

DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=15.04
DISTRIB_CODENAME=vivid
DISTRIB_DESCRIPTION="Ubuntu 15.04"

2) 安装并编译了ruby,使用rvm工具。

3) ActiveRecord版本为4.2.6


1
什么时候?你是指while吗? - matt
1
可能更好的方式是展示准确的代码?但大致看起来像这样。 - fl00r
@floor 不,名称上有唯一约束。我可以说记录很大,但我希望它在第一次运行(while循环的第一次)时不会占用10GB的内存,内存保持在3234MB(根据htop)。下一次运行时,它再次飙升,并且一直保持这种状态,直到没有更多的内存可用。 - Viren
1
@niceman,没有太多时间测试分析工具,正在生产模式下运行。如果我有时间,我会分享结果的。 - Viren
@joanbm 我能说的是,这些记录很大,但我希望它在第一次运行(while循环)时不会占用10GB的内存。根据htop,内存保持在约3234MB左右。在下一次运行时,内存会急剧增加,并且会一直增加,直到没有更多的内存可用。 - Viren
显示剩余11条评论
1个回答

2
我无法告诉你内存泄漏的来源,但我确实找到了一些简单易行的解决方法。
但首先有两件事情要说:
  1. 你确定ActiveRecord是从一个数据库复制数据到另一个数据库的正确方法吗?我非常自信地认为不是。每个主流数据库产品都拥有强大的导入和导出功能,使用这些工具的性能会比在Ruby中执行时快得多,而且你可以始终从应用程序中调用这些工具。在继续这条路之前,请好好思考。

  2. 数字10000从哪来的?你的代码表明,你知道一次获取全部记录并不是一个好主意,但是10000仍然是很多的记录。通过尝试不同的数字,例如100或者1000,你可能会看到一些收益。

话虽如此,让我们深入探讨一下这一行代码在做什么:
users = User.limit(10000).offset(offset).as_json

首先,User.limit(10000).offset(offset) 创建了一个 ActiveRecord::Relation 对象来表示查询。当调用 as_json 方法时,该查询将被执行,实例化10,000个 User 模型对象并将它们放入数组中,然后从每个 User 对象的属性构建哈希表。(可以在这里查看 ActiveRecord::Relation#as_json 的源代码 源代码。)

换句话说,在你获得它们的属性之后,你只是实例化了10,000个 User 对象然后将它们丢弃。

因此,一个快速的解决方法是完全跳过这一部分。只需选择原始数据:

user_keys = User.attribute_names

until break_condition
  # ...
  users_values = User.limit(10000).offset(offset).pluck(user_keys)

  users_values.each do |vals|
    user_attrs = user_keys.zip(vals).to_h
    member = Member.find_by(name: user_attrs["name"])
    member.update_attributes(user_attrs)  
  end
end

ActiveRecord::Calculations#pluck方法返回每条记录的值组成的数组。在循环user_values.each中,我们将这个值数组转换为一个哈希表。不需要实例化任何用户对象。

现在让我们来看一下这个:

member = Member.find_by(name: user_attrs["name"])
member.update_attributes(user_attrs)

这段代码从数据库中选择一条记录,实例化一个Member对象,然后在每次while循环迭代中更新数据库中的记录——共计10,000次。如果需要在更新记录时运行验证,则这是正确的方法。但是,如果不需要运行验证,你可以通过再次不实例化任何对象来节省时间和内存:

Member.where(name: user_attrs["name"]).update_all(user_attrs)

区别在于 ActiveRecord :: Relation#update_all 不会从数据库中选择记录或实例化Member对象,它只是更新记录。您在上面的评论中说 name 列有唯一限制,所以我们知道这将仅更新单个记录。

进行了这些更改后,您仍然必须应对一个事实,即你必须在每次 while 循环迭代中执行10,000次UPDATE查询。同样,请考虑使用数据库内置的导出和导入功能,而不是尝试让Rails完成此操作。


感谢您的回答。非常抱歉,由于复制到不同的数据库并不是那么直接,因此无法使用pg_import和pg_dump。 - Viren
我已经更新了代码,以展示复制的工作方式。 - Viren
然而,有更好的方法来完成这个任务。你基本上是在使用一个简单的 updated_at 条件进行 upsert 操作。如果数据在同一个数据库中的两个不同表中,你可以使用相同的条件进行 JOIN 操作以获取需要 upsert 的行。由于它们不在同一个数据库中,你可以将其导出并导入到具有不同名称的表中,或者使用 postgres_fdw 直接连接到另一个数据库。 - Jordan Running

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接