使用HTTP :: Async&和Perl的并发下载速度太慢网::异步HTTP ::

时间:2017-03-26 03:37:14

标签: perl http asynchronous concurrency download

我正在尝试与脚本并行获取大约7个网址:第一个在下面,使用HTTP :: Async,第二个是on pastebin,使用Net :: Async :: HTTP。 问题是我得到了相同的计时结果 - 所有网址列表大约8..14秒。与从shell开始的curl + xargs相比,这是不可接受的缓慢,使用10-20“线程”可以在不到3秒的时间内完成所有操作。 例如,第一个脚本中的Devel :: Timer显示最大队列长度甚至小于6($queue->in_progress_count <= 5,$queue->to_send_count = 0总是如此)。因此,它看起来像foreach with $ queue-&gt; add执行速度太慢,我不知道为什么。 我使用Net :: Async :: HTTP(在pastebin上的第二个脚本)获得了相同的情况,这比第一个更慢。

所以,请问,有人知道,我做错了什么?与从shell开始的curl + xargs相比,我怎样才能获得并发下载速度?

#!/usr/bin/perl -w
use utf8;
use strict;
use POSIX qw(ceil);
use XML::Simple;
use Data::Dumper;
use HTTP::Request;
use HTTP::Async;
use Time::HiRes qw(usleep time);
use Devel::Timer;

#settings
use constant passwd => 'ultramegahypapassword';
use constant agent => 'supa agent dev.alpha';
use constant timeout => 10;
use constant slots => 10;
use constant debug => 1;

my @qids;
my @xmlz;
my $queue = HTTP::Async->new(slots => slots,max_request_time => 10, timeout => timeout, poll_interval => 0.0001);
my %responses;
my @urlz = (
'http://testpodarki.afghanet/api/products/4577',
'http://testpodarki.afghanet/api/products/4653',
'http://testpodarki.afghanet/api/products/4652',
'http://testpodarki.afghanet/api/products/4571',
'http://testpodarki.afghanet/api/products/4572',
'http://testpodarki.afghanet/api/products/4666',
'http://testpodarki.afghanet/api/products/4576',
'http://testpodarki.afghanet/api/products/4574',
'http://testpodarki.afghanet/api/products/4651',
'http://testpodarki.afghanet/api/stock_availables/?display=full&filter[id_product]=[3294]',
'http://testpodarki.afghanet/api/specific_prices/?display=full&filter[id_product]=[3294]',
'http://testpodarki.afghanet/api/combinations/?display=full&filter[id_product]=[4577]',
'http://testpodarki.afghanet/api/stock_availables/?display=full&filter[id_product]=[4577]',
'http://testpodarki.afghanet/api/specific_prices/?display=full&filter[id_product]=[4577]',
'http://testpodarki.afghanet/api/product_option_values/188',
'http://testpodarki.afghanet/api/product_option_values/191',
'http://testpodarki.afghanet/api/product_option_values/187',
'http://testpodarki.afghanet/api/product_option_values/190',
'http://testpodarki.afghanet/api/product_option_values/189',
'http://testpodarki.afghanet/api/stock_availables/?display=full&filter[id_product]=[4653]',
'http://testpodarki.afghanet/api/specific_prices/?display=full&filter[id_product]=[4653]',
'http://testpodarki.afghanet/api/images/products/4577/12176',
'http://testpodarki.afghanet/api/stock_availables/?display=full&filter[id_product]=[4652]',
'http://testpodarki.afghanet/api/specific_prices/?display=full&filter[id_product]=[4652]',
'http://testpodarki.afghanet/api/images/products/4653/12390',
'http://testpodarki.afghanet/api/combinations/?display=full&filter[id_product]=[4571]',
'http://testpodarki.afghanet/api/stock_availables/?display=full&filter[id_product]=[4571]',
'http://testpodarki.afghanet/api/specific_prices/?display=full&filter[id_product]=[4571]',
'http://testpodarki.afghanet/api/images/products/4652/12388',
'http://testpodarki.afghanet/api/product_option_values/175',
'http://testpodarki.afghanet/api/product_option_values/178',
'http://testpodarki.afghanet/api/product_option_values/179',
'http://testpodarki.afghanet/api/product_option_values/180',
'http://testpodarki.afghanet/api/product_option_values/181',
'http://testpodarki.afghanet/api/images/products/3294/8965',
'http://testpodarki.afghanet/api/product_option_values/176',
'http://testpodarki.afghanet/api/product_option_values/177',
'http://testpodarki.afghanet/api/combinations/?display=full&filter[id_product]=[4572]',
'http://testpodarki.afghanet/api/stock_availables/?display=full&filter[id_product]=[4572]',
'http://testpodarki.afghanet/api/specific_prices/?display=full&filter[id_product]=[4572]',
'http://testpodarki.afghanet/api/product_option_values/176',
'http://testpodarki.afghanet/api/product_option_values/181',
'http://testpodarki.afghanet/api/product_option_values/180',
'http://testpodarki.afghanet/api/images/products/4571/12159',
'http://testpodarki.afghanet/api/product_option_values/177',
'http://testpodarki.afghanet/api/product_option_values/179',
'http://testpodarki.afghanet/api/product_option_values/175',
'http://testpodarki.afghanet/api/product_option_values/178',
'http://testpodarki.afghanet/api/stock_availables/?display=full&filter[id_product]=[4666]',
'http://testpodarki.afghanet/api/combinations/?display=full&filter[id_product]=[4576]',
'http://testpodarki.afghanet/api/specific_prices/?display=full&filter[id_product]=[4666]',
'http://testpodarki.afghanet/api/stock_availables/?display=full&filter[id_product]=[4576]',
'http://testpodarki.afghanet/api/specific_prices/?display=full&filter[id_product]=[4576]',
'http://testpodarki.afghanet/api/images/products/4572/12168',
'http://testpodarki.afghanet/api/product_option_values/185',
'http://testpodarki.afghanet/api/product_option_values/182',
'http://testpodarki.afghanet/api/product_option_values/184',
'http://testpodarki.afghanet/api/product_option_values/183',
'http://testpodarki.afghanet/api/product_option_values/186',
'http://testpodarki.afghanet/api/images/products/4666/12413',
'http://testpodarki.afghanet/api/combinations/?display=full&filter[id_product]=[4574]',
'http://testpodarki.afghanet/api/stock_availables/?display=full&filter[id_product]=[4574]',
'http://testpodarki.afghanet/api/specific_prices/?display=full&filter[id_product]=[4574]',
'http://testpodarki.afghanet/api/product_option_values/177',
'http://testpodarki.afghanet/api/product_option_values/181',
'http://testpodarki.afghanet/api/images/products/4576/12174',
'http://testpodarki.afghanet/api/product_option_values/176',
'http://testpodarki.afghanet/api/product_option_values/180',
'http://testpodarki.afghanet/api/product_option_values/179',
'http://testpodarki.afghanet/api/product_option_values/175',
'http://testpodarki.afghanet/api/product_option_values/178',
'http://testpodarki.afghanet/api/specific_prices/?display=full&filter[id_product]=[4651]',
'http://testpodarki.afghanet/api/images/products/4574/12171',
'http://testpodarki.afghanet/api/stock_availables/?display=full&filter[id_product]=[4651]',
'http://testpodarki.afghanet/api/images/products/4651/12387'
);

