LoginSignup
4
3

More than 5 years have passed since last update.

すごいErlangゆかいに学んだメモ 17章 スーパーバイザー

Posted at

使うのは簡単だが、設計は難しい。
スーパーバイザでプロセスを監視することで、ゾンビプロセスの発生を防ぎ、メモリリークを防げる。
また、監視下に置かれたアプリケーションの終了処理を正常な順番で行えるようになる。
トップにあるスーパーバイザをシャットダウンすると、その子プロセスに終了処理が伝播していく。

よってプロセス監視ツリーには以下の要素が必要

  • スーパーバイザープロセス
    • 他のスーパーバイザとワーカープロセスを監視する
  • 監視されるワーカープロセス
    • ワーカーは常にスーパーバイザの監視下で使用される
  • 依存関係の指定方法
  • 子プロセスに対する再起動戦略
  • 再起動を断念するタイミング
    • 無限に再起動するのを防ぐ

supervisor ビヘイビアの関数

init/1

ビヘイビア関数は init/1 のみ.

返り値の定義は以下。

{ok, {{RestartStrategy, MaxRestart, MaxTime},[ChildSpecs]}}.
  • RestartStrategy
    • 再起動戦略
  • MaxRestart と MaxTime
    • MaxTime(秒) 以内に MaxRestart回以上再起動された場合、スーパーバイザはシャットダウンされる
  • ChildSpec
    • 子プロセスの仕様を決めるタプルのリスト

実際の返り値の例:

{ok, {{one_for_all, 5, 60},  %% {RestartStrategy, MaxRestart, MaxTime}
      %% [ChildSpecs]
      [{fake_id,
        {fake_mod, start_link, [SomeArg]},
        permanent,
        5000,
        worker,
        [fake_mod]},
       {other_id,
       {event_manager_mod, start_link, []},
        transient,
        infinity,
        worker,
        dynamic}]}}.

再起動戦略について

返り値の RestartStrategy の部分。

one_for_one (1対1)

  • 1つが死んだら、その1つが再起動される
  • 各ワーカーが独立していて、他のプロセスに影響しない場合はこれ

one_for_one

one_for_all (1対全部)

  • 1つがクラッシュしたら、他の全てのプロセスを含め全て再起動する
  • 監視下のプロセスが互いに依存している場合はこれ

one_for_all

rest_for_one

  • あるプロセスがクラッシュした場合に、そのプロセスより後に起動されたプロセスも含めて再起動する
  • チェーンのようにプロセスが依存している場合はこれ
    • 例えば AがBを起動してBがCを起動するとき
    • BがクラッシュしたらBとCを再起動する

rest_for_one

simple_one_for_one

  • 監視下のプロセスが1種類のみで、それを動的に追加したいときに使う
  • 名前ほどシンプルではない

子プロセスの仕様(ChildSpec)

