Hateburo: kazeburo hatenablog

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

Amon2でKossy::Request/Responseを使う

使う事があるかどうかは置いといて、使えた

package MyApp::Web;
..
# request/response
use Kossy::Request;
use Kossy::Response;
sub create_request  { Kossy::Request->new($_[1]) }
sub create_response { shift; Kossy::Response->new(@_) }


メリット?

  • URL::Encode::XS / HTTP::Headers::Fast による高速化
  • application/jsonの自動decodeが使える
  • uri_forがある

デメリット?

データベースのmasterとslaveの使い分けの話。2014年版

社内で少し話題になったので。

運用上の話はfujiwaraさんの

MySQLをmaster:slave=1:1構成にして参照をslaveに向けるのがなぜ良くないか - 酒日記 はてな支店
MySQLで参照の負荷分散を行うslaveは3台から構成するのがよいのでは - 酒日記 はてな支店

をみてください。

最近、新しくサービスができたり、新規機能でデータベースを追加する際には必ず全ての参照をmasterに向けてもらっています。理由は上記のエントリを読んでください。このような構成が取れるのはもちろん性能的にそれで問題ないからです。

新しいハードウェアに、設定されたMySQL、問題のないように書かれたSQLであれば、数千QPSは余裕に、また少し頑張れば数万QPSを一台で賄えます。なので大体のサービスはmaster一台で十分です。

さらにこの考え方を進めて、Webアプリケーションの中で

sub dbh {
    DBI->connect("MASTER");
}

sub dbh_slave {
    my $dbh;
    for my $dsn ([MASTER]) { #masterを使ってる
        $dbh = DBI->connect($dsn);
        last if $dbh;
    }
}

sub controller_post {
    $c->dbh->do("insert ..")
}

sub controller_get {
    $c->dbh_slave->selectrow..("select ..")
}

のようにしているところのdbs_slaveが要らないんじゃないかと考えています。dbmとdbs_slaveの使い分けを考えてコード書くの、ちゃんとやると面倒ですよね。

sub dbh {
    DBI->connect("MASTER");
}

sub controller_post {
    $c->dbh->do("insert ..")
}

sub controller_get {
    $c->dbh->selectrow..("select ..")
}

シンプルになるのでサービスの構築も速くなるでしょう。

んで、サービスがかなりヒットして、master一台ではつらいという嬉しい悲鳴があがった時に、初めてdbs_slaveを足して、「スイートスポットだけ」をslave参照にします。

sub dbh {
    DBI->connect("MASTER");
}

sub dbh_slave {
    my $dbh;
    for my $dsn ([SLAVES]) {
        $dbh = DBI->connect($dsn);
        last if $dbh;
    }
}

sub controller_post {
    $c->dbh->do("insert ..")
}

sub controller_get {
    # ここアクセスが多いからslaveを参照する!
    $c->dbh_slave->selectrow..("select ..")
}

実際どこをslave参照にするかは、ちゃんと計測して行うといいですね。

また、こんな感じでシステムを作る事で、参照をslaveに向けた時に起きるであろう、レプリケーション遅延による様々な問題も回避しやすいんじゃないかなと思います。

Starlet + Server::Stater で UNIX domain socketに対応しました

Starlet-0.21がリリースされました。

Starlet-0.21 - a simple, high-performance PSGI/Plack HTTP server - metacpan.org - Perl programming language

このバージョンからServer::Staterとの組み合わせでUNIX domain socketをListenすることができるようになりました。

$ start_server --path /tmp/app.sock -- plackup -s Starlet app.psgi

Server::Stater は以前からUNIX domain socketをサポートしていたのですが、Starlet側でうまくハンドリングできていなかったのを修正したのが今回のリリースです。

ベンチマーク

#plackconでの発表にもある通り、UNIX domain socketを使うとTCPで接続するよりもよいベンチマーク結果が得られます。

f:id:kazeburo:20131126002528p:plain

このベンチマークはEC2のm3.2xlargeを使い、nginxとStarletを以下のように起動してwrkを使って計測しました。

wrkの起動はこんな感じ。

$ ./wrk -c 300 -t 4 -d 10 http://localhost:8080/
TCPの場合

