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 似乎从未将内存释放回操作系统。

我知道有一长串与此相关的问题。我什至尝试通过代码更改看起来像这样(特别是最后 3 行) 。即GC.start手动运行结果仍然相同。

while true

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

在 Ruby 版本上对此进行了测试,2.2.2并且2.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

4

1 回答 1

2

我不能告诉你内存泄漏的来源,但我确实发现了一些容易实现的成果。

但首先,有两件事:

  1. 您确定 ActiveRecord 是将数据从一个数据库复制到另一个数据库的正确方法吗?我非常有信心它不是。每个主要的数据库产品都具有强大的导出和导入功能,您将看到的性能将比在 Ruby 中执行的性能好很多很多倍,并且您始终可以从您的应用程序中调用这些工具。在你继续沿着这条路走之前,请仔细考虑一下。

  2. 10,000 这个数字从何而来?您的代码表明您知道一次获取所有记录不是一个好主意,但是 10,000 条记录仍然很多。通过简单地尝试不同的数字,您可能会看到一些收益:比如 100 或 1,000。

也就是说,让我们深入研究这条线在做什么:

users = User.limit(10000).offset(offset).as_json

第一部分,User.limit(10000).offset(offset)创建一个表示您的查询的 ActiveRecord::Relation 对象。当您调用as_json它时,将执行查询,该查询实例化 10,000 个用户模型对象并将它们放入一个数组中,然后从每个用户对象的属性构造一个哈希。(看看ActiveRecord::Relation#as_json 这里的来源。)

换句话说,您实例化了 10,000 个用户对象,只是在您获得了它们的属性后才将它们丢弃。

所以,一个快速的胜利是完全跳过这部分。只需选择原始数据:

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)

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

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

不同之处在于ActiveRecord::Relation#update_all它不从数据库中选择记录或实例化成员对象,它只是更新它。您在上面的评论中说您对name列有唯一约束,因此我们知道这只会更新一条记录。

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

于 2016-04-21T16:54:05.560 回答