Hateburo: kazeburo hatenablog

SRE / 運用系小姑 / Goを書くPerl Monger

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を効率的に行います。

tmm1/gctools · GitHub

日本語訳: 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でもやってみようと思い、まず picohttpparserRuby バインディングと、perforkなサーバを書く時に便利なモジュールであるParallel::PreforkRuby版を書いてリリースしました。

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

unicorn

$ ./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.

f:id:kazeburo:20141114172529p:plain

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を使った事ないので実際の仕事で役に立つモジュールかどうか全くわからない。

他のモジュールと違って getsetといったメソッドは(まだ)ない。単純に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);
}

実際のサンプルは

https://github.com/kazeburo/isucon4-elimination-myhack/blob/master/perl/lib/Isu4Qualifier/Model.pm#L120

このあたり

Chobi

nginxでreverse proxyするという前提でつくったPlackのサーバ。KeepAliveなしのベンチマークでStarletよりはやくなってる

kazeburo/Chobi · GitHub

特徴

  • 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しないようになってる。今回使ってないけど、JSONcookieに入れておけば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を、アプリケーションのコードを変更せずに超える事に成功しました。ぱちぱちぱち

// 追記

「記憶に残る風景」 #地元発見伝

伊豆長岡の方から祖父の家がある内浦に出たところ。
左に曲がると三津シーパラダイス。右は沼津市内に戻る


「記憶に残る風景」 #地元発見伝
https://www.google.com/maps/preview?q=35.024057%2C138.898694
沼津市, 静岡県県道17号線

地元の魅力を発見しよう!特別企画「地元発見伝
http://partner.hatena.ne.jp/jimoto/

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