スクレイピング処理をしていると大量のリクエストを発行する事が多いので、サーバの応答待ち時間がもったいないと感じていたのですが、最近巷でよく目にするCoroというモジュールを調べてみた所、非同期処理が割と簡単に書けるという事で試してみました。
以下では複数のURLに対してCoroを使って非同期でリクエストを投げる関数と、一つ一つリクエストを処理していく関数で、申し訳程度ですが比較ベンチマークを取っています。基本的にはasyncブロックの中に書いた処理が非同期になります。この例で注意するのはasyncブロック内のgetを非同期的に動かす為にCoro::LWPをuseしないと駄目だと言う事です。
#!/usr/bin/perl
use strict;
use warnings;
use Perl6::Say;
use Benchmark qw(timethese cmpthese);
use LWP::Simple;
use Web::Scraper;
use YAML;
use Coro;
use Coro::AnyEvent;
use Coro::Handle;
use Coro::LWP; # これが無いとLWP::Simple::getがそのまま同期処理になる
# 候補リンクを取得する
my $scraper = scraper {
process '//a', 'links[]' => '@href';
};
my $res = $scraper->scrape(URI->new('http://www.google.co.jp/'));
my @links = @{ $res->{links} };
STDOUT->autoflush(1);
my $result = timethese(10, {
Blocking => 'get_by_blocking',
NonBlocking => 'get_by_nonblocking',
});
cmpthese($result);
STDOUT->autoflush(0);
# 同期処理で取得する
sub get_by_blocking {
say "@ BLOCKING @";
my @content;
foreach my $link (@links) {
say "retrieving $link";
push(@content, get($link)); # 本来はここでスクレイピング
say "complete $link";
}
return @content;
}
# 非同期処理で取得する
sub get_by_nonblocking {
say "@ NON BLOCKING @";
my @cvs;
my @content;
foreach my $link (@links) {
my $cv = AnyEvent->condvar;
push(@cvs, $cv);
async {
say "retrieving $link";
push(@content, get($link)); # 本来はここでスクレイピング
say "complete $link";
$cv->send;
};
}
foreach my $cv (@cvs) {
$cv->recv;
}
return @content;
}
以下が実行結果になります。Googleのトップページのリンクを辿っているだけなので参考程度ですが、wallclockの値が結構大きく変わっているので、やはりサーバの応答待ち時間はバカに出来ない雰囲気です。少なくともこの例ではブロッキング処理にかかる時間のうち75%は応答待ちという事になります。これからは大量のリクエストは非同期で!
Blocking: 59 wallclock secs ( 1.81 usr + 0.38 sys = 2.19 CPU) @ 4.57/s (n=10)
NonBlocking: 14 wallclock secs ( 1.48 usr + 0.31 sys = 1.79 CPU) @ 5.59/s (n=10)
Rate Blocking NonBlocking
Blocking 4.57/s -- -18%
NonBlocking 5.59/s 22% --
あと非同期で処理した方が何故かCPU負荷が下がっているんですが、理由はわかりません。どちらかというと非同期の方がアイドル時間が短いので負荷は高いと思うんですけど、不思議です。正確にwallclockとかCPU等の数字の意味を理解していないので、追々調べようと思います。