ツチノコブログのWEBサーバベンチマークツール比較の記事で紹介されていた。WebサーバのG-WAN。この記事によると凄く速いようです。

Intel Xeon E5-2640 (6コア/12スレッド 2.50GHz) を2つというサーバで

gwan  334944 req/s
nginx 111842 req/s

と、速いと言われているnginxの3倍の速度を出しています。

このベンチマーク結果がとても気になったので、なぜG-WANが速いのか、自分でも検証してみました。

結論から言うと以下の2つ。

1) G-WANはデフォルトで物理CPUに合わせた数のスレッドを起動する
2) HTMLファイルも一度読み込んでキャッシュする

という事です。

今回はAWSのcc2.8xlarge(E5-2670 8コア/16スレッド 2.60GHz *2)を使ってベンチマークを行いました。OSはAmazon Linuxです。

G-WANの起動とベンチマーク

G-WANはソースコードが公開されていないので、バイナリをダウンロードしてきて起動します

$ wget http://gwan.com/archives/gwan_linux64-bit.tar.bz2
$ tar jxf gwan_linux64-bit.tar.bz2 
$ cd gwan_linux64-bit
$ ./gwan

これで8080でListenされるはずなのですが、なぜか分かりませんが8080じゃなくランダムなポートが割り振られていました。 ここのところはもっと検証する必要があるかもしれませんが、ソースコード公開されてないし、パス。

pstreeコマンドでみると

$ pstree $(pgrep gwan)
gwan───16*[{gwan}]

となり、デフォルトで16個のスレッドが起動されてました。

ドキュメントルートディレクトリ以下に”hello world”と書かれたファイルを設置してwrkを使ってベンチマークすると

$ ./wrk -c 300 -t 4 -d 10 http://localhost:40832/helloworld.html
Running 10s test @ http://localhost:40832/helloworld.html
  4 threads and 300 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
   Latency   511.46us  165.04us  17.13ms   89.64%
  Req/Sec    78.63k     8.37k   98.00k    68.01%
  2957522 requests in 10.00s, 744.62MB read
 Requests/sec: 295814.08
 Transfer/sec:     74.48MB

となりました。ツチノコブログと大体同程度、30万req/sec速いですね!

nginxの起動とベンチマーク

nginxはyumでいれました。

$ sudo yum install nginx

この状態で、service nginx start してベンチマークを行うと

$ ./wrk -c 300 -t 4 -d 5 http://localhost/helloworld.html
Running 5s test @ http://localhost/helloworld.html
  4 threads and 300 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     5.94ms   49.24ms   3.60s    99.00%
    Req/Sec     8.61k     2.27k   15.78k    65.09%
  162163 requests in 5.00s, 34.94MB read
  Socket errors: connect 0, read 0, write 0, timeout 116
Requests/sec:  32440.86
Transfer/sec:      6.99MB

なんか遅いですね。timeoutのエラーも出てるし

この時のnginx.confの主な箇所を抜き出すと

worker_processes 1;
events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    access_log off;
    sendfile        on;
    keepalive_timeout  65;
    access_log off;
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }
    }
}

となっています。デフォルトと異なるのは access_log を offにしている点

nginxのデフォルトは、worker_processes が 1 なので、32個CPUが見えても 1個しかCPUを利用しません。最初から16個使うG-WANとここが大きく異なります。

nginxのチューニング

これは差が付き過ぎ。ということでnginxのチューングを行います。

スレッド数

G-WANのスレッドと同じ数のプロセスを起動します

worker_processes 16;
worker_cpu_affinity 0000000000000001 0000000000000010 0000000000000100 0000000000001000 0000000000010000 0000000000100000 0000000001000000 0000000010000000 0000000100000000 0000001000000000 0000010000000000 0000100000000000 0001000000000000 0010000000000000 0100000000000000 1000000000000000;

acceptのタイミング調整

