APIへの問い合わせを高速化したい
結論
Furlは非常に速い。でも、AnyEventの方が仕事が早く終わった。
今回は、相手サーバーが応答してデータを取得するまでの時間の方がボトルネック。
なお、結果として次の動画を作るのにとっても役に立った。
ときどきAEでAPI叩いているとエラーが返ってきましたけど。
背景
nicomas.sqlite用にデータを取得するperlスクリプトの実行に時間がかかっているのを何とかしたい。
135,000件以上の「アイドルマスター」タグのついた動画があるので、時間がかかるのはしょうがないけど、工夫できないだろうか。
なお、環境はstrawberry perl 5.12.1 built for MSWin32-x86-multi-threadをWindows XPで動かしています。
現状
一番時間がかかるのはタグ検索ページのスクレイピング。でもこれはニコニコ動画側の負荷を考えてウェイトをいれているし、これ以上早くする気は無い。
二番目がAPI問い合わせなので、ここを改善する。コードはだいたい下のとおり。
# もともとはLWPを使った、いたって普通のコード my $enc = Encode::find_encoding('utf8'); my $ua = LWP::UserAgent->new; for my $url (@urls) { my $res = $ua->get($url); if ($res->is_success) { my $content = $enc->decode('utf-8', $res->content); # do something } }
Furl編
参考情報とベンチマーク
にひりずむ::しんぷる - 初めての Furlを参考にした。
Kazuho's Weblog: 5x performance - switching from LWP to Furl & Net::DNS::LiteのNet::DNS::Liteを使う方法は、Net::DNS::Liteが上手く動いてくれなかったので試していない。
さくっとベンチマークを取ってみた。実際に使うサブルーチンをコピペした。
なお、localhost:5000ではplackupでhttp://ext.nicovideo.jp/api/gettumbinfo/sm9のデータを返すように書いたpsgiを動かしている。
use strict; use warnings; use 5.0100; use Furl::HTTP; use LWP::UserAgent; use Benchmark qw/timethese cmpthese/; # setup vars my $timeout_in_seconds = 5; my $count = 1000; # create HTTP object my $furl = Furl::HTTP->new( timeout => $timeout_in_seconds, ); # create LWP object my $ua = LWP::UserAgent->new(); # request param my $url = 'http://localhost:5000/'; my $retry = 3; cmpthese timethese $count, { Furl => sub { furl_get($furl, $url, $retry); }, LWP => sub { lwp_get($ua, $url, $retry); }, }; sub furl_get { my ($furl, $url, $retry, $wait) = @_; $retry ? $retry += 1 : $retry = 1; $wait = 0 unless $wait; my ($minor_version, $status, $message, $headers, $content); while ($retry) { ($minor_version, $status, $message, $headers, $content) = $furl->request( method => 'GET', url => $url, ); if ($status == 200) { last; } else { $retry--; sleep $wait if $wait; } } return ($status, $content); } sub lwp_get { my ($lwp, $url, $retry, $wait) = @_; $retry ? $retry += 1 : $retry = 1; $wait = 0 unless $wait; my $res; while ($retry) { $res = $lwp->get($url,); if ($res->is_success) { last; } else { $retry--; sleep $wait if $wait; } } return ($res->code, $res->content); }
結果はこちら。
Benchmark: timing 1000 iterations of Furl, LWP... Furl: 3 wallclock secs ( 0.78 usr + 0.48 sys = 1.27 CPU) @ 789.89/s (n=1000) LWP: 6 wallclock secs ( 2.42 usr + 0.76 sys = 3.19 CPU) @ 313.77/s (n=1000) Rate LWP Furl LWP 314/s -- -60% Furl 790/s 152% --
AnyEvent + Coro編
use strict; use warnings; use 5.0100; use Furl::HTTP; use AnyEvent::HTTP; use Coro; use Coro::AnyEvent; use Coro::Semaphore; use Benchmark qw/timethese cmpthese/; # 中略 open my $fh, '<:utf8', 'video_id_list.txt'; my @list; for (0 .. 100) { my $line = <$fh>; chomp $line; push @list, $url . $line; } close $fh; cmpthese timethese $count, { Furl => sub { for (@list) { furl_get($furl, $_, $retry) } }, Coro => sub { my $sem = Coro::Semaphore->new(5); my @coros; for my $video_id (@list) { chomp $video_id; push @coros, async { my $guard = $sem->guard; http_get $video_id, Coro::rouse_cb; my ($data, $hdr) = Coro::rouse_wait; if ($hdr->{Status} =~ /^2/) { #print "finish, $hdr->{Status} $hdr->{Reason} $hdr->{URL}\n"; } else { warn "error, $hdr->{Status} $hdr->{Reason} $hdr->{URL}\n"; } }; } $_->join for @coros; }, }; sub furl_get { my ($furl, $url, $retry, $wait) = @_; $retry ? $retry += 1 : $retry = 1; $wait = 0 unless $wait; my ($minor_version, $status, $message, $headers, $content); while ($retry) { ($minor_version, $status, $message, $headers, $content) = $furl->request( method => 'GET', url => $url, ); if ($status == 200) { last; } else { $retry--; #sleep $wait if $wait; } } return ($status, $content); }
AnyEvent::HTTPは同一ホストへのアクセスを4に制限しているので、セマフォで並列を5にする必要はなさそうです。ちょっとCoroを触ってみたかったので、上のようなコードになっています。
結果はこちら。
Benchmark: timing 1 iterations of Coro, Furl... Coro: 24 wallclock secs ( 0.25 usr + 0.09 sys = 0.34 CPU) @ 2.91/s (n=1) (warning: too few iterations for a reliable count) Furl: 6 wallclock secs ( 0.08 usr + 0.08 sys = 0.16 CPU) @ 6.41/s (n=1) (warning: too few iterations for a reliable count) Rate Coro Furl Coro 2.91/s -- -55% Furl 6.41/s 121% --
ご覧の通り、Furlの方が処理は速いけど、AnyEvent::HTTPの方が全ての仕事をやり終えるのが早い。ローカルでデータをパースしたりするともっと差が出るようです。
検証が足りない気もするけど、自分の用途では役に立ったのでここまで。なお、データ収集スクリプトでの実戦投入はまだやってません。