[{fake_id,  % ChildID
  {fake_mod, start_link, [SomeArg]},  % StartFunc
  permanent,    % Restart
  5000,         % Shutdown
  worker,       % Type
  [fake_mod]},  % Modules
 {other_id, {event_manager_mod, start_link, []},
  transient,
  infinity,
  worker,
  dynamic}
  • ChildID
    • 子プロセスの名前
    • デバッグ時などに便利
  • StartFunc
    • スーパーバイザの起動方法を指定するタプル
    • {Mod, Func, Arity} の形式
    • 起動関数をOTP準拠にして、ランタイムの呼び出し元とリンクするべき
      • gen_*:start_link は常に自分のモジュール内でラップして使うべき
  • Restart
    • 子プロセスが死んだ時の処理方法をスーパーバイザに伝える
      • permanet(永続)
        • 常に再起動
        • 長時間可動するプロセスに対して使う
      • temporary(一時的)
        • 再起動しない
        • 失敗することも想定される短命なプロセスに使う
      • transient(暫定的)
        • 永続と一時の中間の扱い
        • 正常終了するまでは常に再起動
          • つまり異常終了の場合は再起動
        • 正常終了後は再起動しない
  • Shutdown
    • シャットダウン処理時の待機時間
      • infinite で無期限にもできる
    • 指定した時間をすぎると exit(Pid, kill) が送信されプロセスが強制終了される
  • Type
    • 子プロセスのタイプを指定する
      • supervisor
        • 子のスーパーバイザ
      • supervisor_bridge
        • ?
      • worker
        • 上記2つ以外のOTPプロセス
  • Modules
    • 子プロセスのビヘイビアが利用するコールバックモジュール
      • 1要素のリストで指定する
    • 事前にモジュールが不明な場合(イベントハンドラ等)は dynamic と指定する

supervisorの関数

supervisor:start_link/2-3

start_link(モジュール, 引数)
start_link(名前, モジュール, 引数)
  • 引数
    • init/1 を呼び出すコールバックモジュール
      • 例えば同じモジュール内にコールバック関数が定義されていたら ?MODULE と指定する
    • コールバック関数に渡す引数
    • {local, 名前} でサーバーの登録名を指定できる
  • 返り値
    • {ok, pid()}
    • ignore
    • {error, startlink_err()}
      • {already_started, pid()}
      • {shutdown, term()}
      • term()

supervisor の動作確認

  • gen_serverのコード
-module(echo_server).
-behavior(gen_server).

%% API
-export([start_link/1, stop/2, echo/2]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).


%% gen_server callbacks
init(Name) ->
    %% 親が子を終了する時にわかるように紐づける
    process_flag(trap_exit, true),
    io:format("echo_server が起動しました:~p~n", [Name]),
    {ok, Name}.

handle_call({echo, Message}, _From, Name) ->
    io:format("echo_server ~p: echo ~p~n", [Name, Message]),
    {reply, ok, Name};
handle_call({stop, Reason}, _From, Name) ->
    {stop, Reason, ok, Name};
handle_call(_Message, _From, State) ->
    {noreply, State}.

handle_cast(_Message, State) ->
    {noreply, State}.

handle_info(Message, State) ->
    io:format("不明なメッセージを受信しました: ~p~n", [Message]),
    {noreply, State}.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

terminate(normal, Name) ->
    io:format("echo_server が終了しました:~p~n", [Name]);
terminate(Reason, Name) ->
    io:format("echo_server ~p~p の理由で終了しました.~n", [Name, Reason]).


%% api functions
start_link(Name) ->
    gen_server:start_link({local, Name}, ?MODULE, Name, []).

stop(Pid, Reason) ->
    gen_server:call(Pid, {stop, Reason}).

echo(Pid, Message) ->
    gen_server:call(Pid, {echo, Message}).
  • supervisor のコード
-module(echo_server_sup).
-behaviour(supervisor).

-export([start_link/3]).
-export([init/1]).

% supervisor api
start_link(RestartStrategy, MaxRestart, MaxTime) ->
    supervisor:start_link({local,?MODULE}, ?MODULE, {RestartStrategy, MaxRestart, MaxTime}).

% callback function
init({simple_one_for_one, _MaxRestart, _MaxTime}) ->
    {ok, {{simple_one_for_one, 3, 60},
        [{dynamic_echo_server_sup,
            {echo_server, start_link, []},
            temporary, 1000, worker, [echo_server]}
        ]}};
init({RestartStrategy, MaxRestart, MaxTime}) ->
    {ok, {{RestartStrategy, MaxRestart, MaxTime},
        [{id_process1,
            {echo_server, start_link, [process1]},
            permanent, 1000, worker, [echo_server]},
         {id_process2,
            {echo_server, start_link, [process2]},
            temporary, 1000, worker, [echo_server]},
         {id_process3,
            {echo_server, start_link, [process3]},
            transient, 1000, worker, [echo_server]},
         {id_process4,
            {echo_server, start_link, [process4]},
            transient, 1000, worker, [echo_server]}
        ]}}.

プロセスの種類は以下

名前 種類
process1 permanent
process2 temporary
process3 transient
process4 transient

one_for_one の動作確認

%% モジュールの読み込み
1> c(echo_server).
{ok,echo_server}
2> c(echo_server_sup).
{ok,echo_server_sup}

%% one_for_one 戦略のスーパーバイザを起動する
3> {ok, Pid} = echo_server_sup:start_link(one_for_one, 5, 60).
echo_server が起動しました:process1
echo_server が起動しました:process2
echo_server が起動しました:process3
echo_server が起動しました:process4
{ok,<0.72.0>}

%% permanent プロセスを終了する
4> echo_server:stop(whereis(process1), normal).
echo_server が終了しました:process1
echo_server が起動しました:process1
ok
5> echo_server:stop(whereis(process1), kill).  
echo_server process1  kill の理由で終了しました.
echo_server が起動しました:process1
ok
6>
=ERROR REPORT==== 23-Mar-2018::01:11:26 ===
** Generic server process1 terminating
** Last message in was {stop,kill}
** When Server state == process1
** Reason for termination ==
** kill
** Client <0.60.0> stacktrace
** [{gen,do_call,4,[{file,"gen.erl"},{line,169}]},
    {gen_server,call,2,[{file,"gen_server.erl"},{line,202}]},
    {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
    {shell,exprs,7,[{file,"shell.erl"},{line,687}]},
    {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
    {shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]

6> echo_server:stop(whereis(process1), exit).
echo_server process1  exit の理由で終了しました.

=ERROR REPORT==== 23-Mar-2018::01:11:35 ===
** Generic server process1 terminating
** Last message in was {stop,exit}
** When Server state == process1
** Reason for termination ==
** exit
** Client <0.60.0> stacktrace
** [{gen,do_call,4,[{file,"gen.erl"},{line,169}]},
    {gen_server,call,2,[{file,"gen_server.erl"},{line,202}]},
    {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
    {shell,exprs,7,[{file,"shell.erl"},{line,687}]},
    {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
    {shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]
echo_server が起動しました:process1
ok

7> echo_server:stop(whereis(process1), shutdown).
echo_server process1  shutdown の理由で終了しました.
echo_server が起動しました:process1
ok

%% temporary プロセスを終了する(再起動しない)
8> echo_server:stop(whereis(process2), exit).  
echo_server process2  exit の理由で終了しました.

=ERROR REPORT==== 23-Mar-2018::01:12:09 ===
** Generic server process2 terminating
** Last message in was {stop,exit}
** When Server state == process2
** Reason for termination ==
** exit
** Client <0.60.0> stacktrace
** [{gen,do_call,4,[{file,"gen.erl"},{line,169}]},
    {gen_server,call,2,[{file,"gen_server.erl"},{line,202}]},
    {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
    {shell,exprs,7,[{file,"shell.erl"},{line,687}]},
    {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
    {shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]
ok

%% transient プロセスを終了する
9> echo_server:stop(whereis(process3), normal).
echo_server が終了しました:process3
ok
10> echo_server:stop(whereis(process4), shutdown).
echo_server process4  shutdown の理由で終了しました.
ok

%% 子プロセスの確認
11> supervisor:which_children(Pid).
[{id_process4,undefined,worker,[echo_server]},
 {id_process3,undefined,worker,[echo_server]},
 {id_process1,<0.84.0>,worker,[echo_server]}]
12> supervisor:count_children(Pid).
[{specs,3},{active,1},{supervisors,0},{workers,3}]

rest_for_one の動作確認

%% モジュールの読み込み
1> c(echo_server).
{ok,echo_server}
2> c(echo_server_sup).
{ok,echo_server_sup}

%% rest_for_one 戦略のスーパーバイザの起動
3> {ok, Pid} = echo_server_sup:start_link(rest_for_one, 2, 60).
echo_server が起動しました:process1
echo_server が起動しました:process2
echo_server が起動しました:process3
echo_server が起動しました:process4
{ok,<0.72.0>}

%% permanent プロセスの終了(後続のプロセスもまきこまれて異常終了したので再起動される)
4> echo_server:stop(whereis(process1), normal).
echo_server が終了しました:process1
echo_server process4  shutdown の理由で終了しました.
ok
echo_server process3  shutdown の理由で終了しました.
echo_server process2  shutdown の理由で終了しました.
echo_server が起動しました:process1
echo_server が起動しました:process3
echo_server が起動しました:process4
5> exit(whereis(process1), kill).
echo_server process4  shutdown の理由で終了しました.
true
echo_server process3  shutdown の理由で終了しました.
echo_server が起動しました:process1
echo_server が起動しました:process3
echo_server が起動しました:process4

%% transient プロセスを終了する
6> echo_server:stop(whereis(process3), normal).
echo_server が終了しました:process3
ok

one_for_all の動作確認

%% モジュールの読み込み
1> c(echo_server).
{ok,echo_server}
2> c(echo_server_sup).
{ok,echo_server_sup}

%% one_for_all スーパーバイザの起動
3> {ok, Pid} = echo_server_sup:start_link(one_for_all, 5, 60).
echo_server が起動しました:process1
echo_server が起動しました:process2
echo_server が起動しました:process3
echo_server が起動しました:process4
{ok,<0.72.0>}

%% permanent プロセスを終了する(全部のプロセスがまきこまれて終了し、再起動される)
4> echo_server:stop(whereis(process1), normal).
echo_server が終了しました:process1
echo_server process4  shutdown の理由で終了しました.
ok
echo_server process3  shutdown の理由で終了しました.
echo_server process2  shutdown の理由で終了しました.
echo_server が起動しました:process1
echo_server が起動しました:process3
echo_server が起動しました:process4

%% transient のプロセスを終了する
5> echo_server:stop(whereis(process3), normal).
echo_server が終了しました:process3
ok

%% transient のプロセスを normal, shutdown 以外で終了する
6> echo_server:stop(whereis(process4), kill).
echo_server process4  kill の理由で終了しました.
echo_server process1  shutdown の理由で終了しました.
ok
echo_server が起動しました:process1
echo_server が起動しました:process3
echo_server が起動しました:process4

=ERROR REPORT==== 23-Mar-2018::01:20:00 ===
** Generic server process4 terminating
** Last message in was {stop,kill}
** When Server state == process4
** Reason for termination ==
** kill
** Client <0.60.0> stacktrace
** [{gen,do_call,4,[{file,"gen.erl"},{line,169}]},
    {gen_server,call,2,[{file,"gen_server.erl"},{line,202}]},
    {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
    {shell,exprs,7,[{file,"shell.erl"},{line,687}]},
    {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
    {shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]

動作確認のまとめ

  • transient なプロセスは shutdown, normal だと再起動されない
    • ただし rest_for_one, one_for_all によって shutdown された場合は再起動される
  • temporary なプロセスは rest_for_one, one_for_all にまきこまれて shutdown しても再起動しない
  • temporary なプロセスが異常終了しても rest_for_one, one_for_all はトリガーしない

simple_one_for_one の動作確認

%% モジュールの読み込み
1> c(echo_server).
{ok,echo_server}
2> c(echo_server_sup).
{ok,echo_server_sup}
3> {ok, Pid} = echo_server_sup:start_link(simple_one_for_one, 5 ,60).
{ok,<0.72.0>}

%% プロセスを動的に追加する
4> supervisor:start_child(echo_server_sup, [process1]).
echo_server が起動しました:process1
{ok,<0.74.0>}
5> supervisor:start_child(echo_server_sup, [process2]).
echo_server が起動しました:process2
{ok,<0.76.0>}
6> supervisor:start_child(echo_server_sup, [process3]).
echo_server が起動しました:process3
{ok,<0.78.0>}

%% プロセスを終了させる
7> supervisor:terminate_child(echo_server_sup, whereis(process1)).
echo_server process1  shutdown の理由で終了しました.
ok

参考リンク

参考文献

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3