acceptシリアライズ化のためのmutexが確保出来なかった際に、retryするまでの待機時間だそうです。デフォルトは500msec。

accept_mutex_delay 100ms;

keepalive

1接続中に何回のリクエストまで受けるかの設定、nginxのデフォルトは 100 しかないので増やします

keepalive_requests 500000;

これでベンチマークを行うと、

$ ./wrk -c 300 -t 4 -d 10 http://localhost/helloworld.html
Running 10s test @ http://localhost/helloworld.html
  4 threads and 300 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   525.74us  218.71us   5.46ms   74.17%
    Req/Sec    85.20k     9.04k   98.44k    87.22%
  3213990 requests in 10.00s, 695.78MB read
Requests/sec: 321432.91
Transfer/sec:     69.59MB

この時点でG-WANを少し超えました。

ファイルIOレス化

G-WANに対してアクセスをかけながらstraceで見ていて、一つ気になったことがあります。

accept4(53, 0x7ff3982dce10, [16], SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
epoll_wait(47, {{EPOLLIN, {u32=7831200, u64=7831200}}}, 1024, 1000) = 1
accept4(53, {sa_family=AF_INET, sin_port=htons(34988), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_NONBLOCK) = 70
setsockopt(70, SOL_TCP, TCP_NODELAY, [1], 4) = 0
epoll_ctl(47, EPOLL_CTL_ADD, 70, {EPOLLIN|EPOLLPRI|0x2000, {u32=70, u64=70}}) = 0
accept4(53, {sa_family=AF_INET, sin_port=htons(35008), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_NONBLOCK) = 113
setsockopt(113, SOL_TCP, TCP_NODELAY, [1], 4) = 0
epoll_ctl(47, EPOLL_CTL_ADD, 113, {EPOLLIN|EPOLLPRI|0x2000, {u32=113, u64=113}}) = 0
epoll_wait(47, {{EPOLLIN, {u32=7831200, u64=7831200}}, {EPOLLIN, {u32=70, u64=70}}, {EPOLLIN, {u32=113, u64=113}}}, 1024, 1000) = 3
read(113, "GET /helloworld.html HTTP/1.1\r\nH"..., 4064) = 56
writev(113, [{"HTTP/1.1 200 OK\r\nServer: G-WAN\r\n"..., 251}, {"Hello World\n\n", 13}], 2) = 264
read(70, "GET /helloworld.html HTTP/1.1\r\nH"..., 4064) = 56
writev(70, [{"HTTP/1.1 200 OK\r\nServer: G-WAN\r\n"..., 251}, {"Hello World\n\n", 13}], 2) = 264
accept4(53, {sa_family=AF_INET, sin_port=htons(35017), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_NONBLOCK) = 187
setsockopt(187, SOL_TCP, TCP_NODELAY, [1], 4) = 0
epoll_ctl(47, EPOLL_CTL_ADD, 187, {EPOLLIN|EPOLLPRI|0x2000, {u32=187, u64=187}}) = 0
accept4(53, 0x7ff3982dce10, [16], SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
epoll_wait(47, {{EPOLLIN, {u32=70, u64=70}}, {EPOLLIN, {u32=113, u64=113}}, {EPOLLIN, {u32=187, u64=187}}}, 1024, 1000) = 3
read(187, "GET /helloworld.html HTTP/1.1\r\nH"..., 4064) = 56
writev(187, [{"HTTP/1.1 200 OK\r\nServer: G-WAN\r\n"..., 251}, {"Hello World\n\n", 13}], 2) = 264
read(113, "GET /helloworld.html HTTP/1.1\r\nH"..., 4064) = 56
writev(113, [{"HTTP/1.1 200 OK\r\nServer: G-WAN\r\n"..., 251}, {"Hello World\n\n", 13}], 2) = 264
read(70, "GET /helloworld.html HTTP/1.1\r\nH"..., 4064) = 56
writev(70, [{"HTTP/1.1 200 OK\r\nServer: G-WAN\r\n"..., 251}, {"Hello World\n\n", 13}], 2) = 264
epoll_wait(47, {{EPOLLIN, {u32=187, u64=187}}}, 1024, 1000) = 1
read(187, "GET /helloworld.html HTTP/1.1\r\nH"..., 4064) = 56
writev(70, [{"HTTP/1.1 200 OK\r\nServer: G-WAN\r\n"..., 251}, {"Hello World\n\n", 13}], 2) = 264
epoll_wait(47, {{EPOLLIN, {u32=187, u64=187}}}, 1024, 1000) = 1
read(187, "GET /helloworld.html HTTP/1.1\r\nH"..., 4064) = 56
writev(187, [{"HTTP/1.1 200 OK\r\nServer: G-WAN\r\n"..., 251}, {"Hello World\n\n", 13}], 2) = 264

なんだか分かりますか?

接続を受けてリクエストを読んでレスポンスしてを繰り返していますが、この間にhelloworld.htmlを読み込む動作がありません。おそらく一度メモリに読み込んでファイルIOをしないようになっているのでしょう。

nginxでもファイルのIOを減らしてあげる事で高速化するかもということでもう2つnginx.confにパラメータを追加。

ファイルディスクリプタのキャッシュ

open_file_cache を使うと一度開いたファイルディスクリプタを再利用できます

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        open_file_cache max=100;
    }

TCP_CORKによるデータ書き出しタイミングの変更

tcp_nopush を使います。

tcp_nopush on;

nginxはデフォルトでファイルを送り出す際にsendfileシステムコールを使いますが、sendfileの前に動くHTTPヘッダのパケット書き出しを遅延させ、ヘッダとファイルの中身を一度に送り出すように調整します

これで再度ベンチマークを行うと、

$ ./wrk -c 300 -t 4 -d 10 http://localhost/helloworld.html
Running 10s test @ http://localhost/helloworld.html
  4 threads and 300 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   510.32us  382.89us  13.19ms   84.26%
    Req/Sec   102.05k    17.80k  125.89k    60.65%
  3820481 requests in 10.00s, 827.07MB read
Requests/sec: 382102.47
Transfer/sec:     82.72MB

さらに速くなりました。G-WANよりも20%ぐらい高速です。

まとめ

速い遅いには理由がある。psやstraceだけでもいろんなことが分かりますね。

G-WANって何が嬉しいのかというと、Cでハンドラを書いてそれがそれなり高速に動作する & 内蔵のKVSやグラフ描画などWebで便利なAPIが用意されているってことなんだろうけど、今やmod_mrubyやnginx-lua、LLのアプリケーションサーバでもそれなりの速度がでるので使いどころは少ないだろうなというのが個人的感想。なによりソースコードが公開されていないので問題が起きたときの調査に難がありますね。

ちなみにこのサーバでStarletを動かすと、

./wrk -c 32 -t 8 -d 10 http://localhost:5000/
Running 10s test @ http://localhost:5000/
  8 threads and 32 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   152.96us   91.59us   3.88ms   89.21%
    Req/Sec    22.96k     2.71k   37.11k    77.09%
  1729085 requests in 10.00s, 227.56MB read
Requests/sec: 172989.08
Transfer/sec:     22.77MB

G-WANやnginxほどじゃないけどやっぱり速い。

このブログ記事について

このページは、Masahiro Naganoが2013年9月 4日 10:08に書いたブログ記事です。

ひとつ前のブログ記事は「Starlet が HTTP/1.1 に対応しました / wrkによるベンチマークとYAPC::Asiaのトーク宣伝」です。

次のブログ記事は「 YAPC::Asia 2013 Tokyo で PSGI/Plack サーバの高速化について発表してきました」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

ウェブページ

OpenID対応しています OpenIDについて
Powered by Movable Type 4.27-ja