Hateburo: kazeburo hatenablog

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

Starman と Starlet のベンチマークと Accept Serialization

StarmanとStarletの違いはいくつかありますが、Starletにいくつか手を加えたあと、速度はどうなっているのか比較してみた。

なお、以下の記事はHello Worldベンチマークなので、実際のアプリケーションのパフォーマンスにはあまり影響がないと思われます。

各ソフトウェアのバージョンは以下。

Starletのベンチマークとほぼ同じアプリケーションを書いてサーバを起動した

use Plack::Builder;
use Plack::Request;
my $length = 12;
my $body = 'x'x$length;
builder {
    enable 'AccessLog', logger => sub { };
    sub {
        my $env = shift;
        my $req = Plack::Request->new($env);
        my @params = $req->param('foo');   
        [200, ['Content-Type'=>'text/plain','Content-Length'=>$length],[$body]]
    }
}

前回との違いはbodyのサイズ、今回は1200Byteだったのを12Byteと小さくしている。

ベンチマークの環境は、4core/8threadの L5630*2を搭載したサーバ。abを使って別のホストからリクエストを送っている。abを起動したサーバも同じスペック。

abは -c 30 で実行して、Starman、Starletはそれぞれ workers、max-workersの設定を変更してベンチマークを行った。

ab

$ ab -c 30 -n 30000 'http://10.x.x.x:5000/foo?foo=bar&bar=baz&baz=hoge&hoge=foo'

Starman

$ carton exec -- starman --preload-app --workers=16 --max-requests=50000 -a app.psgi

Starlet

$ carton exec -- plackup -s Starlet -E production --max-workers=16 --max-reqs-per-child=50000 -a app.psgi 


下のグラフが結果。

f:id:kazeburo:20130415174210p:plain

Starletの方が全体に request/sec がよく、worker数に応じてスケールしている。Starmanはworker数6で頭打ちになり、それ以降 request/sec が下がってくるという結果になった。

このパフォーマンス低下の原因として考えたのがStarman(Net::Server)が使っている accept(2)のserialization。id:naoya さんの2007年のblogが詳しい。

http://d.hatena.ne.jp/naoya/20070311/1173629378

Starmanでは、accept(2)のserializationにflockを使っている。straceでも確認できる

write(6, "2043 waiting\n", 13)          = 13
flock(7, LOCK_EX)                       = 0
getsockopt(4, SOL_SOCKET, SO_TYPE, [223338299393], [4]) = 0
accept(4, {sa_family=AF_INET, sin_port=htons(44030), sin_addr=inet_addr("10.x.x.x")}, [18350761115241152528]) = 8
ioctl(8, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fffb5f62250) = -1 EINVAL (Invalid argument)
lseek(8, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
ioctl(8, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fffb5f62250) = -1 EINVAL (Invalid argument)
lseek(8, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
fcntl(8, F_SETFD, FD_CLOEXEC)           = 0
flock(7, LOCK_UN)                       = 0
write(6, "2043 processing\n", 16)       = 16
getsockname(8, {sa_family=AF_INET, sin_port=htons(5000), sin_addr=inet_addr("10.x.x.x")}, [4657227515171962896]) = 0
...
read(8, "GET /foo?foo=bar&bar=baz&baz=hoge&hoge=foo HTTP/1.0\r\nHost: 10.x.x.x:5000\r\nUser-Agent: ApacheBench/2.3\r\nAccept: */*\r\n\r\n", 65
536) = 122

acceptの前後でflockしていますね。

Net::Server(::PreforkSimple)ではオプションでserializationの方法が選択できて、none(なし)も選べる。そこでserializationが必要ないパターンをApacheのconfigureを参考にして

diff --git a/lib/Starman/Server.pm b/lib/Starman/Server.pm
index 1c2e08a..73e7e64 100644
--- a/lib/Starman/Server.pm
+++ b/lib/Starman/Server.pm
@@ -68,7 +68,7 @@ sub run {
         port                       => $port,
         host                       => $host,
         proto                      => $proto,
-        serialize                  => 'flock',
+        serialize                  => ( $^O =~ m!(linux|darwin|bsd|cygwin)$! ) ? 'none' : 'flock',
         log_level                  => DEBUG ? 4 : 2,
         ($options->{error_log} ? ( log_file => $options->{error_log} ) : () ),
         min_servers                => $options->{min_servers}       || $workers,

とパッチして、もう一度ベンチマークをとってみたのが次のグラフ。

f:id:kazeburo:20130415174223p:plain

パフォーマンスが落ちる事はなくなったけど、期待するほどの伸びにはならなかった。

ふむー。