Erlang で httpd

Erlang を改めて勉強中。
なんか並列化で遊べそうなことないかなーと思ったので、とりあえず httpd
ベンチとか楽だし。

あ、最近は httpd なんてモジュールが標準で入ったみたいなのでそっち使うと良いのかもしれないですよ。

ソース

とりあえずソース。

並列化前
-module(httptest).
-export([listen/1]).


print(X) ->
    io:format("~p\n", [X]).

recvAll(Sock, Result) ->
    receive
        {tcp, Sock, Bin} ->
            {ok, [Bin|Result]};
        {tcp_closed, Sock} -> {ok, Result}
    end.

recvAll(Sock) ->
    {ok, Result} = recvAll(Sock, []),
    {ok, list_to_binary(lists:reverse(Result))}.

response(Sock) ->
    {ok, Bin} = recvAll(Sock),
    gen_tcp:send(Sock, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello, Erlang"),
    gen_tcp:close(Sock).

acceptLoop(Sock) ->
    {ok, Accept} = gen_tcp:accept(Sock),
    response(Accept),
    acceptLoop(Sock).

listen(Port) ->
    {ok, Sock} = gen_tcp:listen(Port, [binary, {packet, 0}]),
    acceptLoop(Sock).

1> httptest:listen(8080).

してブラウザでアクセスすると "Hello, Erlang" と表示される。

並列化後

acceptLoop を process 分けて何個か走らせる。
Listen Socket に対する accept は複数プロセスから発行しても良いらしい。

-module(httptest).
-export([listen/1]).


print(X) ->
    io:format("~p\n", [X]).

recvAll(Sock, Result) ->
    receive
        {tcp, Sock, Bin} ->
            {ok, [Bin|Result]};
        {tcp_closed, Sock} -> {ok, Result}
    end.

recvAll(Sock) ->
    {ok, Result} = recvAll(Sock, []),
    {ok, list_to_binary(lists:reverse(Result))}.

response(Sock) ->
    {ok, Bin} = recvAll(Sock),
    gen_tcp:send(Sock, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello, Erlang"),
    gen_tcp:close(Sock).

acceptLoop(Sock) ->
    {ok, Accept} = gen_tcp:accept(Sock),
    response(Accept),
    acceptLoop(Sock).

listen(Port) ->
    {ok, Sock} = gen_tcp:listen(Port, [binary, {packet, 0}]),
    lists:map(fun(_) ->
                      spawn(fun() ->
                                    acceptLoop(Sock)
                            end)
              end,
              lists:seq(1, 100)),
    acceptLoop(Sock).

ベンチ

$ ab -c 10 -n 10000 http://127.0.0.1:8080/

してみる。

並列化前
$ ab -c 10 -n 10000 http://127.0.0.1:8081/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        
Server Hostname:        127.0.0.1
Server Port:            8081

Document Path:          /
Document Length:        13 bytes

Concurrency Level:      10
Time taken for tests:   2.999 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      580000 bytes
HTML transferred:       130000 bytes
Requests per second:    3334.00 [#/sec] (mean)
Time per request:       2.999 [ms] (mean)
Time per request:       0.300 [ms] (mean, across all concurrent requests)
Transfer rate:          188.84 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0  30.0      0    2998
Processing:     0    1  16.1      1    1171
Waiting:        0    1  16.1      1    1171
Total:          0    2  34.0      1    2999

Percentage of the requests served within a certain time (ms)
  50%      1
  66%      1
  75%      1
  80%      1
  90%      1
  95%      1
  98%      1
  99%      1
 100%   2999 (longest request)
並列化後

大体 Requests per second が倍まで行かない程度あがっている感じ。
Core2 Duo のマシンなのでコア数分くらいは速くなっている。すげー。

$ ab -c 10 -n 10000 http://127.0.0.1:8080/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        
Server Hostname:        127.0.0.1
Server Port:            8080

Document Path:          /
Document Length:        13 bytes

Concurrency Level:      10
Time taken for tests:   1.680 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      580000 bytes
HTML transferred:       130000 bytes
Requests per second:    5951.62 [#/sec] (mean)
Time per request:       1.680 [ms] (mean)
Time per request:       0.168 [ms] (mean, across all concurrent requests)
Transfer rate:          337.10 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       5
Processing:     0    1  23.6      1    1670
Waiting:        0    1  23.6      1    1670
Total:          0    1  23.6      1    1671

Percentage of the requests served within a certain time (ms)
  50%      1
  66%      1
  75%      1
  80%      1
  90%      1
  95%      1
  98%      2
  99%      3
 100%   1671 (longest request)

はまったところ

Socket からのデータを receive {tcp, Scok, Bin} -> ... で受信する場合は、そのソケットを開いたプロセスじゃないとだめなので、 accept で取れたソケットを spawn で別プロセスに渡すと receive での受信ができない。
そういうときは gen_tcp:recv すれば良いんだと思うんだけども、試していない。
あと、ベンチは別マシンでやらないと意味ないかもね、とも。

まとめ

  • 並列化万歳!
  • Erlang 楽しい