SqaleでRailsアプリを高速サーバ「Rhebok」を使って起動する
Herokuに続き、Unicornの2倍のパフォーマンス発揮するRackサーバ「Rhebok」をパパボさんのPaaSであるSqaleで動かしてみる。15日間は無料お試しが出来るそうですよ。
Sqale - 開発者のためのホスティングサービス【スケール】 Ruby on Rails 対応。
Gemfileの用意
まず Gemfileを用意します。適当なディレクトリで
$ bundle init
して Gemfileを編集します
cat Gemfile # A sample Gemfile source "https://rubygems.org" gem "rails", "4.2.0"
とりあえず最新のRails 4.2.0で固定して、bundle install
$ bundle install --path vendor/bundle
Railsアプリケーションの作成
$ bundle exec rails new . --skip-bundle
すると、途中でGemfileを上書きするか聞かれるので、そのままリターンで続行
conflict Gemfile Overwrite /Users/kazeburo/Develop/sqale-rhebok/Gemfile? (enter "h" for help) [Ynaqdh]
上書きされたGemfileにRhebokを追加する
$ tail Gemfile gem 'gctools' gem 'rhebok', '>= 0.2.1'
OobGCを使う為にgctoolsも入れる。
ここまで出来たらbundle install
$ bundle install
Hello Worldだけでは面白くないので、Scaffoldを使ってアプリケーションのひな形を作る
$ bundle exec rails g scaffold msg nick:string body:text $ bundle exec rake db:migrate
ローカル環境で動くかどうか確認します。
$ bundle rails s
ローカルでRhebokを起動するには以下のコマンド
$ bundle exec rackup -s Rhebok -O MaxWorkers=1 -O OobGC=yes -O MaxRequestPerChild=0
Rhebokに変更しても問題なく動作すると思われます。
デプロイ準備
secrets.ymlのsecret_key_baseを設定する必要があります。今回はとりあえず環境変数にて設定してみました。
echo 'SECRET_KEY_BASE="'$(bundle exec rake secret)'"' > .env
Sqaleは.env
というファイルに環境変数を書いておくと起動時に読み込んでくれます。ちなみに、SqaleではデータベースにMySQLを使う事ができますが、今回はテストなのでSQLiteのままいきます
Rhebokでアプリケーションを起動したいので、Procfileも書きます。
$ cat Procfile app: bundle exec rackup -s Rhebok -O Path=/var/run/app/app.sock -O MaxWorkers=3 -O OobGC=yes -O MaxRequestPerChild=1000 -O MinRequestPerChild=500
Sqaleのコンテナはnginxとアプリケーションサーバが起動していて、間の通信にUnix Domain Socketを使うのが大きな特徴。なので、RhebokもUnix Domain SocketをListenさせる。
Rubyのバージョンを2.1.4に変更。ちなみに手元はrbenvを使わずにxbuildで2.1.5をいれてる
echo '2.1.4' > .ruby-version
vendorディレクトリをgitで管理しないようにして、git commit
$ echo '/vendor' >> .gitignore $ git init $ git add . $ git commit -m 'init'
最後に、sqaleにpush。
$ git remote add sqale ssh://sqale@gateway.sqale.jp:2222/userid/app_name.git $ git push sqale master
最初は起動までに数分かかりました。2回目からは1分ぐらいでデプロイされた。
ブラウザでみると、こんなページができました。
SqaleにSSHして確認
ちゃんとRhebokで起動したか、SSHでコンテナにログインして ps コマンドを打ってみました。
$ ssh -p 2222 sqale@gateway.sqale.jp sqale@app_name-1:~$ ps fax PID TTY STAT TIME COMMAND 6816 ? S 0:07 sshd: sqale@pts/0 6817 pts/0 Ss 0:00 \_ -bash 11236 pts/0 R+ 0:00 \_ ps fax 92 ? S 0:00 nginx: worker process 11133 ? Sl 0:00 foreman: master 11218 ? Sl 0:02 \_ ruby /home/sqale/current/vendor/bundle/ruby/2.1.0/bin/rackup -s Rhebok -O Path=/var/run/app/app.sock -O MaxWorkers=3 -O OobGC=yes -O MaxRequestPerC 11223 ? Sl 0:00 \_ ruby /home/sqale/current/vendor/bundle/ruby/2.1.0/bin/rackup -s Rhebok -O Path=/var/run/app/app.sock -O MaxWorkers=3 -O OobGC=yes -O MaxRequest 11226 ? Sl 0:00 \_ ruby /home/sqale/current/vendor/bundle/ruby/2.1.0/bin/rackup -s Rhebok -O Path=/var/run/app/app.sock -O MaxWorkers=3 -O OobGC=yes -O MaxRequest 11229 ? Sl 0:00 \_ ruby /home/sqale/current/vendor/bundle/ruby/2.1.0/bin/rackup -s Rhebok -O Path=/var/run/app/app.sock -O MaxWorkers=3 -O OobGC=yes -O MaxRequest
ちゃんと起動出来ているようですね〜
SSHで入ってログ見たり、straceでデバックしたり、killコマンドでアプリケーションの再起動できたりするのでSqale便利!
HerokuでSinatraのアプリを「Rhebok」で起動する
Unicornの2倍のパフォーマンス発揮するRackサーバ「Rhebok」をherokuで動かしてみる
アプリケーションは
heroku で Sinatra のアプリを動かす - Please Sleep
を参考にさせて頂きました。
Gemfile
まずGemfileを用意します
$ cat Gemfile source 'https://rubygems.org' ruby "2.1.5" gem 'sinatra' gem 'gctools' gem 'rhebok'
rubyのバージョンは2.1系を使い、gctoolsもいれます。Rhebokはgctoolsに含まれるGC::OOBを利用し、リクエスト処理終了後(Out Of Band)のGCを効率的に行います。
日本語訳: Ruby 2.1: Out-of-Band GC — sawanoboly.net
app.rb
"hello world"を出力するアプリケーションです。
$ cat app.rb require 'sinatra' get '/' do @text = "hello world\n" erb :index end
views/index.erb
テンプレート
$ cat views/index.erb <!DOCTYPE html> <html> <body> <%= @text %> </body> </html>
config.ru
config.ruファイルもつくります。
$ cat config.ru require './app' run Sinatra::Application
bundle install
依存モジュールのインストール
$ bundle install --path vendor/bundle
.gitignoreの用意
vendor、.bundleディレクトリを対象外とします
cat .gitignore vendor .bundle
手元で起動
$ bundle exec rackup -s Rhebok config.ru Rhebok starts Listening on localhost:9292 Pid:77840
9292ポートで起動するのでブラウザやcurlで確認します。
Procfile
Rhebokでアプリケーションが起動するようにProcfileを書きます
$ cat Procfile web: bundle exec rackup --port $PORT -s Rhebok -O OobGC=yes config.ru
OobGCを有効にしてみました。デフォルトで5個のworkerが起動します。
herokuへpush
git commitしてherokuへpushします
$ git init $ git add . $ git commit -m '…' $ heroku create APP_NAME $ git push heroku master
これでアプリケーションがherokuにて起動します。
curlでアクセスして確認します。
$ curl -v https://APP_NAME.herokuapp.com/ ... < HTTP/1.1 200 OK < Connection: keep-alive < Server: Rhebok < Content-Type: text/html;charset=utf-8 < Content-Length: 59 < X-Xss-Protection: 1; mode=block < X-Content-Type-Options: nosniff < X-Frame-Options: SAMEORIGIN < Date: Thu, 25 Dec 2014 02:47:58 GMT < Via: 1.1 vegur < <!DOCTYPE html> <html> <body> hello world </body> </html>
Server: Rhebok
と表示されたので、無事にRhebokにてアプリケーションが起動したことが確認できました。
同じアクセス数を処理している場合、RhebokはUnicornと比べてCPU使用率が低いという特徴もあるので、ぜひ試してくださいませ
picohttpparserのRubyバインディングとPreforkサーバを書く時に便利なgemをリリースしたので、Rackサーバ書いてみた
GazelleでやったことをRubyでもやってみようと思い、まず picohttpparser の Ruby バインディングと、perforkなサーバを書く時に便利なモジュールであるParallel::PreforkのRuby版を書いてリリースしました。
pico_http_parser
http://rubygems.org/gems/pico_http_parser
prefork_engine
http://rubygems.org/gems/prefork_engine
そしてこの2つを使って、StarletのRuby版を書いてみました。ソースコードはprefork_engineのrepositoryにあります。
https://github.com/kazeburo/prefork_engine/blob/master/example/starlet.rb
動かし方
まず上の2つのgemをいれます。bundlerを使っても良いですね
$ gem install pico_http_parser prefork_engine
prefork_engineのrepositoryをcloneしてきてexampleディレクトリに移動
$ git clone https://github.com/kazeburo/prefork_engine.git
$ cd prefork_engine/example
hello worldなサンプルアプリケーションも同梱されているので、そいつを動かしてみます
$ rackup -r ./starlet.rb -s Starlet -O MaxWorkers=5 -O MaxRequestPerChild=1000 config.ru
curlでアクセスすると
$ curl -sv http://localhost:9292/|less
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9292 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:9292
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Connection: close
< Content-Type: text/html
< Date: Thu, 11 Dec 2014 07:00:30 GMT
< Server: RubyStarlet
<
{ [data not shown]
* Closing connection 0
hello world
ちゃんと出て来た。大丈夫そう!
軽くベンチマーク
環境はEC2のc3.4xlarge、Amazon Linuxを使いました。nginxのうしろにアプリケーションサーバとして動作させた場合を想定してます。
ベンチマーク準備
Rubyはxbuildを使っていれます。unix domain socketで通信する為にstart_serverというPerl製のコマンドが必要になるのでPerlもいれます。
$ git clone https://github.com/tagomoris/xbuild.git $ ./xbuild/ruby-install 2.1.5 ~/local/ruby-2.1 $ xbuild/perl-install 5.20.1 ~/local/perl-5.20 -j4 $ export PATH=/home/ec2-user/local/ruby-2.1/bin:$PATH $ export PATH=/home/ec2-user/local/perl-5.20/bin:$PATH
nginxはyumでインストールし、次のように設定をしました
worker_processes 4; 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; } } }
比較のためにunicornもいれました。
$ gem install pico_http_parser prefork_engine unicorn
ruby版Starletの起動コマンド
$ start_server --path /dev/shm/app.sock -- rackup -r ./starlet.rb -E production -s Starlet -O MaxWorkers=12 -O MaxRequestPerChild=500000 config.ru
unicornのconfigと起動コマンド
$ cat unicorn.rb worker_processes 8 preload_app true listen "/dev/shm/app.sock" $ unicorn -E production -c unicorn.rb config.ru
ベンチマーク結果
ruby版Starlet
$ ./wrk -t 2 -c 32 -d 30 http://localhost/ Running 30s test @ http://localhost/ 2 threads and 32 connections Thread Stats Avg Stdev Max +/- Stdev Latency 401.75us 114.12us 5.67ms 80.92% Req/Sec 40.83k 2.90k 53.56k 72.18% 2291892 requests in 30.00s, 384.58MB read Requests/sec: 76397.24 Transfer/sec: 12.82MB
$ ./wrk -t 2 -c 32 -d 30 http://localhost/ Running 30s test @ http://localhost/ 2 threads and 32 connections Thread Stats Avg Stdev Max +/- Stdev Latency 393.75us 188.08us 4.41ms 78.60% Req/Sec 42.66k 6.64k 62.33k 71.41% 2389390 requests in 30.00s, 437.40MB read Requests/sec: 79647.31 Transfer/sec: 14.58MB
picohttpparserがすごく速いというのはありますが、それ以外の部分がRubyだけで書かれているにしては良いベンチマーク結果になっている気がします
Released Gazelle, new Simple and Fast Plack Handler
I released Gazelle, new Plack handler
Gazelle - Preforked Plack Handler for performance freaks - metacpan.org
Gazelle is a Plack Handler/PSGI server. This server is optimized for running HTTP application server behind a reverse proxy like nginx. When start Gazelle behind nginx and connect via unix domain socket, Gazelle is 3 times faster than starman.
Here is benchmark of Gazelle, starman and Starlet.
Benchmark details is here.
Gazelle supports following features.
- only supports HTTP/1.0. But not support KeepAlive feature
- All of network i/o code are written in XS
- ultra fast HTTP processing using picohttpparser
- uses accept4(2) if OS support
- uses writev(2) for output responses
- prefork and graceful shutdown using Parallel::Prefork
- hot deploy using Server::Starter
If you found problems and bug, please send p-r or issue to github
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公開したいと思ってます
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