Starletの起動

$ start_server --port 5000 -- carton exec -- plackup -E production \
  -s Starlet --max-reqs-per-child 5000 --min-reqs-per-child 4000 \
  --max-workers 10 -a app.psgi

nginx.conf

upstream apps {
     server 127.0.0.1:5000;
}
server {
    listen 8080;
    server_name _;
    etag off;
    location / {
        proxy_set_header Host $host;
        proxy_pass http://apps;
    }
}
UNIX domain socketの場合

Starletの起動

$ start_server --path /tmp/app.sock -- carton exec -- plackup -E production \
  -s Starlet --max-reqs-per-child 5000 --min-reqs-per-child 4000 \
  --max-workers 10 -a app.psgi

nginx.conf

upstream apps {
     server unix:/tmp/app.sock;
}
server {
    listen 8080;
    server_name _;
    etag off;
    location / {
        proxy_set_header Host $host;
        proxy_pass http://apps;
    }
}

どうぞご利用くださいませ

ISUCON3 予選のAMIでスコア 65000点以上を出す方法 #isucon

ISUCON3 の予選AMIが公開されてから、ごにょごにょとHackした結果、スコアで65000まで出す事ができました。(一回だけ66000でけどたぶんインスタンスガチャ)

ISUCON 本戦出場者決定のお知らせ」をみると予選の時のトップのスコアが3.3万(自分たちは1.5万ぐらい)、その後の解説記事「ざっくりと #isucon 2013年予選問題の解き方教えます」だと、5.6万という話でしたので、その数字よりも良いスコアを出すことができました。

んで、本選終わったのでそのソースコード公開しました。

https://github.com/kazeburo/isucon3qualifier-myhack

構成はnginx + perlです。主なポイントは

  • セッションに直接ユーザIDを埋める
  • / と /recent は静的にHTMLを書き出して、nginxのssiを使ってセッション情報を埋める。
  • 初期化時とmemoのPOST時に、memcachedにJSONでキャッシュする
  • /memo はluaでmemcachedにアクセスして組み立てる
  • 改造Starlet使って、nginxからunix domain socketでproxy

といったあたり。ベンチマークはworkload 2よりも大きくすると遅くなります

$ sudo isucon3 benchmark --init /home/isucon/webapp/perl/conf/init.sh --workload 2
2013/11/11 14:49:42 <<<DEBUG build>>>
2013/11/11 14:49:42 benchmark mode
2013/11/11 14:49:42 initialize data...
2013/11/11 14:49:57 run /home/isucon/webapp/perl/conf/init.sh timeout 60 sec...
2013/11/11 14:50:32 done
2013/11/11 14:50:32 sleeping 5 sec...
2013/11/11 14:50:37 run benchmark workload: 2
2013/11/11 14:51:38 done benchmark
Result:   SUCCESS 
RawScore: 65508.9
Fails:    0
Score:    65508.9

最後まで @methane さんの70kには届かなかったのが残念

How to automate CPAN.pm configuration on Travis

I want to automate CPAN.pm configuration on Travis.
I wrote .travis.yaml like this

language: perl
before_install:
  - cpanm -n CPAN
  - (echo y;echo y;echo o conf commit)|cpan
  - cpanm -n Module::Install Module::Install::Repository Module::Install::AuthorTests Module::Install::CPANfile
perl:
  - 5.18
  - 5.16
  - 5.14

Kossy-0.24 Trial has been released. It contains new BodyParser

I released Kossy-0.24-Trial. It contains new request-body parser Kossy::BodyParser.

https://metacpan.org/release/KAZEBURO/Kossy-0.24-TRIAL

Kossy::BodyParser is based on tokuhirom's Plack::BodyParser
https://github.com/plack/Plack/pull/434

I think that this trial version of Kossy is positioned as verification of the next Plack.

Benchmark

package MyApp;

use strict;
use warnings;
use Kossy;

get "/" => sub {
    my ( $self, $c )  = @_;
    $c->response->body("ok");
};

router [qw/GET POST/] => "/bench" => sub {
    my ( $self, $c )  = @_;
    my $param = $c->req->param('q');
    $c->response->body("ok => " . $param);
};

