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_CACHE
xslateの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公開したいと思ってます