supervisor / Erlang World


top > otp > supervisor

supervisor

スーパーバイザーとは

OTPには スーパーバイザー(supervisor) というものがあります。これは、 親プロセスが子プロセスを監視し、子プロセスが健全な状態にあるかをチェックする機能を持っています。

スーパーバイザーは子プロセスを

させるために存在するのです。

スーパーバイザーの本質的な役割は、子供のプロセスを常に動いている状態に置くことです。 たとえば、子プロセスになんらかのエラーが生じて死んでしまった場合は、スーパーバイザーは 問題があった子プロセスを再起動させます

ここでいう子供のプロセスとは、gen_servergen_eventgen_fsmsupervisor などといったものになります。

この節ではサンプルを通して supervisor の基本的な機能を概観してみましょう。詳細な内容については次の節で解説します。 以下に使用するプログラムのコードを記載します。プログラムは少し長いのですが、肝心の supervisor についてはあまり多くありません。 supervisorで管理される子供のプロセスが存在するため長くなっているだけです。

supervosorもgen_serverと同じように、ビヘイビアとコールバックにより構成されています。

サンプルプログラム

メールサーバの機能を持つ。スーパーバイザーであり、子供としてネームサーバとメールボックスを持つ。 子供がクラッシュした際にリスタートさせる。

-module(mail_server).
-export([start/0, create_mailbox/1]).
-export([init/1]).

start() ->
    supervisor:start_link({local,mail_server},spb,[]).

name_server() ->
    ID = name_server,
    StartFunc = {name_server, start, []},
    Restart = permanent,
    Shutdown = brutal_kill,
    Type = worker,
    Modules = [name_server],
    _ChildSpec = {ID, StartFunc, Restart, Shutdown, Type, Modules}.

init(_Args) ->
    RestartStrategy = one_for_one,
    MaxR = 1,
    MaxT = 60,
    ChildSpec = [name_server()],
    {ok, {{RestartStrategy, MaxR, MaxT},ChildSpec}}.

create_mailbox(OwnerName_MailBox) -> 
    case name_server:is_exist(OwnerName_MailBox) of
	true ->
	    io:format("this name is already used");
	false ->
	    ID = OwnerName_MailBox,
	    StartFunc = {mailbox, start, [OwnerName_MailBox]},
	    Restart = permanent,
	    Shutdown = brutal_kill,
	    Type = worker,
	    Modules = [mailbox],
	    ChildSpec = {ID, StartFunc, Restart, Shutdown,Type,Modules},
	    supervisor:start_child(mail_server, ChildSpec)
    end.

ネームサーバとしての機能を持つ。ここでは単純化して名前の登録、削除の管理を行なうのみである。

-module(name_server).
-export([start/0,is_exist/1,add_name/1,remove_name/1]).
-export([init/1, handle_call/3, handle_cast/2]).
-define(REGNAME, name_server).

start() ->
    gen_server:start_link({local, ?REGNAME}, ?MODULE, [],[]).

is_exist(Name) ->
    gen_server:call(?REGNAME,{is_exist, Name}).

add_name(Name) ->
    gen_server:cast(?REGNAME, {add_name,Name}).

remove_name(Name) ->
    gen_server:cast(?REGNAME, {remove_name, Name}).

init(_Args) ->
    {ok,[]}.

handle_call({is_exist, Name}, _ , State) ->
    Bool = lists:member(Name, State),
    {reply, Bool, State}.

handle_cast({add_name, Name}, State) ->
    case lists:member(Name,State) of
       true ->
	    io:format("already registerd mailbox: ~p~n",[Name]),
	    NewState = State;
	false ->
	    io:format("add mailbox ~p~n",[Name]),
	    NewState = [Name | State]
    end,
    {noreply, NewState};

handle_cast({remove_name, Name}, State) ->
    io:format("remove ~p from ~p~n",[Name, State]),
    NewState = State -- Name,
    io:format("owner list:~p~n",[NewState]),
    {noreply, NewState}.

メールボックスと、メーラーの役割を持つプログラム。 メーラは、メールボックスに対して送信依頼をしたり、受信したメールをメールボックスより受け取ったりする。

-module(mailbox).
-export([start/1, start_mailer/1, mailer/1]).
-export([init/1, handle_call/3, handle_cast/2,terminate/2]).

%% mail box
start(OwnerName) ->
    name_server:add_name(OwnerName),
    gen_server:start_link({local, OwnerName}, ?MODULE, [OwnerName],[]).

init(Name) ->
    {ok, {Name,[]}}.

handle_cast({store, Mail},{Name, MailList}) ->
    io:format("receive mail~n",[]),
    {noreply,{Name, [Mail | MailList]}};

