如何使用ruby加速处理大型CSV

时间:2011-03-24 22:54:04

标签: ruby-on-rails ruby multithreading performance

对于一个项目,我需要解析一些非常大的CSV文件。某些条目的内容存储在MySQL数据库中。我试图使用多线程来提高速度,但到目前为止,这只能减慢速度。

我解析一个CSV文件(最多10GB),其中一些记录(20M +记录CSV中的aprox.5M)需要插入MySQL数据库。要确定需要插入哪条记录,我们使用Redis服务器,其中的集合包含正确的id / references。

由于我们在任何给定时间处理大约30个这样的文件,并且存在一些依赖关系,我们将每个文件存储在Resque队列中,并让多个服务器处理这些(优先级)队列。

简而言之:

class Worker
  def self.perform(file)
    CsvParser.each(file) do |line|
      next unless check_line_with_redis(line)
      a = ObjectA.find_or_initialize_by_reference(line[:reference])
      a.object_bs.destroy_all
      a.update_attributes(line)
    end
  end

这适用于水平缩放(更多CSV文件=更多服务器),但较大的CSV文件会造成问题。我们目前有这样的文件需要花费超过75小时来解析。我已经想到了许多优化:

一个是减少MySQL查询;我们实例化AR对象,而使用纯SQL的插入,如果我们知道对象Id,则要快得多。通过这种方式,我们可以摆脱大部分AR,甚至Rails也可以通过这种方式消除开销。我们不能使用普通的MySQL加载数据,因为我们必须将CSV记录映射到可能具有不同ID的其他实体(我们将十几个旧数据库组合到一个新数据库中)。

另一个是试图在同一时间做更多事情。有一些IO等待时间,Redis和MySQL的网络等待时间,甚至在MRI使用绿色线程时,这可能允许我们在IO读取等的同时安排我们的MySQL查询。但是使用以下代码:

class Worker
  def self.perform(file)
    CsvParser.each(file) do |line|
      next unless check_line_with_redis(line)
      create_or_join_thread(line) do |myLine|
        a = ObjectA.find_or_initialize_by_reference(myLine[:reference])
        a.object_bs.destroy_all
        a.update_attributes(myLine)
      end
    end

    def self.create_or_join_thread(line)
      @thread.join if @thread.present?
      @thread = Thread.new(line) do |myLine|
        yield myLine
      end
    end
  end

这会慢慢减慢过程。当我ps au时,它以100%CPU开始,但随着时间的推移,它下降到仅2-3%。在那一刻它根本不插入新记录,它似乎挂起。

我有strace这个过程,起初我看到MySQL查询经过,一段时间后它似乎根本没有执行我的ruby代码。可能是一个死锁(它在解析CSV的 last 行后挂起,但是进程继续以5%的CPU运行并且没有退出),或者我在这里阅读的内容:http://timetobleed.com/ruby-threading-bugfix-small-fix-goes-a-long-way/

我在Ubuntu 10.10上使用Rails 2.3.8,REE,1.8.7-2010.02。任何关于如何处理大量线程的见解(或者为什么不在这里使用线程)都非常感谢!

2 个答案:

答案 0 :(得分:1)

这些表上有索引吗?

您可以在批量插入期间暂时禁用这些索引吗?

在我们进行批量插入之前,我们禁用了索引键:

ALTER TABLE foo DISABLE KEYS

完成后,我们启用索引键:

ALTER TABLE foo ENABLE KEYS

来自文档:

ALTER TABLE ... DISABLE KEYS告诉MySQL停止更新非唯一索引。然后应该使用ALTER TABLE ... ENABLE KEYS来重新创建缺失的索引。 MySQL使用一种比逐个插入密钥快得多的特殊算法来实现这一点,因此在执行批量插入操作之前禁用密钥应该会带来相当大的加速。使用ALTER TABLE ... DISABLE KEYS除了前面提到的权限外,还需要INDEX权限。虽然禁用了非唯一索引,但是对于SELECT和EXPLAIN等语句将忽略它们,否则将使用它们。

答案 1 :(得分:1)

你可以尝试在一次交易中包装整个事情 - 这可能会产生很大的不同:

class Worker
  def self.perform(file)
    ObjectA.transaction do 
      CsvParser.each(file) do |line|
        next unless check_line_with_redis(line)
        a = ObjectA.find_or_initialize_by_reference(line[:reference])
        a.object_bs.destroy_all
        a.update_attributes(line)
      end
    end
  end
end

否则每次保存都将包含在自己的交易中。虽然,对于一个10GB的文件,你可能想要将其分解为每个事务或者其他东西1000个插入。