Subscribed unsubscribe Subscribe Subscribe

Hateburo: kazeburo hatenablog

Operations Engineer / Site Reliability / 運用系小姑 / Perl Monger

2007-2015 YAPC::Asia Tokyo で喋ってきたまとめ

今年のblogはまだ書いてないけど書きました、これまでYAPC::Asia Tokyo で喋ってきたblog記事をまとめてみました 2007年から2015年まで10回中9回、トーク(2007年はLT)をさせて頂きました。

2015 ISUCONの勝ち方

isucon優勝するぞー!

blog.nomadscafe.jp

2014 Dockerで遊んでみよっかー

2014年はDockerの話をしました

blog.nomadscafe.jp

2013 PSGI/Plack・Monocerosで学ぶハイパフォーマンスWebアプリケーションサーバの作り方

2013年はPlackのサーバを作る話。前夜祭LTもやりました

blog.nomadscafe.jp

2012 1台から500台までのMySQL運用(YAPC::Asia編)

2012年はDB運用の話

blog.nomadscafe.jp

2011 運用しやすいWebアプリケーションの構築方法

2011年は運用しやすいWebアプリケーションというテーマで、ログやSQLについて

blog.nomadscafe.jp

2010 Introduction to CloudForecast

cloudforecastの話。最近はkuradoというのを作って使ってます

blog.nomadscafe.jp

2009 大規模画像配信とPerl

大規模画像配信の話

blog.nomadscafe.jp

2008 memcached in mixi

mixiのエンジニアblogで書いてました。2つトークをしていて、1つはmemcachedの話、もう一つはmod_perlをつかったjob処理でした

alpha.mixi.co.jp

2007 Expectをつかった Expectをつかったサーバ管理レシピ

LTでコマンドを複数のサーバで実行する話をしました

YAPC::Asia 2007でLTしてきました : blog.nomadscafe.jp

Rhebok, a High Performance Rack Handler/Server 2x faster than Unicorn

Last December I released Rhebok to rubygems. Rhebok is a High Performance Rack Handler/Server.

rhebok | RubyGems.org | your community gem host

kazeburo/rhebok · GitHub

Rhebok is a standalone Preforking Web Server. This server is optimized for running HTTP application server behind a reverse proxy like nginx. When start Rhebok as upstream server of nginx and connect via unix domain socket, Rhebok is 2 times faster than Unicorn.

Benchmark

f:id:kazeburo:20150106161554p:plain

"nginx static file" represents req/sec when delivering 13 bytes static files from nginx

Benchmark details is here.

Features

Rhebok supports following features.

  • ultra fast HTTP processing using picohttpparser
  • uses accept4(2) if OS support
  • uses writev(2) for output responses
  • prefork and graceful shutdown using prefork_engine
  • hot deploy using start_server (here is golang version by lestrrat-san)
  • supports HTTP/1.1. But does not have Keepalive.
  • supports OobGC

Installation and Usage

Add this line to your application's Gemfile and execute bundle install

gem 'rhebok'

Or install it yourself as

$ gem install rhebok

To run Rhebok, use the rackup command.

$ rackup -s Rhebok --port 8080 -O MaxWorkers=5 -O MaxRequestPerChild=1000 -O OobGC=yes -E production config.ru

For more details, please read README.md. If you found problems and bug, please send p-r or issue to github

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分ぐらいでデプロイされた。

ブラウザでみると、こんなページができました。

f:id:kazeburo:20141227233921p:plain

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

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