my $timer = Devel::Timer->new();


foreach my $el (@urlz) {
    my $request = HTTP::Request->new(GET => $el);
    $request->header(User_Agent => agent);
    $request->authorization_basic(passwd,''); 
    push @qids,$queue->add($request);
    $timer->mark("pushed [$el], to_send=".$queue->to_send_count().", to_return=".$queue->to_return_count().", in_progress=".$queue->in_progress_count());
}

$timer->mark('requestz pushed');

while ($queue->in_progress_count) {
    usleep(2000);
    $queue->poke();
}

$timer->mark('requestz complited');

process_responses();


$timer->mark('responzez processed');

foreach my $q (@xmlz) {
#    print ">>>>>>".Dumper($q)."<<<<<<<<\n";
}

$timer->report();
print "\n\n";

4 个答案:

答案 0 :(得分:6)

HTTP::Async的最佳结果是超过4秒,最多超过5秒。据我所知,这种方法不是必需的,这是一个简单的分叉示例,需要2点多一点,最多3秒钟。

它使用Parallel::ForkManagerLWP::UserAgent进行下载。

use warnings;
use strict;
use Path::Tiny;    
use LWP::UserAgent;
use Parallel::ForkManager;

my @urls = @{ get_urls('https://pastebin.com/raw/VyhMEB3w') };

my $pm = new Parallel::ForkManager(60);  # max of 60 processes at a time
my $ua = LWP::UserAgent->new; 
print "Downloading ", scalar @urls, " files.\n";

my $dir = 'downloaded_files/';
mkdir $dir if not -d $dir;
my $cnt = 0;   
foreach my $link (@urls) 
{
    my $file = "$dir/file_" . ++$cnt . '.txt';

    $pm->start and next;                        # child process

    # add code needed for actual pages (authorization etc)            
    my $response = $ua->get($link);        
    if ($response->is_success) {
        path($file)->spew_utf8($response->decoded_content);
    }
    else { warn $response->status_line }

    $pm->finish;                                # child exit
}
$pm->wait_all_children;

