ISUCON4 予選でアプリケーションを変更せずに予選通過ラインを突破するの術
AMIが公開されたのでもう一度やってみた。
AMIについてはこちらのエントリに書かれています
ISUCON4 予選問題の解説と講評 & AMIの公開 : ISUCON公式Blog
まず ami-e3577fe2 を m3.xlargeで起動します。
CPUは
model name : Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz
でした。
とりあえず、MySQLのindexを追加する。init.shに追加
$ cat init.sh
cat <<'EOF' | mysql -h ${myhost} -P ${myport} -u ${myuser} ${mydb}
alter table login_log add index ip (ip), add index user_id (user_id);
EOF
ベンチマークツールのhttp keepaliveが無効なので、sysctrl.conf でephemeral portを拡大したり、TIME_WAITが早く回収されるように変更する
$ cat /etc/sysctl.conf
net.ipv4.tcp_max_tw_buckets = 2000000
net.ipv4.ip_local_port_range = 10000 65000
net.core.somaxconn = 32768
net.core.netdev_max_backlog = 8192
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 10
ついでに、backlogも増やしている。
適用は
sudo /sbin/sysctl -p
で行う。
セッション管理にmemcachedを使うのでいれる
sudo yum install -y wget libevent-devel perl-devel
cd /tmp
wget http://www.memcached.org/files/memcached-1.4.21.tar.gz
tar zxf memcached-1.4.21.tar.gz
cd memcached-1.4.21
./configure --prefix=/usr/local/memcached && make && sudo make install
Supervisord経由で起動
[program:memcahed]
directory=/
command=/usr/local/memcached/bin/memcached -p 11211 -U 0 -u nobody -m 256 -c 200000 -v -t 1 -C -B ascii
autostart = true
memcachedの起動オプションについてはこのエントリが詳しい
memcached おすすめ起動オプションまとめ - blog.nomadscafe.jp
次に、Perlにアプリケーションを変更し、起動方法も変更
まず、cpanfileに以下を追加してcarton install
$ cat cpanfile
requires "Kossy", 0.38;
requires "Starlet", 0.24;
requires "URL::Encode::XS";
requires "Plack::Middleware::Session::Simple";
requires "Cache::Memcached::Fast";
requires "Sereal";
$ carton install
アプリケーションはStarletをつかって起動。その際にunix domain socketをlistenする
[program:isucon_perl]
directory=/home/isucon/webapp/perl
command=/home/isucon/env.sh carton exec start_server --path /dev/shm/app.sock --backlog 16384 -- plackup -s Starlet --workers=4 --max-reqs-per-child 500000 --min-reqs-per-child 400000 -E production -a app.psgi
user=isucon
stdout_logfile=/tmp/isucon.perl.log
stderr_logfile=/tmp/isucon.perl.log
autostart=true
Starlet + unix domain socketの話はこちらのエントリ
Starlet + Server::Stater で UNIX domain socketに対応しました - Hateburo: kazeburo hatenablog
セッションまわりだけ、PlackのMiddlewareを入れ替える
use Cache::Memcached::Fast;
use Sereal;
my $decoder = Sereal::Decoder->new();
my $encoder = Sereal::Encoder->new();
my $app = Isu4Qualifier::Web->psgi($root_dir);
builder {
enable 'ReverseProxy';
enable 'Session::Simple',
store => Cache::Memcached::Fast->new({
servers => [ { address => "localhost:11211",noreply=>0} ],
serialize_methods => [ sub { $encoder->encode($_[0])},
sub { $decoder->decode($_[0])} ],
}),
httponly => 1,
cookie_name => "isu4_session",
keep_empty => 0;
$app;
};
ここで使っているPlack::Middleware::Session::Simpleはこのエントリで紹介しています
Plack::Middleware::Session::Simple has been released - Hateburo: kazeburo hatenablog
そして、nginxの設定変更
$ cat /etc/nginx/nginx.conf
worker_processes 1;
events {
worker_connections 10000;
}
http {
include mime.types;
access_log off;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
etag off;
upstream app {
server unix:/dev/shm/app.sock;
}
server {
location / {
proxy_pass http://app;
}
location ~ ^/(stylesheets|images)/ {
open_file_cache max=100;
root /home/isucon/webapp/public;
}
}
}
cssや画像をnginxから配信するのと、アプリケーションサーバがunix domain socketになったのでその切り替え
nginxの設定はこのエントリが参考になるかな
G-WANはなぜ速いのか?をnginxと比べながら検証してみた - blog.nomadscafe.jp
最後にmysqlの設定
$ cat /etc/my.cnf
innodb_buffer_pool_size = 1G
innodb_flush_log_at_trx_commit = 0
innodb_flush_method=O_DIRECT
この3つだけ追加
全ての設定ができたので、mysql、nginxとSupervisordを再起動
$ sudo service mysqld restart
$ sudo /usr/bin/supervisorctl reload
$ sudo service nginx restart
これで準備完了。
いよいよ、benchmarkを動かしてみる
$ ./benchmarker bench --workload 8
07:51:41 type:info message:launch benchmarker
07:51:41 type:warning message:Result not sent to server because API key is not set
07:51:41 type:info message:init environment
07:51:50 type:info message:run benchmark workload: 8
07:52:50 type:info message:finish benchmark workload: 8
07:52:55 type:info message:check banned ips and locked users report
07:52:57 type:report count:banned ips value:605
07:52:57 type:report count:locked users value:4384
07:52:57 type:info message:Result not sent to server because API key is not set
07:52:57 type:score success:188930 fail:0 score:40812
見事予選突破ラインの37,808を、アプリケーションのコードを変更せずに超える事に成功しました。ぱちぱちぱち
// 追記
実際のスコアはtagomorisとsugyanのアプリケーションの変更が入って、ここから+10k以上だし、僕のしたことは足場を組んだだけ
— masahiro nagano (@kazeburo) October 14, 2014
Run any Perl applications on Heroku
I made a simple heroku-buildpack for perl. With using this buildpack you can run any perl application from Procfile.
github: https://github.com/kazeburo/heroku-buildpack-perl-procfile
Sample and Usage
This sample runs a PSGI server and a worker in 1 Dyno (It's free).
$ ls cpanfile Procfile server.pl lib/ $ cat cpanfile requires 'HTTP::Tiny','0.043'; requires 'Getopt::Long'; requires 'Proclet'; requires 'Plack'; requires 'Starlet'; $ cat Procfile web: ./server.pl --port $PORT $ heroku create yourappname --buildpack https://github.com/kazeburo/heroku-buildpack-perl-procfile.git $ git push heroku master ... -----> Heroku receiving push -----> Fetching custom buildpack -----> Perl/Procfile app detected -----> Installing dependencies
And server.pl
#!/usr/bin/env perl use strict; use warnings; use FindBin; use lib "$FindBin::Bin/lib"; use Proclet; use Plack::Loader; use Getopt::Long; use HTTP::Tiny; my $port = 5000; Getopt::Long::Configure ("no_ignore_case"); GetOptions( "p|port=s" => \$port, ); $proclet->service( tag => 'worker', code => sub { my $worker = MyWorker->new; $worker->run }, ); my $app = MyWeb->to_psgi; $proclet->service( code => sub { my $loader = Plack::Loader->load( 'Starlet', port => $port, host => 0, max_workers => 5, ); $loader->run($app); }, tag => 'web', ); $proclet->run;
In some cases, adding a worker to access the Web server periodically might be good.
$proclet->service( every => '*/30 * * * *', tag => 'ping', code => sub { my $ua = HTTP::Tiny->new; $ua->get("http://${yourservicename}.herokuapp.com/"); } );
Proclet is minimalistic Supervisor. it also supports cron like jobs. it's very useful!
At the last, don't forget to add exec permission to server.pl
chmod +x server.pl
Web::Module::CoreList
I created Web::Module::CoreList. This site provides Web interface of Module::CoreList. You can know what modules shipped with versions of perl through this web site.
Web::Module::CoreList is created based on tokuhirom's one. His site is currently unavailable.
(解決済み) DBIx::TransactionManager + File::RotateLogsで意図せずトランザクションが終了してしまう件
DBIx::TransactionManager 1.13で子プロセスでrollbackを実行しないような変更が入っています。
https://metacpan.org/release/NEKOKAK/DBIx-TransactionManager-1.13
TengやDBIx::Sunnyなどでトランザクションを使用し、File::RotateLogsでログを書き出している場合はバージョンアップをお勧めします。
経緯など
某サービスにおいて、DBIx::TransactionManagerを使ってトランザクションを実行している箇所で9時にトランザクションが意図せず終了するという問題がありました。
コードにするとこんな感じ
my $rotatelogs = File::RotateLogs->new( logfile => '/path/to/access_log.%Y%m%d%H%M', linkname => '/path/to/access_log', rotationtime => 86400, maxage => 86400*14, ); my $tm = DBIx::TransactionManager->new($dbh); my $txn = $tm->txn_scope; $dbh->do("insert into foo (id, var) values (1,'baz')"); $rotatelogs->print("log! log! log!"); $dbh->do("update foo set var='woody' where id=1"); $txn->commit;
DBIx::TransactionManager->txn_scopeすると、トランザクションを開始し、guardオブジェクトが返ります。commitせずに途中で終了した場合は、guradオブジェクトが破棄され、自動でrollbackがかかる仕組みになっています。
このトランザクション中にログを書くだけなら問題なさそうですが、File::RotateLogsはrotationtimeで設定した時間ごとにファイルを切り替え、maxageよりも古いログがあればProc::Daemonモジュール使い、削除用プロセスをデーモンとして起動して、古いログを削除します。
この際、Proc::Daemonはforkを2回して、プロセスを元のプロセスから切り離しますが、1回目にforkされた子プロセスは孫プロセスをforkしたあとすぐにexit()します。そこで、global destructionが走り、$txn も破棄されるので、なんとrollbackが走ってしまいます。
File::RotateLogsのprintとProc::Daemonを展開するとこんな感じ
$fh->print($log); if ( 削除するファイルがある ) { my $pid = fork; #1回目のfork if ( $pid == 0 ) { #子プロセス $pid = fork; #2回目のfork if ( $pid == 0 ) { #孫プロセス unlink .. #削除 POSIX::_exit(); #ここはglobal destructionを防いでいた } exit; ここでrollback } waitpid($pid,0); }
トランザクション中にforkなんて普通はしないと思いますが、今回はDBIx::TransactionManagerの方で対策をいれてもらい、guardオブジェクトのDESTROYの中で、トランザクション開始時のpidと現在のpidが異なっていたらrollbackせずにそのまま終了するようになりました。
nekokakさんありがとうございます
参考
DBIとforkの関係 - heboi blog
http://nihen.hatenablog.com/entry/2011/11/17/102557
Router::BoomとRouter::Simpleの文字列エンコードまわりの動作
昨日気付いた。
Router::Simpleはいわゆるutf8 flaggedな内部文字列を渡すと、キャプチャしたテキストも内部文字列として得られるけど、Router::Boomはバイナリ列となる。
use Router::Boom; use Router::Simple; use Encode; use Test::More; use utf8; subtest 'boom' => sub { my $path = '/foobarです'; my $router = Router::Boom->new(); $router->add('/:user', 'dispatch_user'); my @args = $router->match($path); is $args[1]->{user}, 'foobarです', 'match'; ok Encode::is_utf8($args[1]->{user}),'utf8'; }; subtest 'simple' => sub { my $path = '/foobarです'; my $router = Router::Simple->new(); $router->connect('/:user', {action=>'dispatch_user'}); my $args = $router->match($path); is $args->{user}, 'foobarです', 'match'; ok Encode::is_utf8($args->{user}),'utf8'; }; done_testing;
結果
% perl -MTest::Pretty ./boom.pl boom ✖ match # Failed test 'match' # at /path/to/boom.pl line 12. # got: 'foobarã§ã' # expected: 'foobarです' ✖ utf8 # Failed test 'utf8' # at /path/to/boom.pl line 13. simple ✓ match ✓ utf8
KossyはRouter::Boom使っているので、得られたテキストをdecodeするようにした
通信先が明確な内部APIなどのURIを構築するときはURI.pmを使わなくても良いんじゃないかな
通信先が明確な内部APIなどのURIを構築するときはURI.pmを使わなくても良いというかURI.pmはあまり速くないので、文字列連結だけで十分だと思います
#!/usr/bin/perl use strict; use warnings; use Benchmark qw/:all/; use URI; use URI::Escape; use URL::Encode::XS qw/url_encode/; my $base = q!http://api.example.com!; my $path = q!/path/to/endpoint!; my %param = ( token => 'bar', message => 'foo bar baz hoge hogehoge hogehoghoge', ); cmpthese(timethese(-2, { 'uri' => sub { my $uri = URI->new($base . $path); $uri->query_form( s_id => 1, type => 'foo', %param ); $uri->as_string; }, 'concat' => sub { my @qs = ( s_id => 1, type => 'foo', %param ); my $uri = $base . $path . '?'; while ( @qs ) { $uri .= shift(@qs) . '='. uri_escape(shift(@qs)) . '&' } substr($uri,-1,1,""); $uri; }, 'concat_xs' => sub { my @qs = ( s_id => 1, type => 'foo', %param ); my $uri = $base . $path . '?'; while ( @qs ) { $uri .= shift(@qs) . '='. url_encode(shift(@qs)) . '&' } substr($uri,-1,1,""); $uri; }, })); __END__ Benchmark: running concat, concat_xs, uri for at least 2 CPU seconds... concat: 2 wallclock secs ( 2.04 usr + 0.00 sys = 2.04 CPU) @ 81818.63/s (n=166910) concat_xs: 2 wallclock secs ( 2.17 usr + 0.00 sys = 2.17 CPU) @ 277470.51/s (n=602111) uri: 2 wallclock secs ( 2.20 usr + 0.00 sys = 2.20 CPU) @ 25653.18/s (n=56437) Rate uri concat concat_xs uri 25653/s -- -69% -91% concat 81819/s 219% -- -71% concat_xs 277471/s 982% 239% --