Erlangでhttpdの負荷発生装置を作ってみた(2008/8/30追記あり)

Apachebench(ab)コマンドというものが存在しますが、アクセスできるURLが一つに限定されてしまうので、実際のアクセスパターンを再現できるツールを作ってみました。(まだPOSTもできないし、cookieの処理もしてません)
apacheの実際のアクセスログ(logformatはcombinedかcommon)を読み込んで、GETのリクエストを取り出して、複数のプロセスから接続する。プログラムを簡単にするため、アクセスログのファイル名は、サーバ名を仮定している。
指定できるパラメータは、アクセスログを置いたディレクトリ名、タイムアウト、プロセスの数、接続のインターバル、keepaliveの間隔など。
テストを実施する場合、apache bench(ab)などと同様、なるべく対象サーバに近いところから実施することをお勧めします。
うまく使えば、hotstandbyのwebサーバのキャッシュを暖めるのにも使えるかも。

  • 実行例
$ erlc manager.erl
$ erlc logreader.erl
$ erlc client.erl
% ls logdir
-rw-rw-r-- 1 user user 1339  828 01:26 a.example.com
-rw-rw-r-- 1 user user 10973  828 01:28 b.example.com
$ erl -noshell -s manager start "logdir" 10000 20 3000 1 -s init stop
  • メインルーチン manager:start/1
-module(manager).
-export([start/1]).

start([RawLogdir, RawTimeout, MaxClients, Interval, KeepAlive])->
        Logdir = atom_to_list(RawLogdir),
        Timeout = list_to_integer(atom_to_list(RawTimeout)),
        MC = list_to_integer(atom_to_list(MaxClients)),
        Intv = list_to_integer(atom_to_list(Interval)),
        KA = list_to_integer(atom_to_list(KeepAlive)),

        {ok, Loglist}=file:list_dir(Logdir),

        attach_reader(Logdir, Loglist),
        Requestqueue = wait_reader([], Loglist),

        attach_clients(MC, Intv, KA),

        dispatchloop(Timeout, Requestqueue).

%%
%% private functions
%%

attach_reader(_Logdir, [])->0;
attach_reader(Logdir, [Hostname|L])->
        Myself = self(),
        spawn(fun() -> logreader:new(Myself, Logdir ++ "/" ++ Hostname, Hostname) end),
        attach_reader(Logdir, L).

wait_reader(Requests, [])-> Requests;
wait_reader(Requests, [_H|List])->
        receive
                {done, Req} ->
                        wait_reader(Req ++ Requests, List);
                _Other ->
                        wait_reader(Requests, [_H|List])
        after 5000 ->
                io:format("wait_reader timeout",[])
        end.

attach_clients(0, _, _)->0;
attach_clients(N, Interval, KeepAlive)->
        Myself = self(),
        spawn(fun() -> client:new(Myself, Interval, KeepAlive) end),
        attach_clients(N - 1, Interval, KeepAlive).

dispatchloop(_Timeout, [])-> done;
dispatchloop(Timeout, [ReqTerm|Rest])->
        {Req} = ReqTerm,
        receive
                {Pid, ready} ->
                        %% io:format("request for ~p: ~p~n", [Pid, Req]),
                        Pid ! {self(), Req},
                        dispatchloop(Timeout, Rest)
        after Timeout ->
                io:format("may be complete", []),
                {done}
        end.
  • ログを読み込むプロセス logreader:new/
-module(logreader).
-compile(export_all).

new(Pid, File, Host)->
        {ok, IO} = file:open(File, read),
        Pid ! {done, loop(Host, IO, [])}.

loop(Host, IO, Acc)->
        case io:get_line(IO, '') of
        eof -> Acc;
        Line ->
                case lists:nth(6, string:tokens(Line, " \"")) of
                        "GET" ->
                                URL = "http://" ++ Host ++ lists:nth(7, string:tokens(Line, " \"")),
                                loop(Host, IO,  [{URL}] ++  Acc );
                        "POST"  ->
                                io:format("POST record dropped~n", []),
                                loop(Host, IO, Acc);
                        _A ->
                                io:format("unmatch record dropped~n", []),
                                loop(Host, IO, Acc)
                end
        end.
  • リクエストの配列から一つとりだして、httpdのサーバに接続するプロセス client
-module(client).
-compile(export_all).

new(ManagerPid, Interval, KeepAlive) ->
        spawn(fun() -> init(ManagerPid, Interval, KeepAlive) end).

init(ManagerPid, Interval, KeepAlive) ->
        inets:start(),

        %% set keepalive (pesistent connection)
        %%
        http:set_options([
                {max_sessions, 100},
                {pipeline_timeout, KeepAlive}
        ]),
        loop(ManagerPid, Interval).

%%
%%
loop(ManagerPid, Interval) ->
        ManagerPid ! {self(), ready},
        try
                receive
                        {_Pid, Request} ->
                                send_req(Request);
                        _ -> donothing
                end
        catch
                Type:Why -> io:format("~p : ~p~n", [Type, Why])
        end,
        %% sleep for Interval msec
        receive
        after Interval ->
                true
        end,
        loop(ManagerPid, Interval).

%%
%%
send_req(Request) ->
        try
                {ok, {{_Version, _RetCode, _ReasonPhrase}, _Headers, _Body}} =  http:request(Request)
        catch
                Type:Why -> io:format("~p : ~p~n", [Type, Why])
        end.

検証しないといけないこと

