使うのは簡単だが、設計は難しい。
スーパーバイザでプロセスを監視することで、ゾンビプロセスの発生を防ぎ、メモリリークを防げる。
また、監視下に置かれたアプリケーションの終了処理を正常な順番で行えるようになる。
トップにあるスーパーバイザをシャットダウンすると、その子プロセスに終了処理が伝播していく。
よってプロセス監視ツリーには以下の要素が必要
- スーパーバイザープロセス
- 他のスーパーバイザとワーカープロセスを監視する
- 監視されるワーカープロセス
- ワーカーは常にスーパーバイザの監視下で使用される
- 依存関係の指定方法
- 子プロセスに対する再起動戦略
- 再起動を断念するタイミング
- 無限に再起動するのを防ぐ
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_all (1対全部)
- 1つがクラッシュしたら、他の全てのプロセスを含め全て再起動する
- 監視下のプロセスが互いに依存している場合はこれ
rest_for_one
- あるプロセスがクラッシュした場合に、そのプロセスより後に起動されたプロセスも含めて再起動する
- チェーンのようにプロセスが依存している場合はこれ
- 例えば AがBを起動してBがCを起動するとき
- BがクラッシュしたらBとCを再起動する
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(暫定的)
- 永続と一時の中間の扱い
- 正常終了するまでは常に再起動
- つまり異常終了の場合は再起動
- 正常終了後は再起動しない
- permanet(永続)
- 子プロセスが死んだ時の処理方法をスーパーバイザに伝える
- Shutdown
- シャットダウン処理時の待機時間
-
infinite
で無期限にもできる
-
- 指定した時間をすぎると
exit(Pid, kill)
が送信されプロセスが強制終了される
- シャットダウン処理時の待機時間
- Type
- 子プロセスのタイプを指定する
- supervisor
- 子のスーパーバイザ
- supervisor_bridge
- ?
- worker
- 上記2つ以外のOTPプロセス
- supervisor
- 子プロセスのタイプを指定する
- 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