多个Nokogiri立即请求

时间:2011-11-23 17:55:51

标签: ruby-on-rails ruby multithreading ruby-on-rails-3 nokogiri

我有一个控制器,我试图从远程源获取XML文件。

类似的东西:

@artist = Nokogiri.XML(open(url).read)

但是,我想一次执行多个这些以获取不同的数据。我可以用某种方式使用线程吗?

单独执行一次需要400毫秒。因此,当它们连续三次执行时,响应最多可达1s +。

2 个答案:

答案 0 :(得分:5)

是的,你可以使用线程:

named_urls = {
  artist: 'http://foo.com/bar',
  song:   'http://foo.com/jim',
  # etc.
}
@named_xmls = {}
one_at_a_time = Mutex.new
named_urls.map do |name,url|
  Thread.new do
    doc = Nokogiri.XML(open(url).read)
    one_at_a_time.synchronize{ @named_xmls[name] = doc }
  end
end.each(&:join)

# At this point @named_xmls will be populated will all Nokogiri documents

我不确定在共享哈希中写入不同的密钥是否需要Mutex,但是为了安全起见并没有坏处。

答案 1 :(得分:4)

对于大量网址,您无法打开大量线程,因为您将使连接带宽饱和,并且您将开始出现连接错误。对于我的特定电缆调制解调器和特定服务器,我发现16个线程是一个很好的值。

我使用 Mathematica 来控制和改变我的ruby web报废程序的线程数,并监控其在不同线程数下的性能。这是结果:

performance vs threads plot

我没有直接使用Thread.new,而是编写了一个包装函数,只有在线程总数小于配置的最大值时才会打开一个新线程:

def maybe_new_thread
  File.open('max_threads.cfg', 'r') { |file| @MAX_THREADS =  file.gets.to_i }
  if Thread.list.size < @MAX_THREADS
    Thread.new { yield }
  else
    yield
  end
end

请注意,所需线程的最大数量只是存储在名为max_threads.cfg的文件中的数字,每次调用该函数时都会读取该文件。这允许您在程序运行时更改此变量的值。

该计划的一般结构如下:

named_urls = [ 'http://foo.com/bar', (... hundreds of urls ... ),'http://foo.com/jim']
named_urls.each do |url|
  maybe_new_thread do
    doc = Nokogiri.HTML(open(url))
    process_and_insert_in_database(doc)
  end
end

请注意,每个线程都将其结果存储在数据库中,因此我不需要使用Mutex类来协调线程之间的任何内容。

当我在数据库中插入时,我会在每个结果插入时包含一个精确时间的列。这是至关重要的,以便您可以计算您获得的性能。确保您使用毫秒支持定义此列(我使用MariaDB 5.3)。

这是我在 Mathematica 中使用的代码,用于控制最大线程数并实时绘制图形:

named_urls = {
  'http://foo.com/bar', (... hundreds of urls ... ),'http://foo.com/jim',
}
named_urls.each do |url|
  maybe_new_thread do
    doc = Nokogiri.HTML(open(url))
    process_and_insert_in_database(doc)
  end
end

setNumberOfThreads[n_] := Module[{},
  Put[n, "max_threads.cfg"];
  SQLExecute[conn,"DELETE FROM results"]]

operationsPerSecond := SQLExecute[conn,
  "SELECT
     (SELECT COUNT(*) FROM results)/
     (SELECT TIME_TO_SEC(TIMEDIFF((SELECT fin FROM results ORDER BY finishTime DESC LIMIT 1),
                                  (SELECT fin FROM results ORDER BY finishTime      LIMIT 1))))"][[1, 1]];
cops = {};
RunScheduledTask[AppendTo[cops, operationsPerSecond], 2];
Dynamic[ListLinePlot[cops]]

当它正在运行时,一旦您发现性能稳定,您可以使用setNumberOfThreads[]更改线程数,并查看性能效果。

最后一条评论。我没有直接使用open-uri的open方法,而是使用这个包装器,因此遇到错误,它会自动重试:

def reliable_open(uri)
  max_retry = 10
  try_counter = 1
  while try_counter < max_retry
    begin
      result = open(uri)
      return result
    rescue
      puts "Error when trying to open #{uri}"
      try_counter += 1
      sleep try_counter * 10
    end
  end
  raise "Imposible to open after #{max_retry} retries"
end