ISUCON4 予選アプリケーションの復習した結果
本選までの間に地道に復習をした結果です。
repositoryはここで公開されています
https://github.com/kazeburo/isucon4-elimination-myhack
最終スコア
派手な点ではありませんが、63000弱となりました
$ ~/benchmarker bench --workload 8 07:26:29 type:info message:launch benchmarker 07:26:29 type:warning message:Result not sent to server because API key is not set 07:26:29 type:info message:init environment 07:26:44 type:info message:run benchmark workload: 8 07:27:44 type:info message:finish benchmark workload: 8 07:27:49 type:info message:check banned ips and locked users report 07:27:52 type:report count:banned ips value:1048 07:27:52 type:report count:locked users value:5478 07:27:53 type:info message:Result not sent to server because API key is not set 07:27:53 type:score success:291580 fail:0 score:62985
構成
nginx、Perl、Redisを使った構成です。ユーザデータは変化しないので、最初に読み込んでしまいますが、他はRedisを使っています。集計は実装が面倒だったのでfujiwara組と同じようにRedisからMySQLに書き出して集計しています。
cookieがない場合だけ、Nginxから静的にindex.htmlをnginxから吐き出していますが、この設定をしなくても6万点でました。
新しく作ったもの/アップデートしたもの
性能をあげるため、いくつかの新しいモジュールをつくったり、バージョンアップをしました。
新作
- Redis::Jet
- Chobi
アップデート
- Kossy@0.39開発版
Redis::Jet
Redisのクライアント。たぶんPerlで一番速い。既にCPANにリリースしています。
https://metacpan.org/pod/Redis::Jet
Redisのプロトコルのパーサも含めてすべてXSで実装。業務でRedisを使った事ないので実際の仕事で役に立つモジュールかどうか全くわからない。
他のモジュールと違って getやsetといったメソッドは(まだ)ない。単純にRedisと通信するためのモジュールです
use Redis::Jet; my $jet = Redis::Jet->new( server => 'localhost:6379' ); # GET my $ret = $jet->command(qw/set redis data-server/); # $ret eq 'OK' my $value = $jet->command(qw/get redis/); # $value eq 'data-server' # SET my $ret = $jet->command(qw/set memcached cache-server/);
複数の値を返すコマンドは配列のリファレンスを返す
my $values = $jet->command(qw/mget redis memcached mysql/); # $values eq ['data-server','memcached',undef]
エラーも取れる
($values,$error) = $jet->command(qw/get redis memcached mysql/); # $error eq q!ERR wrong number of arguments for 'get' command!
複数のコマンドを一気におくる
my @values = $jet->pipeline([qw/get redis/],[qw/get memcached/]); # \@values = [['data-server'],['cache-server']] my @values = $jet->pipeline([qw/get redis/],[qw/get memcached mysql/]); # \@values = [['data-server'],[undef,q!ERR wrong...!]]
Redis::Jetのnoreplyモード
memcachedのnoreplyと同じような感じで、レスポンスを待たないモード。ただし、必要ないかもしれないけど、1回だけread(2)を発行している。memcachedのnoreplyはプロトコルにもある正規の動作だけど、Redisにはないのですね。
ベンチマークすると通常で1.5倍ぐらい速い
single get =======
Rate redis fast hiredis jet
redis 46494/s -- -58% -70% -75%
fast 109506/s 136% -- -30% -41%
hiredis 156052/s 236% 43% -- -16%
jet 185561/s 299% 69% 19% --
single incr =======
Rate redis fast hiredis jet jet_noreply
redis 49723/s -- -56% -70% -73% -82%
fast 111858/s 125% -- -33% -39% -61%
hiredis 165731/s 233% 48% -- -10% -42%
jet 184785/s 272% 65% 11% -- -35%
jet_noreply 283305/s 470% 153% 71% 53% --
pipeline =======
Rate redis fast jet jet_noreply
redis 15794/s -- -73% -87% -91%
fast 57769/s 266% -- -54% -67%
jet 124368/s 687% 115% -- -28%
jet_noreply 172577/s 993% 199% 39% --
noreplyを使うときは通常のインスタンスとは分けて使うのがお勧め。
sub redis {
$_[0]->{redis} ||= Redis::Jet->new(server => '127.0.0.1:6379');
}
sub redis_noreply {
$_[0]->{redis_noreply} ||= Redis::Jet->new(server => '127.0.0.1:6379',noreply=>1);
}
実際のサンプルは
このあたり
Chobi
nginxでreverse proxyするという前提でつくったPlackのサーバ。KeepAliveなしのベンチマークでStarletよりはやくなってる
特徴
- HTTP/1.0だけをサポート。KeepAliveはサポートなし
- 一部XSで実装
- HTTPのパースはHTTP::Parser::XSのコードをもってきて、ChobiのXS内部で処理
- Dateヘッダを吐かないオプションあり
- accept4(2) を使う。accept4(2)が使えない場合はacceptになる
- writevをつかってレスポンスを書き出す
最後のwritevが大きな特徴で、PSGIのレスポンス
[200,[..],['foo','bar','barz']]
このbodyの部分を文字列連結なしにそのまま書き出せます。
4コア8スレッドのマシンで参考ベンチマーク。plackとabは同じサーバ。
Chobi
$ plackup -I./lib -Iblib/arch -s Chobi --port 5003 --max-workers 8 --max-reqs-per-child 500000 --max-keepalive-reqs 500 -E prod --disable-date-header -e 'sub{[200,[],["foo","bar","\n"]]}'
$ ab -c 100 -n 80000 http://localhost:5003/
Requests per second: 61631.61 [#/sec] (mean)
$ plackup -I./lib -Iblib/arch -s Chobi --port 5003 --max-workers 8 --max-reqs-per-child 500000 --max-keepalive-reqs 500 -E prod --disable-date-header -e 'sub{[200,[],["foobar\n"]]}'
$ ab -c 8 -n 80000 http://localhost:5003/
Requests per second: 62545.11 [#/sec] (mean)
Starlet
$ plackup -I./lib -Iblib/arch -s Starlet --port 5003 --max-workers 8 --max-reqs-per-child 500000 --max-keepalive-reqs 500 -E prod --disable-date-header -e 'sub{[200,[],["foo","bar","\n"]]}'
$ ab -c 8 -n 80000 http://localhost:5003/
Requests per second: 33796.43 [#/sec] (mean)
$ plackup -I./lib -Iblib/arch -s Starlet --port 5003 --max-workers 8 --max-reqs-per-child 500000 --max-keepalive-reqs 500 -E prod --disable-date-header -e 'sub{[200,[],["foobar\n"]]}'
$ ab -c 8 -n 80000 http://localhost:5003/
Requests per second: 35143.41 [#/sec] (mean)
Starletは複数個の要素が入った配列だと遅くなる。配列内の個数が増えるともっと遅くなると思う。
予選アプリのスコアで、Starlet => Chobiで2000ぐらいスコア上がる。
Kossy@0.39開発版
cpanには0.39-trialであがっている。改善点は
- Router結果のcache
- JSONレスポンスの高速化
- header出力まわりの高速化
- xslateのcacheオプション
あたり。いくつかオプションが増えてて
$Kossy::XSLATE_CACHExslateのcache設定$Kossy::XSLATE_CACHE_DIR$Kossy::SECURITY_HEADERセキュリティ系のヘッダを出さないオプション
こんな感じで使う
local $Kossy::XSLATE_CACHE = 2; local $Kossy::XSLATE_CACHE_DIR = tempdir(DIR=>-d "/dev/shm" ? "/dev/shm" : "/tmp"); local $Kossy::SECURITY_HEADER = 0; my $app = Isu4Qualifier::Web->psgi($root_dir);
手元のベンチマークでは速くはなるけど、スコアはあまり変化しなかった
アプリケーションの工夫
ここからはその他のアプリケーションの変更点
Cookie Sessionの簡単な実装
cookieにセッション情報をそのまま入れてしまうやつの簡単な実装。Redis/memcachedへの通信を減らします。暗号化とかしてないので仕事でつかっっちゃだめよ。
use Cookie::Baker;
use WWW::Form::UrlEncoded::XS qw/parse_urlencoded build_urlencoded/;
my $cookie_name = 'isu4_session';
builder {
enable sub {
my $mapp = shift;
sub {
my $env = shift;
my $cookie = crush_cookie($env->{HTTP_COOKIE} || '')->{$cookie_name};
if ( $cookie ) {
$env->{'psgix.session'} = +{parse_urlencoded($cookie)};
$env->{'psgix.session.options'} = {
id => $cookie
};
}
else {
$cookie = '{}';
$env->{'psgix.session'} = {};
$env->{'psgix.session.options'} = {
id => '{}',
new_session => 1,
};
}
my $res = $mapp->($env);
my $cookie2 = build_urlencoded(%{$env->{'psgix.session'}});
my $bake_cookie;
if ($env->{'psgix.session.options'}->{expire}) {
$bake_cookie = bake_cookie( $cookie_name, {
value => '{}',
path => '/',
expire => 'none',
httponly => 1
});
}
elsif ( $cookie ne $cookie2 ) {
$bake_cookie = bake_cookie( $cookie_name, {
value => $cookie2,
path => '/',
expire => 'none',
httponly => 1
});
}
Plack::Util::header_push($res->[1], 'Set-Cookie', $bake_cookie) if $bake_cookie;
$res;
};
};
$app
}
cookieの内容が変わらない限り、set-cookieしないようになってる。今回使ってないけど、JSONでcookieに入れておけばnginx-luaからも参照ができる。
テンプレートエンジン使わない
テンプレートで複雑なことをしていないので、Xslate使うより、パーツ毎にHTMLを組み立てていくので十分。
テンプレートの静的な部分は、新しくモジュールつくって、DATA に入れておくと前処理が楽
以下はHTMLのminifyもしてる
package Isu4Qualifier::Template;
use strict;
use warnings;
use Data::Section::Simple;
my $reader = Data::Section::Simple->new(__PACKAGE__)->get_data_section;
chomp($reader->{$_}) for keys %$reader;
chomp($reader->{$_}) for keys %$reader;
$reader->{$_} =~ s/^ +//gms for keys %$reader;
$reader->{$_} =~ s/\n//gms for keys %$reader;
$reader->{$_} =~ s/="([a-zA-Z0-9-_]+)"/=$1/gms for keys %$reader;
sub get {
my $class = shift;
$reader->{$_[0]};
}
1;
__DATA__
@@ base_before
<!DOCTYPE html>
<html>
..
@@ base_after
..
</body>
</html>
使うときは ->getを使う。
my $flash = delete $env->{'psgix.session'}->{flash};
return [200,['Content-Type'=>'text/htmlcharset=UTF-8'],[
Isu4Qualifier::Template->get('base_before'),
Isu4Qualifier::Template->get('index_before'),
$flash ? q!<div id="notice-message" class="alert alert-danger" role="alert">!.$flash.q!</div>! : (),
Isu4Qualifier::Template->get('index_after'),
Isu4Qualifier::Template->get('base_after')
]];
}
前述のChobiのwritevとの合わせ技で高速にHTMLが出せるというおまけ付き。
Kossyの速度
HelloWorldのベンチマークで、Kossyの速度は生PSGIの半分しかでません。(他のWAFと比べてわりと速いとは思うけど)
なので突き詰めるとpsgiファイルにある程度書いていった方が速くなる。
my $app = Isu4Qualifier::Web->psgi($root_dir);
builder {
enable 'ReverseProxy';
sub {
my $env = shift;
if ( $env->{PATH_INFO} eq '/' ) {
…
return [200,[…],[…]];
}
elsif ( $env->{PATH_INFO} eq '/mypage' ) {
}
return $app->($env)
};
}
仕事でこんなコード書いたら怒られますが、ISUCONでは怒られない。
インデントが多くてきつい場合は、builderの外に出す。
Web.pmに書かれている $self-> でつかうメソッドは以下のように呼べる。
my $isuweb = Isu4Qualifier::Web->new($root_dir);
my $app = $web->psgi;
builder {
enable 'ReverseProxy';
sub {
my $env = shift;
if ( $env->{PATH_INFO} eq '/mypage' ) {
my $user_id = $env->{'psgix.session'}->{user_id};
my $user = $isuweb->user_id($user_id); #Web.pmに書かれているメソッド呼べる
return [200,[…],[…]];
}
elsif ( $env->{PATH_INFO} eq '/mypage' ) {
}
return $app->($env)
};
}
メソッドに$cが必要になる場合は修正するしかない。
Plack::Middleware::ReverseProxy
使わなくてもいける
builder {
sub {
my $env = shift;
my ( $ip, ) = $env->{HTTP_X_FORWARDED_FOR} =~ /([^,\s]+)$/;
$env->{REMOTE_ADDR} = $ip;
$app->run($env);
};
};
これで十分。効果は不明
Plack::Requestを使わないPOSTデータの読み込み
application/x-www-form-urlencodedのPOSTデータなら、Plack::Request使わずに決めうちで読んじゃえ。
use WWW::Form::UrlEncoded::XS qw/parse_urlencoded/;
my $input = $env->{'psgi.input'};
$input->seek(0, 0);
$input->read(my $chunk, 8192);
my $params = +{parse_urlencoded($chunk)};
$params->{login}
WWW::Form::UrlEncoded::XSも割と高速なので、単純なフォームだったらこれで速度稼げます。
さいごに
css/img消して実行するとこんな感じでした
$ GOGC=off ~/benchmarker bench --workload 8 05:10:19 type:info message:launch benchmarker 05:10:19 type:warning message:Result not sent to server because API key is not set 05:10:19 type:info message:init environment 05:10:33 type:info message:run benchmark workload: 8 05:11:33 type:info message:finish benchmark workload: 8 05:11:38 type:info message:check banned ips and locked users report 05:11:53 type:report count:banned ips value:4792 05:11:53 type:report count:locked users value:14442 05:12:01 type:info message:Result not sent to server because API key is not set 05:12:01 type:score success:232494 fail:0 score:232494
Chobiも近いうちにCPAN公開したいと思ってます