package main;

use HTTP::Request::Common;
use HTTP::Message::PSGI;
use Plack::Test;
use Plack::Builder;
use Benchmark qw/cmpthese timethese/;

my $env = req_to_psgi(GET '/');
my $env_get = req_to_psgi(GET '/bench?q=HTTP%3A%3ARequest%3A%3ACommon&ie=UTF-8&sourceid=chrome');
my $env_post = req_to_psgi(POST '/bench',
                           [ q=>'HTTP::Request::Common', ie => 'UTF-8', sourceid => 'chrome' ] );
my $app = MyApp->psgi;

warn $Kossy::VERSION;

cmpthese(timethese(0,{
    'kossy_noparam' => sub {
        $app->($env);
    },
    'kossy_get' => sub {
        $app->($env_get);
    },
    'kossy_post' => sub {
        $app->($env_post);
    },
}));

Results

0.23
Benchmark: running kossy_get, kossy_noparam, kossy_post for at least 3 CPU seconds...
 kossy_get:  4 wallclock secs ( 3.26 usr +  0.00 sys =  3.26 CPU) @ 11825.46/s (n=38551)
kossy_noparam:  3 wallclock secs ( 3.23 usr +  0.00 sys =  3.23 CPU) @ 12707.43/s (n=41045)
kossy_post:  3 wallclock secs ( 3.02 usr +  0.00 sys =  3.02 CPU) @ 11577.81/s (n=34965)
                 Rate    kossy_post     kossy_get kossy_noparam
kossy_post    11578/s            --           -2%           -9%
kossy_get     11825/s            2%            --           -7%
kossy_noparam 12707/s           10%            7%            --
0.24
Benchmark: running kossy_get, kossy_noparam, kossy_post for at least 3 CPU seconds...
 kossy_get:  3 wallclock secs ( 3.21 usr +  0.00 sys =  3.21 CPU) @ 12763.24/s (n=40970)
kossy_noparam:  3 wallclock secs ( 3.11 usr +  0.00 sys =  3.11 CPU) @ 13610.29/s (n=42328)
kossy_post:  3 wallclock secs ( 3.29 usr +  0.00 sys =  3.29 CPU) @ 12449.85/s (n=40960)
                 Rate    kossy_post     kossy_get kossy_noparam
kossy_post    12450/s            --           -2%           -9%
kossy_get     12763/s            3%            --           -6%
kossy_noparam 13610/s            9%            7%            --

アプリケーションに手を入れずに #isucon 2013 予選のperlアプリケーションのスコアをあげてみようの巻

ということで、やってみた。

初期

オンライン予選で使用した問題が手元で再現できるAMIを公開しました」に書かれているAMIを使って起動し、まず、初期状態でのベンチマークを取ってみる。

2013/10/28 14:21:18 done benchmark
Result:   SUCCESS 
RawScore: 865.9
Fails:    0
Score:    865.9

disble?

たぶんミスだと思うのですが、Starmanの起動オプションがdisbleになっていたので修正

- command=/home/isucon/env.sh carton exec -- plackup -s Starman --workers 10 --port 5000 -E production --disble-keepalive app.psgi
+ command=/home/isucon/env.sh carton exec -- plackup -s Starman --workers 10 --port 5000 -E production --disable-keepalive app.psgi

supervisorctl reloadしてベンチマークを動かすとかなり結果がよくなった

2013/10/28 14:24:15 done benchmark
Result:   SUCCESS 
RawScore: 1857.9
Fails:    0
Score:    1857.9

Monoceros & OpenFileCache

次にこのエントリで紹介した、Monoceros と Plack::Middleware::Static::OpenFileCacheをいれる。

