为什么 Ruby 中的 curl 比命令行 curl 更慢?

7
我正在尝试下载超过1m个页面(URL以序列ID结尾)。我已经实现了一种多用途的下载管理器,可配置下载线程数和一个处理线程。下载程序按批次下载文件:
curl = Curl::Easy.new

batch_urls.each { |url_info|
    curl.url = url_info[:url]
    curl.perform
    file = File.new(url_info[:file], "wb")
    file << curl.body_str
    file.close
    # ... some other stuff
}

我尝试下载了8000页的样本。使用上述代码时,2分钟内获得1000个页面。当我将所有的URL写入文件并在shell中执行时:
cat list | xargs curl

我在两分钟内生成了8000页的内容。但是,由于需要进行其他监控和处理,所以需要将其写成Ruby代码。
我尝试过:
- Curl::Multi - 速度更快,但会丢失50-90%的文件(不下载并且没有原因/代码) - 使用Curl::Easy的多个线程 - 速度与单线程相同
为什么重复使用Curl::Easy比后续的命令行curl调用要慢?我该如何使它更快?或者我做错了什么?
我更喜欢修复我的下载管理器代码,而不是通过其他方式下载。
在此之前,我正在调用带有URL列表文件的命令行wget。然而,并非所有错误都被处理,而且当使用URL列表时不能为每个URL指定输出文件。
现在我认为最好的方法是使用多个线程和对“curl”命令的系统调用。但是,既然可以直接在Ruby中使用Curl,为什么还要这样做呢?
下载管理器的代码在这里,如果有帮助的话:下载管理器(我已经尝试过超时时间的设置,从不设置到各种值的调整,似乎都没有帮助)。
任何提示将不胜感激。

我尝试创建一个简单的curl系统调用,而不是使用Curl :: Easy:result = system("curl", "-s", "-o", path, url)看起来速度要快得多。与Curl :: Easy相比,我获得了约300kb / s而不是60kb / s。这很奇怪-具有巨大系统开销且没有连接重用的系统调用比库函数更快。显然,CPU使用率更高,但速度更快。无论如何,这仍然不是解决我的问题的好方法,即使它的工作效果更好。 - Stiivi
你尝试过使用更新版本的curb和Curl::Multi.download接口吗? - todd
1
cat list | xargs curl 在命令行中一次性传递多个URL给Curl,以便同时检索多个URL。你可以在Ruby中轻松实现这一点,但是你需要进行苹果与苹果的比较,并使用HTTPClient或Typhoeus。 - the Tin Man
6个回答

5
这可能是 Typhoeus的一个适合的任务。
类似以下内容(未经测试):
require 'typhoeus'

def write_file(filename, data)
    file = File.new(filename, "wb")
    file.write(data)
    file.close
      # ... some other stuff
end

hydra = Typhoeus::Hydra.new(:max_concurrency => 20)

batch_urls.each do |url_info|
    req = Typhoeus::Request.new(url_info[:url])
    req.on_complete do |response|
      write_file(url_info[:file], response.body)
    end
    hydra.queue req
end

hydra.run

说起来,由于大量文件的存在,您可能会遇到内存问题。防止这种情况的一种方法是不将数据存储在变量中,而是直接将其流式传输到文件中。您可以使用 em-http-request 来实现这一点。
EventMachine.run {
  http = EventMachine::HttpRequest.new('http://www.website.com/').get
  http.stream { |chunk| print chunk }
  # ...
}

很整洁,看起来运行得非常好! :-) 我会尝试在晚上进行测试(不要关闭政府网站之一),至少测试50-100k个文件,以查看需要多长时间并捕获可能存在的问题。稍后需要定期运行。感谢您的提示。 - Stiivi

3

因此,如果您不设置on_body处理程序,则Curb将缓冲下载。如果您要下载文件,则应使用on_body处理程序。如果您想使用Ruby Curl下载多个文件,请尝试使用Curl :: Multi.download接口。

require 'rubygems'
require 'curb'

urls_to_download = [
  'http://www.google.com/',
  'http://www.yahoo.com/',
  'http://www.cnn.com/',
  'http://www.espn.com/'
]
path_to_files = [
  'google.com.html',
  'yahoo.com.html',
  'cnn.com.html',
  'espn.com.html'
]

Curl::Multi.download(urls_to_download, {:follow_location => true}, {}, path_to_files) {|c,p|}

如果您只想下载单个文件。

Curl::Easy.download('http://www.yahoo.com/')

这是一个好的资源: http://gist.github.com/405779

1

已经进行了一些基准测试,将 curb 与其他方法(如 HTTPClient)进行了比较。在几乎所有类别中,获胜者都是 HTTPClient。此外,还有一些记录的情况表明,在多线程场景下,curb 无法正常工作。

就像您一样,我也有过这样的经历。我在20多个并发线程中运行了curl系统命令,它比在20多个并发线程中运行curb快10倍。无论我尝试什么,情况总是如此。

自那以后,我已经转向使用HTTPClient,差异非常大。现在它的运行速度与同时运行20个curl系统命令一样快,而且CPU使用率更低。


为HTTPClient添加链接? - jwfearn

0

首先,我要说的是我对Ruby几乎一无所知。

我知道的是Ruby是一种解释性语言;它比为特定平台编译的高度优化代码慢并不奇怪。每个文件操作可能都会有检查,而curl则没有。"其他一些东西"会使事情变得更加缓慢。

您是否尝试过对代码进行分析以查看大部分时间花在哪里?


Ruby Curl::Easy类方法是libcurl C函数的“包装器”,所有下载代码都是用C编写的。在这种情况下,额外的调用和解释器开销是可以忽略不计的。另外请注意,执行curl命令行更快,并且包括进程创建和其他内容(这很快,但我认为比Ruby解释器函数调用要慢)。 - Stiivi
@Stiivi,显然你做出的一些假设是错误的。可能是这个部分:“附加调用和解释器开销可以忽略不计。”有些东西是不能忽略的。也许是将C字符串转换为Ruby字符串的过程。 - tster

0

Stiivi,

有没有可能Net::HTTP对于简单的HTML页面下载就足够了呢?


0

您没有指定 Ruby 版本,但 1.8.x 中的线程是用户空间线程,不由操作系统调度,因此整个 Ruby 解释器只能使用一个 CPU/核心。此外还有全局解释器锁和可能干扰并发性的其他锁。由于您试图最大化网络吞吐量,您可能正在低效地利用 CPU。

生成尽可能多的进程,以机器内存为限,并减少对线程的依赖。


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