sub get_urls {
    my $resp = LWP::UserAgent->new->get($_[0]);
    return [ grep /^http:/, split /\s*'?,?\s*\n\s*'?/, $resp->decoded_content ];
};

使用Path::Tiny编写文件。其path构建一个对象,spew例程编写该文件。

作为参考,顺序下载大约需要26秒。

当最大进程数设置为30时,这需要4秒,而60则超过2秒,大约与(最多)90相同。此测试中有70个URL。

在具有良好网络连接的4核笔记本电脑上进行测试。 (这里的CPU并不是那么重要。)测试反复运行,多次,多天。

与问题

中的方法进行比较

最佳HTTP::Async结果比上述慢约两倍。他们有30-40个“插槽”,因为数字越多,时间越长,谜题(我)。该模块使用select通过Net::HTTP::NBNet::HTTP的非阻止版本)进行多路复用。虽然select“不能很好地扩展”,但这涉及数百个套接字,我希望能够在这个网络绑定问题上使用超过40个。简单的分叉方法。

此外,select被认为是一种监视套接字的慢速方法,而fork甚至不需要,因为每个进程都有自己的url。 (当有许多连接时,这可能导致模块的开销?)Fork的固有开销是固定的,并且由于网络访问而相形见绌。如果我们在(很多)数百次下载后,系统可能会因进程而变得紧张,但select也不会很好。

最后,基于select的方法一次严格下载一个文件, 并且当请求被add时打印看到效果 - 我们可以延迟。分叉下载并行(在这种情况下,所有70个同时没有问题)。然后会出现网络或磁盘瓶颈,但与增益相比,这个瓶颈很小。

更新我将此推送到站点和进程数量的两倍,没有出现OS / CPU压力的迹象,并保持平均速度。

所以我想说,如果你需要剃掉每一秒使用的叉子。但是,如果这不是至关重要的,HTTP::Async(或其他)有其他好处,那么请满足(稍微)一点点下载。

表现良好的HTTP::Async代码最终只是

foreach my $link ( @urls ) {  
    $async->add( HTTP::Request->new(GET => $link) );
}    
while ( my $response = $async->wait_for_next_response ) { 
    # write file (or process otherwise)
}

我也尝试调整标题和时间。 (这包括按照keep-alive的建议放弃$request->header(Connection => 'close'),不起作用。)

答案 1 :(得分:4)

解释我的评论。我很好奇,因为以前从未使用Net::Async::HTTP,想在本地尝试你的脚本。因此,创建了这个极简主义的Plack app.psgi

use 5.014;
use warnings;

use Plack::Builder;
use Plack::Request;
use Plack::Response;
use Time::HiRes qw(usleep);

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);

    my($ms,$id) = $req->path_info =~ /(\d+)/g;
    $ms //= 0;
    $id //= "NOID";

    #fake some processing slowness
    usleep($ms);

    my $res = Plack::Response->new(200);
    $res->content_type('text/plain');
    $res->body( "req# $id served by PID $$ fakewait: $ms\n");
    return $res->finalize;
};

builder {
#    enable "Auth::Basic", authenticator => \&auth;
    $app;
};

#sub auth { return $_[0] eq 'me' && $_[1] eq 'me' }

服务器以GET /sleep_time/reqID格式了解网址,其中

  • usleep的休眠时间以微秒为单位 - 服务器在响应之前休眠给定时间。例如。它假装了一些“处理时间”。
  • id - req-identify的任何数字......

E.g。请求GET /1000000/1,服务器将在响应之前休眠1秒。作为回应,包括响应过程的PID

在一个终端窗口中,使用Starman preforkimg服务器和默认20工作人员运行上述内容。

plackup -s Starman

在另一个窗口中,结果使用xargs

time seq 20 | xargs -n1 -P10 -I% curl http://localhost:5000/1000000/%

所以,发送20个请求,每个响应花费1s个处理时间。

req# 1 served by PID 28163 fakewait: 1000000
req# 2 served by PID 28161 fakewait: 1000000
req# 3 served by PID 28162 fakewait: 1000000
req# 4 served by PID 28160 fakewait: 1000000
req# 5 served by PID 28159 fakewait: 1000000
...
real    0m4,032s
user    0m0,092s
sys     0m0,074s

所以,20个请求= 4秒。可见,响应PID是不同的 - 例如,repsonse由不同的工作人员发送。