handle_cast({send, Destination, Mail}, {Name,MailList}) ->
    case name_server:is_exist(Destination) of
	false ->
	    io:format("~p is not registered~n",[Destination]);
	true ->
	    gen_server:cast(Destination, {store, Mail}),
	    io:format("send mail~n",[])
    end,
    {noreply, {Name,MailList}}.

handle_call(get_mail, _, {Name,MailList}) ->
    {reply, MailList, {Name,[]}}.

terminate(_Reason, {Name,_}) ->
    io:format("destroy mailbox~n", []),
    name_server:remove_name(Name).

%% command interface (easy mailer soft)
start_mailer(OwnerName) ->
    case name_server:is_exist(OwnerName) of
	true ->
	    spawn(?MODULE, mailer, [OwnerName]);
	false ->
	    io:format("~p is not exist",[OwnerName])
    end.

mailer(OwnerName) ->
    receive
	get_mail ->
	    MailList = gen_server:call(OwnerName, get_mail),
	    io:format("~p~n",[MailList]);
  
	{send, Destination, Mail} ->
	    gen_server:cast(OwnerName, {send, Destination, Mail});
	
	destroy -> %for supervisor testing
	    gen_server:cast(OwnerName, unexpected_type)
    end,
    mailer(OwnerName).

一般のメールソフトと構造が違うのですが、単純化のために少し構造を変えました。

実行結果は以下の通りになります。メールサーバの役割は一行目だけになり、 残りの行はメール送受信の実行例です。

1> mail_server:start().
{ok,<0.33.0>}

2> mail_server:create_mailbox(luigi).
add mailbox luigi
{ok,<0.36.0>}
3> Luigi = mailbox:start_mailer(luigi).
<0.38.0>
4> mail_server:create_mailbox(mario).
add mailbox mario
{ok,<0.40.0>}
5> Mario = mailbox:start_mailer(mario).
<0.42.0>

6> Luigi ! {send, mario, "hello"}.
send mail
{send,mario,"hello"}
receive mail
7> Mario ! get_mail.
["hello"]
get_mail

スーパーバイザの起動

それでは、スーパーバイザの解説に入ります。他の部分(name_server, mailbox)については 脱線してしまうため詳しい解説は行ないません。

スーパーバイザは mail_server の start/0 の中で開始されています。

start() ->
    supervisor:start_link({local,mail_server},spb,[]).

これは supervisor を起動させるためのビヘイビアです。書式は次のようになっています。

start_link(Module, Args) -> Result
start_link(SupName, Module, Args) -> Result
SupName = {local,Name} | {global,Name}
Name = atom()
Module = atom()
Args = term()
Result = {ok,Pid} | ignore | {error,Error}
Pid = pid()
Error = {already_started,Pid}} | shutdown | term()
監視木の一部として、スーパーバイザーのプロセスを作成する。Nameは登録する名前、 Moduleはコールバックモジュール、Argsはコールバックに渡される引数である。特に問題無く起動できた場合は、 {ok, Pid} が返される

これは次のような書式のコールバックを呼び出します。

Module:init(Args) -> Result
Args = term()
Result = {ok,{{RestartStrategy,MaxR,MaxT},[ChildSpec]}} | ignore
RestartStrategy = one_for_all | one_for_one | rest_for_one | simple_one_for_one
MaxR = MaxT = int()>=0
ChildSpec = child_spec()
Argsは、supervisor:start/2,3 の最後の引数である Args が渡されます。RestartStrategyは子供のプロセスが 死亡した時の動作を決め、ChildSpecは起動させ監視下に置く子供のプロセスを指定します。

子供プロセスの再起動の方針(RestartStrategy)

init/1の返り値で必要になるRestartStrategyには以下の3タイプがあります。

one_for_oneはあるプロセスが死亡した際に、そのプロセスのみリスタートさせる方針です。 one_for_allはあるプロセスが死亡した際に、兄弟プロセス全てをリスタートさせる方針です。

rest_for_oneは死亡したプロセスより後に起動したもののみを対象とした、 one_for_allだと言えます。前二者に比べてこちらはあまり使いません。

リスタート頻度の設定

MaxRとMaxTは、リスタート頻度に関する指定です。バグが深刻でクラッシュ、リスタートを何度も繰り返してしまう場合は それを止める必要があります。ある頻度以上でリスタートが繰り返されるのであれば、スーパーバイザーごと死亡させてしまうことが可能です。

MaxT秒間にMaxR回再起動されたら、スーパーバイザごと死亡します。

スーパーバイザが死亡した場合、その上位のスーパーバイザで対処を行ないます。 なお、この値を無限の値とすることでどのようなバグであれ永久にリスタートが繰り返されるようにすることも可能です。

子供プロセスの詳細設定(Child Specification)

スーパーバイザで起動させる子供プロセスの指定書式について扱います。他の項目よりも複雑ですが、 仕組み自体は簡単ですので参照しながら作成できれば十分です。