Erlangのビルドのオプションにもよるが、inetsやhttpのモジュールが実際に並列に動作するかどうかをチェックしないといけない。ダメなら、KeepAliveとかはとりあえずあきらめて、より下位レベルの関数を利用することになるだろう。
(2008/8/30追記
tcpdumpで確認してみたが、ちゃんと並列動作してはいるようだ。
erlの-Sオプションによる動作の違いは、tcpdumpなどからはよくわからない。
テスト環境は、Intel Centrino Duo 1.66GHz, メモリ1GBのノートブックマシンで、Windows XP上でVMware Server 1.0.6を動かし、その上でCentOS5相当のLinux kernel2.6.18-92.1.6.el5 #1 SMPを256MBのメモリイメージで実行。とにかくすばらしく遅い(笑。
Erlangのバージョンなどは以下の実行例(ホスト名などは仮名)を参照

# tcpdump -c 200 host example.com > tcpdumplog &

# erl
Erlang (BEAM) emulator version 5.6.3 [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.6.3  (abort with ^G)
1>q().

# erl -noshell -S8 -s manager start localhost "logs" 3000 5 0 10 -s init stop

:
:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 67.11    0.530908       24132        22         1 futex
  4.73    0.037419          16      2398      2028 close
  4.42    0.034927          43       803       578 open
  3.41    0.027011          26      1033           clock_gettime
  2.90    0.022909          59       388         1 poll
  2.53    0.020008          36       562         1 read
  2.02    0.015993          22       743       587 stat64
  1.39    0.010998        1222         9         2 waitpid
  1.13    0.008935         102        88        46 connect
  1.05    0.008284          56       147           recv
  0.84    0.006658         159        42           send
  0.81    0.006432          73        88           writev
  0.63    0.004988          79        63           getdents64
  0.63    0.004984          74        67           munmap
  0.63    0.004982          16       305           fcntl64
  0.61    0.004797          39       123           setsockopt
  0.51    0.003999          61        66           select
  0.50    0.003959          28       141           fstat64
  0.49    0.003901         390        10           execve
  0.39    0.003062          16       196           mmap2
  0.38    0.003000          23       131           write
  0.38    0.002999          22       137           rt_sigaction
  0.38    0.002976          71        42           mprotect
  0.31    0.002445          27        89           socket
  0.25    0.002001          16       126        58 access
  0.25    0.002000          17       120           getsockopt
  0.25    0.001999          41        49         3 ioctl
  0.25    0.001945         973         2           vfork
  0.13    0.001001          35        29           brk
  0.13    0.001000          71        14           pipe
  0.13    0.001000         143         7           clone
  0.13    0.001000          24        41           bind
  0.13    0.000999         250         4         1 sigreturn
  0.13    0.000998          13        75           rt_sigprocmask
  0.05    0.000415          10        42           recvfrom
  0.02    0.000119           2        54           gettimeofday
  0.00    0.000000           0        15           time
  0.00    0.000000           0         3           getpid
  0.00    0.000000           0         3           kill
  0.00    0.000000           0        14           dup2
  0.00    0.000000           0         3           getppid
  0.00    0.000000           0         3           getpgrp
  0.00    0.000000           0         2           setsid
  0.00    0.000000           0         7           uname
  0.00    0.000000           0         4           _llseek
  0.00    0.000000           0        46           getcwd
  0.00    0.000000           0         1           sigaltstack
  0.00    0.000000           0         3           getrlimit
  0.00    0.000000           0         3           getuid32
  0.00    0.000000           0         3           getgid32
  0.00    0.000000           0         3           geteuid32
  0.00    0.000000           0         3           getegid32
  0.00    0.000000           0        10           set_thread_area
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0         2           set_robust_list
  0.00    0.000000           0        41           getsockname
------ ----------- ----------- --------- --------- ----------------
100.00    0.791051                  8426      3306 total

# head tcpdumplog
13:19:48.234946 IP 10.10.10.10.58500 > www.hoge.com.http: S 3505315617:3505315617(0) win 5840 <mss 1460,sackOK,timestamp 86795622 0,nop,wscale 3>
13:19:48.238005 IP 10.10.10.10.44873 > www.hoge.com.http: S 3499413688:3499413688(0) win 5840 <mss 1460,sackOK,timestamp 86795625 0,nop,wscale 3>
13:19:48.240203 IP 10.10.10.10.60753 > www.hoge.com.http: S 3510435421:3510435421(0) win 5840 <mss 1460,sackOK,timestamp 86795627 0,nop,wscale 3>
13:19:48.242465 IP 10.10.10.10.45900 > www.hoge.com.http: S 3500420864:3500420864(0) win 5840 <mss 1460,sackOK,timestamp 86795629 0,nop,wscale 3>
13:19:48.244827 IP 10.10.10.10.50124 > www.hoge.com.http: S 3495664924:3495664924(0) win 5840 <mss 1460,sackOK,timestamp 86795632 0,nop,wscale 3>
13:19:48.631375 IP www.hoge.com.http > 10.10.10.10.58500: S 320291303:320291303(0) ack 3505315618 win 64240 <mss 1460>
13:19:48.631504 IP 10.10.10.10.58500 > www.hoge.com.http: . ack 1 win 5840
13:19:48.675836 IP 10.10.10.10.58500 > www.hoge.com.http: P 1:108(107) ack 1 win 5840
13:19:48.769805 IP www.hoge.com.http > 10.10.10.10.58500: . ack 108 win 64240
13:19:48.769812 IP www.hoge.com.http > 10.10.10.10.44873: S 1269646951:1269646951(0) ack 3499413689 win 64240 <mss 1460>