diff --git a/app.psgi b/app.psgi
index 2fb2ba5..220fd06 100644
--- a/app.psgi
+++ b/app.psgi
@@ -13,7 +13,7 @@ my $root_dir = File::Basename::dirname(__FILE__);
 my $app = Isucon3::Web->psgi($root_dir);
 builder {
     enable 'ReverseProxy';
-    enable 'Static',
+    enable 'Static::OpenFileCache',
         path => qr!^/(?:(?:css|js|img)/|favicon\.ico$)!,
         root => $root_dir . '/public';
     enable 'Session',
diff --git a/cpanfile b/cpanfile
index 25ca66c..21fe478 100644
--- a/cpanfile
+++ b/cpanfile
@@ -9,3 +9,11 @@ requires "DBD::mysql";
 requires "Starman";
 requires "Plack::Session";
 requires "Cache::Memcached::Fast";
+
+requires "Monoceros", 0.26;
+requires "EV";
+requires "Guard";
+requires "Sys::Sendfile";
+requires "Linux::Socket::Accept4";
+requires "Plack::Middleware::Static::OpenFileCache";

HelloWorldなベンチマークではStarmanよりMonocerosの方が速いっていうのと、sendfileによって静的画像のレスポンスがよくなるのを狙った。

supervisord.condを書き換えて

- command=/home/isucon/env.sh carton exec -- plackup -s Starman --workers 10 --port 5000 -E production --disable-keepalive app.psgi
+ command=/home/isucon/env.sh carton exec -- plackup -s Monoceros --workers 10 --port 5000 -E production --disable-keepalive app.psgi

ベンチマーク

2013/10/28 14:35:00 done benchmark
Result:   SUCCESS 
RawScore: 1925.2
Fails:    0
Score:    1925.2

ちょっとスコアあがった。
Starletでも同程度になったので、sendfileは影響ない感じでした。

Kossy 0.23

Kossy 0.23uri_forとResponse->finalizeの最適化が入っているのでアップデート

diff --git a/cpanfile b/cpanfile
index 21fe478..78d7bf9 100644
--- a/cpanfile
+++ b/cpanfile
@@ -1,4 +1,4 @@
-requires "Kossy", 0.19;
+requires "Kossy", 0.23;
 requires "DBIx::Sunny";
 requires "JSON::XS";
 requires "Digest::SHA";

また少しスコアあがる

2013/10/28 14:38:03 done benchmark
Result:   SUCCESS 
RawScore: 2012.7
Fails:    0
Score:    2012.7

Plack::Middleware::Session::Simple

ここで紹介した、セッション周りの改善。keep_emptyを無効にして空のセッションを保存しない、Set-Cookieを行わないようにした

diff --git a/app.psgi b/app.psgi
index 220fd06..2c98b6a 100644
--- a/app.psgi
+++ b/app.psgi
@@ -16,16 +16,12 @@ builder {
     enable 'Static::OpenFileCache',
         path => qr!^/(?:(?:css|js|img)/|favicon\.ico$)!,
         root => $root_dir . '/public';
-    enable 'Session',
-        store => Plack::Session::Store::Cache->new(
-            cache => Cache::Memcached::Fast->new({
-                servers => [ "localhost:11211" ],
-            }),
-        ),
-        state => Plack::Session::State::Cookie->new(
-            httponly    => 1,
-            session_key => "isucon_session",
-        ),
+    enable 'Session::Simple',
+        store => Cache::Memcached::Fast->new({
+            servers => [ "localhost:11211" ],
+        }),
+        httponly => 1,
+        cookie_name => "isucon_session",
+        keep_empty => 0,
     ;
     $app;
 };
diff --git a/cpanfile b/cpanfile
index 78d7bf9..78d3475 100644
--- a/cpanfile
+++ b/cpanfile
@@ -15,5 +15,5 @@ requires "EV";
 requires "Guard";
 requires "Sys::Sendfile";
 requires "Plack::Middleware::Static::OpenFileCache";
+requires "Plack::Middleware::Session::Simple";

もうちょいスコア増えた。

2013/10/28 14:46:10 done benchmark
Result:   SUCCESS 
RawScore: 2131.1
Fails:    0
Score:    2131.1

まとめ

初期 865.9 から 2131.1 と約2.5倍ぐらいの改善となりました!やった!

ちなみに同じインスタンス上で、rubyのアプリケーションを動かした場合のスコアは

2013/10/28 15:05:04 done benchmark
Result:   SUCCESS 
RawScore: 2386.1
Fails:    0
Score:    2386.1

何が違うんだろう..