{Id,StartFunc,Restart,Shutdown,Type,Modules}
Id = term()
StartFunc = {M,F,A}
M = F = atom()
A = [term()]
Restart = permanent | transient | temporary
Shutdown = brutal_kill | int()>=0 | infinity
Type = worker | supervisor
Modules = [Module] | dynamic
Module = atom()

Idはsupervisor内でプロセスを認識するために付ける名前です。これは登録済みプロセスと違って、 直接の親であるsupervisor内でのみ利用されます。

StartFuncには起動するプロセスの、モジュールと関数と引数を指定します。

Restartはリスタートの方針です。permanentは常にリスタート、transientはエラー終了の場合にリスタート、 temporaryはリスタートしません。

Shutdownはどのように子プロセスが終了するかについてです。brutal_killは exit(Child, kill) を利用して子プロセスを 殺します。子供がsupervisorであるときは infinity を設定して下さい。

Typeはプロセスの役割を示します。子プロセスがsupervisorであるならsupervisorとして、 残りのgen_server等はworkerとします。

Modulesは gen_event が子プロセスの場合は dynamic を指定します。 それ以外の場合は [ コールバックのモジュール ] として下さい。

スーパーバイザの起動サンプル

スーパーバイザの難しい部分は乗り越えましたので、あとは簡単にサンプルを通して起動部分の解説を行ないます。 以下の部分が起動のコールバック部分です。

必要な値は init/1 の返り値のみですので最初から一つの行にまとめることも可能ですが、丁寧に記述することにより コードの可読性を上げたほうが良いと思われます。

init(_Args) ->
    RestartStrategy = one_for_one,
    MaxR = 1,
    MaxT = 60,
    ChildSpec = [name_server()],
    {ok, {{RestartStrategy, MaxR, MaxT},ChildSpec}}.

ChildSpecは以下のようになっています。

name_server() ->
    ID = name_server,
    StartFunc = {name_server, start, []},
    Restart = permanent,
    Shutdown = brutal_kill,
    Type = worker,
    Modules = [name_server],
    _ChildSpec = {ID, StartFunc, Restart, Shutdown, Type, Modules}.	

スーバーバイザを起動する際に、同時に子プロセスである name_server を起動しているのがわかります。

ここまでの実行結果と、子プロセスを殺したらどうなるかを確認してみましょう。

1> mail_server:start().
{ok,<0.33.0>}
2> whereis(name_server).
<0.34.0>
3> name_server ! undefined_data.

=ERROR REPORT==== 30-Jun-2008::16:06:09 ===
** Generic server name_server terminating 
** Last message in was undefined_data
** When Server state == []
** Reason for termination == 
** {'function not exported',
       [{name_server,terminate,
            [{undef,
                 [{name_server,handle_info,[undefined_data,[]]},
                  {gen_server,handle_msg,5},
                  {proc_lib,init_p,5}]},
             []]},
        {gen_server,terminate,6},
        {proc_lib,init_p,5}]}
undefined_data
4> whereis(name_server).
<0.37.0>

supervisorである mail_server の作成と同時に、子供のプロセスである name_server が起動されていることを1,2行目より確認できます。 3行目でわざと name_server を殺しています。しかし、4行目では新しく name_server がリスタートされているのが伺えます。 プロセス識別子が異なっていますので、殺される前と後とでは別のプロセスだということがわかります。

子プロセスの追加

この節の最後に子プロセスの追加方法を扱いますが、細かい書式はほとんど先ほどのものと同じなので安心して下さい。

子プロセスを追加するには以下の関数を利用します。

start_child(SupRef, ChildSpec) -> Result
SupRef = Name | {Name,Node} | {global,Name} | pid()
Name = Node = atom()
ChildSpec = child_spec() | [term()]
Result = {ok,Child} | {ok,Child,Info} | {error,Error}
Child = pid() | undefined
Info = term()
Error = already_present | {already_started,Child} | term()
SupRefは supervisor の指定です。supervisorの起動時につけた名前か、pidとなります。 ChildSpecは前に扱った物と同じです。

このコードは、メールボックス(子プロセス)を作成するものです。まだ名前が登録されていなければ 子プロセスを作成しているのが分かります。

create_mailbox(OwnerName_MailBox) -> 
    case name_server:is_exist(OwnerName_MailBox) of
	true ->
	    io:format("this name is already used");
	false ->
	    ID = OwnerName_MailBox,
	    StartFunc = {mailbox, start, [OwnerName_MailBox]},
	    Restart = permanent,
	    Shutdown = brutal_kill,
	    Type = worker,
	    Modules = [mailbox],
	    ChildSpec = {ID, StartFunc, Restart, Shutdown,Type,Modules},
	    supervisor:start_child(mail_server, ChildSpec)
    end.

以上で supervisor の入門は終わりです。次の節ではより詳細に見ていきます。


Yuichi ITO. All rights reserved.
mail to : ad
inserted by FC2 system