现在使用您的脚本async.pl(稍微缩短/修改):

#!/usr/bin/perl
use 5.014;
use warnings;

use HTTP::Request;
use IO::Async::Loop;
use Net::Async::HTTP;
use Future::Utils qw(fmap_void);

my $sleep = $ARGV[0] // 0;
my $numreq = $ARGV[1] // 20;

my $loop = IO::Async::Loop->new();

my $http = Net::Async::HTTP->new( timeout => 10, max_connections_per_host => 0, pipeline => 0, ip_tos => 0x10 );
$loop->add( $http );

my $future = fmap_void {
    (my  $url ) = @_;
    my $request = HTTP::Request->new(GET => $url);
    #$request->authorization_basic('me','me');
    $http->do_request( request => $request )
        ->on_done( sub {
            my $response = shift;
            my($body) = $response->content =~ s/\n.*//sr;
            print "$url [", $response->code, "] --> $body\n";
        } )
        ->on_fail( sub {
            my $failure = shift;
            print "$url failed: $failure\n";
        } );
} foreach => [map { "http://localhost:5000/$sleep/$_" } (1 .. $numreq)];
$loop->await( $future );

命令

time perl async.pl 1000000 20

结果

http://localhost:5000/1000000/1 [200] --> req# 1 served by PID 28160 fakewait: 1000000
http://localhost:5000/1000000/2 [200] --> req# 2 served by PID 28160 fakewait: 1000000
http://localhost:5000/1000000/3 [200] --> req# 3 served by PID 28160 fakewait: 1000000
http://localhost:5000/1000000/4 [200] --> req# 4 served by PID 28160 fakewait: 1000000
http://localhost:5000/1000000/5 [200] --> req# 5 served by PID 28160 fakewait: 1000000
http://localhost:5000/1000000/6 [200] --> req# 6 served by PID 28160 fakewait: 1000000
...
real    0m20,309s
user    0m0,183s
sys     0m0,053s

相同的20个请求= 20秒,每个请求由相同的PID提供。像纯顺序处理一样。 :(

这个可能是,因为请求重用了连接(例如,保持活动状态)。

最后 - 不幸的是,正如我所说 - 我对模块没有任何经验,所以不知道如何强制模块做 - 不重用已打开的连接。

答案 2 :(得分:4)

所以,最后得到了工作样本(full script)。它使用Furl中的fork_callAnyEvent::Util。这个例子在~3秒内返回,这还不错。如果您需要使用基本的HTTP身份验证,只需使用带有以下信头的URI:username:password@hostdomain.org/path?param1=val1&param2=val2。您最好在使用use EV;之前添加AnyEvent,因为EV是最快的。

#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
use Devel::Timer;
use Furl;
use EV;
use AnyEvent;
use AnyEvent::Util 'fork_call';

#get full script
my @urls = (...);


sub fetch {
    my $url = shift;
    my $furl = Furl::HTTP->new(agent => 'Furl/0.31',timeout => 3);
    print "start $url\n";
    my ($ver, $code, $msg, $headers, $body) = $furl->get($url);
    my $size = length $body;
    print "finished $url, $size bytes\n";
    return ($code, $msg, $headers, $body);
}

my %resps;

my $timer = Devel::Timer->new();
$timer->mark('foreach');
$AnyEvent::Util::MAX_FORKS = 20;
my $cv = AE::cv;
foreach my $url (@urls) {
    $timer->mark('next foreach');
    $cv->begin;
    fork_call {
        print "getting $url... ";
        my ($code, $msg, $headers, $body)=fetch($url);
        print "[$code]\n";
        return ($url, $code, $msg, $headers, $body);
        }
        sub {
            print "adding 2 %resps\n";
            my ($url, $code, $msg, $headers, $body)=@_;
            $resps{$url}->{'code'}=$code;
            $resps{$url}->{'msg'}=$msg;
            $resps{$url}->{'headers'}=$headers;
            $resps{$url}->{'body'}=$body;
            $cv->end;
        };
}
$cv->recv;
$timer->mark('end');

print "\nall data is ready, press <ENTER>:";
<STDIN>;
print Dumper(%resps);
print "\n<PRESS ENTER>to print timer report\n";
<STDIN>;
$timer->report();
sleep(3);

答案 3 :(得分:2)

异步将比并行下载慢:异步代码只会在等待响应时产生其他调用,但是在单个进程中按顺序进行下载,而curl + xargs将100%工作(好,几乎100%) ,并且只要你没有使核心饱和)并行,就像使用分叉工人时一样。

请pogle,请注意“并发不是并行”