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 8月 28 01:26 a.example.com -rw-rw-r-- 1 user user 10973 8月 28 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>