Erlang で分散 ApacheBench もどき

書いてみた。どうも綺麗に書けてる気がしないのだが。(関数型の書き方に慣れてないのか……) ソースコードは末尾に。

HTTP のリクエストを並列に投げる部分はこんな感じで。

  • worker(): manager から URI を受け取って http:request()、結果を manager に戻す
  • manager(): workerプロセスのリストを受け取って、それぞれの workerに仕事をやらせる / 完了したリクエストを数える / timer 処理
  • timer(): start / stop 間の経過時間を計る

複数ノード間で分散処理をするために必要な設定。

  • ホームディレクトリの .erlang.cookie ファイルの内容を同じにしておく。パーミッション 400
  • erl コマンドを -name 付きで起動する。
$ erl -name test
Erlang (BEAM) emulator version 5.5 [source] [async-threads:0] [hipe]

Eshell V5.5  (abort with ^G)
(test@fc5.internal)1> net_adm:ping('test@fc5.internal').
pong

test@fc5.internal というのがノード名。net_adm:ping() でノードが生きているかどうかをチェック。生きていれば pong、死んでいれば (通信できなければ) pang が返る。
この状態で spawn の引数にノード名を付けてやれば、リモートノードでプロセスが生成される。

リモートノードでモジュールを load する方法。

deploy(Nodes) ->
    { Mod, Bin, FName } = code:get_object_code(?MODULE),
    lists:foreach(
      fun(Node) ->
              rpc:call( Node, code, load_binary, [ Mod, FName, Bin ] ),
              rpc:call( Node, ?MODULE, init, [])
      end,
      Nodes ).

code:get_object_code(モジュール名) で得た結果を、rpc:call でリモートノードに投げて code:load_binary() に渡す。これでモジュールがリモートになくても大丈夫。

ということで、実行してみる。

% 2node でモジュール読み込み
1> erab:deploy(['test@fc5.internal', 'test@labs.internal']).
ok

% 2node で実行
2> erab:main(['test@fc5.internal', 'test@labs.internal'], "https://kutani/", { 10, 1000 }).
started
elapsed time: 20.2269 (sec)

% 1node で実行
3> erab:main(['test@labs.internal'], "https://kutani/", { 10, 1000 }). started
elapsed time: 39.5101 (sec)

% node 追加
4> erab:deploy(['test@fc6.internal']). 

% 3node で実行
5> erab:main(['test@fc6.internal', 'test@fc5.internal', 'test@labs.internal'], "https://kutani/", { 10, 1000 }).
started
elapsed time: 13.7471 (sec)

1node で 25 req/sec. 3node で 77 req/sec.

サーバのログには以下のように、3ホストからリクエストが来てる様子が残っている。

192.168.0.142 - - [29/May/2007:17:33:36 +0900] "GET / HTTP/1.1" 403 5044
192.168.0.115 - - [29/May/2007:17:33:36 +0900] "GET / HTTP/1.1" 403 5044
192.168.0.149 - - [29/May/2007:17:33:36 +0900] "GET / HTTP/1.1" 403 5044
192.168.0.142 - - [29/May/2007:17:33:36 +0900] "GET / HTTP/1.1" 403 5044
192.168.0.115 - - [29/May/2007:17:33:36 +0900] "GET / HTTP/1.1" 403 5044
192.168.0.149 - - [29/May/2007:17:33:36 +0900] "GET / HTTP/1.1" 403 5044
192.168.0.142 - - [29/May/2007:17:33:36 +0900] "GET / HTTP/1.1" 403 5044

……で、これでサーバの CPU を100%使わせられたか、というと。
ab は単独で 60req/sec 出せたのに、erab では 25req/sec しか出せないこともあって……erlangのノードが 10ぐらい必要そうだという落ちで。そんなに自由になるマシン無いよ。

-module(erab).
-compile(export_all).

worker() ->
    receive
        { URI, Manager } ->
            { ok, _ } = http:request( URI ),
            Manager ! { ok, self() },
            worker();
        die ->
            died
    end.

manager( URI, 0, Timer ) ->
    Timer ! stop,
    Timer ! result,
    finished;
manager( URI, Num, Timer ) ->
    receive
        { start, Clients } ->
            Timer ! start,
            lists:foreach( fun( Client ) ->
                                   Client ! { URI, self() }
                           end, Clients ),
            manager( URI, Num, Timer );
        { ok, Client } ->
            if Num > 1 ->
                    Client ! { URI, self() },
                    manager( URI, Num-1, Timer );
               true ->
                    Client ! die,
                    manager( URI, 0, Timer )
            end
    end.

timer( Start, End ) ->
    receive
        start ->
            timer( erlang:now(), {} );
        stop ->
            timer( Start, erlang:now() );
        result ->
            Diff = timer:now_diff( End, Start ),
            io:format("elapsed time: ~p (sec)~n", [ Diff / 1000000 ])
    end.

run( URI, Clients, N ) ->
    Timer   = spawn( ?MODULE, timer,   [ {}, {} ] ),
    Manager = spawn( ?MODULE, manager, [ URI, N, Timer ] ),
    Manager ! { start, Clients },
    started.

main( Nodes, URI, { C, N } ) ->
    Clients = lists:map(
                fun(Node) ->
                        lists:map(
                          fun(_) ->
                                  spawn(Node, ?MODULE, worker, [])
                          end,
                          lists:seq( 1, C ) )
                end,
                Nodes
               ),
    run( URI, lists:flatten(Clients), N ).

main( URI, { C, N } ) ->
    Clients = lists:map(
                fun(_) ->
                        spawn(?MODULE, worker, [])
                end,
                lists:seq( 1, C )
               ),
    run( URI, Clients, N ).

init() ->
    application:start(inets),
    application:start(ssl).

deploy(Nodes) ->
    { Mod, Bin, FName } = code:get_object_code(?MODULE),
    lists:foreach(
      fun(Node) ->
              rpc:call( Node, code, load_binary, [ Mod, FName, Bin ] ),
              rpc:call( Node, ?MODULE, init, [])
      end,
      Nodes ).