背景
ErlangVM は堅牢だといわれています。
実際社内で運用している Erlang 製のゲーム課金・認証基盤は弊社の全ゲームからのアクセスを数年間落ちずにさばいています。
ここまで頑丈だと逆にどうやれば落とすことができるのかに興味が行くのは自然なことです。
今回は ErlangVMの落とし方を学ぶため、実際に落とすためのアプリケーションを Elixir で書いてみます。
Supervisorの再起動制限
Supervisor の起動時オプションには max_restarts と max_seconds というオプションがあります。
max_restarts: 制限時間内に再起動する回数の上限 デフォルトは3
max_seconds: max_restartsを指定する制限時間 デフォルトは5
どうやら再起動回数には上限があり、デフォルトでは5秒以内に3回再起動したら Supervisor はそれ以上再起動せず沈黙するということのようです。
これを使えば ErlangVM を落とせそうです。
サンプルアプリケーション
実際にErlangVMを落とすためのアプリケーションを書いてみます。
defmodule SupRestartLimit.Application do
@moduledoc false
use Application
def start(_type, _args) do
worker_childlen = [
%{id: :worker1, start: {SupRestartLimit.Worker, :start_link, [500]}},
%{id: :worker2, start: {SupRestartLimit.Worker, :start_link, [700]}},
%{id: :worker3, start: {SupRestartLimit.Worker, :start_link, [800]}},
]
children = [
%{
id: SupRestartLimit.Worker.Supervisor,
start: {Supervisor, :start_link, [worker_childlen, [strategy: :one_for_one, restart: :transient, max_restarts: 1, max_seconds: 10]]}
}
]
opts = [strategy: :one_for_one, name: SupRestartLimit.Supervisor, max_restarts: 3, max_seconds: 60]
Supervisor.start_link(children, opts)
end
end
ApplicationとリンクしたトップレベルのSupervisor, その下にWorker用のSupervisor(one_for_one)、その下に Worker(one_for_one)が 3 つあります。
defmodule SupRestartLimit.Worker do
use GenServer
require Logger
@counter_limit 5
def start_link(interval) do
GenServer.start_link(__MODULE__, [interval])
end
@impl GenServer
def init([interval]) do
state = %{counter: 0, interval: interval}
Process.send_after(self(), :countup, interval)
{:ok, state}
end
@impl GenServer
def handle_info(:countup, %{counter: counter, interval: interval}=state) do
Logger.debug "count_up: #{counter}"
if counter >= @counter_limit do
raise "counter limit exceeded"
else
Process.send_after(self(), :countup, interval)
end
new_state = put_in(state.counter, counter + 1)
{:noreply, new_state}
end
def handle_info(_, state), do: {:noreply, state}
end
Worker は start_link の引数で指定された interval ごとに自身のstateの:counter
をカウントアップし、5回カウントアップすると例外を投げます。
再起動戦略は :transient
なので Supervisor による再起動がかかります。
実行
ちゃんとVMごと落とすために iex -S mix
による実行ではなくパッケージして実行します。
-> mix release
Compiling 1 file (.ex)
==> Assembling release..
==> Building release sup_restart_limit:0.1.0 using environment dev
==> You have set dev_mode to true, skipping archival phase
==> Release successfully built!
You can run it in one of the following ways:
Interactive: _build/dev/rel/sup_restart_limit/bin/sup_restart_limit console
Foreground: _build/dev/rel/sup_restart_limit/bin/sup_restart_limit foreground
Daemon: _build/dev/rel/sup_restart_limit/bin/sup_restart_limit start
-> _build/dev/rel/sup_restart_limit/bin/sup_restart_limit foreground
15:28:46.101 [debug] count_up: 0
15:28:46.301 [debug] count_up: 0
15:28:46.401 [debug] count_up: 0
15:28:46.602 [debug] count_up: 1
15:28:47.002 [debug] count_up: 1
-- 中略 ---
15:29:02.228 [debug] count_up: 1
15:29:02.425 [debug] count_up: 5
15:28:48.607 [error] GenServer #PID<0.564.0> terminating
** (RuntimeError) counter limit exceeded
(sup_restart_limit) lib/sup_restart_limit/worker.ex:23: SupRestartLimit.Worker.handle_info/2
(stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:686: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: :countup
State: %{counter: 5, interval: 500}
=CRASH REPORT==== 3-Dec-2017::15:28:48 ===
crasher:
initial call: Elixir.SupRestartLimit.Worker:init/1
pid: <0.564.0>
registered_name: []
exception error: #{'__exception__' => true,
'__struct__' => 'Elixir.RuntimeError',
message => <<"counter limit exceeded">>}
in function 'Elixir.SupRestartLimit.Worker':handle_info/2 (lib/sup_restart_limit/worker.ex, line 23)
in call from gen_server:try_dispatch/4 (gen_server.erl, line 616)
in call from gen_server:handle_msg/6 (gen_server.erl, line 686)
ancestors: [<0.563.0>,'Elixir.SupRestartLimit.Supervisor',<0.561.0>]
message_queue_len: 0
messages: []
links: [<0.563.0>]
dictionary: []
trap_exit: false
status: running
heap_size: 610
stack_size: 27
reductions: 808
neighbours:
=SUPERVISOR REPORT==== 3-Dec-2017::15:28:49 ===
Supervisor: {<0.563.0>,'Elixir.Supervisor.Default'}
Context: child_terminated
Reason: {#{'__exception__' => true,
'__struct__' => 'Elixir.RuntimeError',
message => <<"counter limit exceeded">>},
[{'Elixir.SupRestartLimit.Worker',handle_info,2,
[{file,"lib/sup_restart_limit/worker.ex"},{line,23}]},
{gen_server,try_dispatch,4,
[{file,"gen_server.erl"},{line,616}]},
{gen_server,handle_msg,6,
[{file,"gen_server.erl"},{line,686}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,247}]}]}
Offender: [{pid,<0.565.0>},
{id,worker2},
{mfargs,{'Elixir.SupRestartLimit.Worker',start_link,[700]}},
{restart_type,permanent},
{shutdown,5000},
{child_type,worker}]
=SUPERVISOR REPORT==== 3-Dec-2017::15:28:49 ===
Supervisor: {<0.563.0>,'Elixir.Supervisor.Default'}
Context: shutdown
Reason: reached_max_restart_intensity
Offender: [{pid,<0.565.0>},
{id,worker2},
{mfargs,{'Elixir.SupRestartLimit.Worker',start_link,[700]}},
{restart_type,permanent},
{shutdown,5000},
{child_type,worker}]
=SUPERVISOR REPORT==== 3-Dec-2017::15:28:49 ===
Supervisor: {local,'Elixir.SupRestartLimit.Supervisor'}
Context: child_terminated
Reason: shutdown
Offender: [{pid,<0.563.0>},
{id,'Elixir.SupRestartLimit.Worker.Supervisor'},
{mfargs,
{'Elixir.Supervisor',start_link,
[[#{id => worker1,
start =>
{'Elixir.SupRestartLimit.Worker',
start_link,
[500]}},
#{id => worker2,
start =>
{'Elixir.SupRestartLimit.Worker',
start_link,
[700]}},
#{id => worker3,
start =>
{'Elixir.SupRestartLimit.Worker',
start_link,
[800]}}],
[{strategy,one_for_one},
{restart,transient},
{max_restarts,1},
{max_seconds,10}]]}},
{restart_type,permanent},
{shutdown,5000},
{child_type,worker}]
子の SupRestartLimit.Worker の再起動制限を超え、親である SupRestartLimit.Worker.Supervisor が shutdownされました。 その結果トップレベルの SupRestartLimit.Supervisor によって再起動されました。
さらに続けます。
=SUPERVISOR REPORT==== 3-Dec-2017::15:29:40 ===
Supervisor: {local,'Elixir.SupRestartLimit.Supervisor'}
Context: shutdown
Reason: reached_max_restart_intensity
Offender: [{pid,<0.599.0>},
{id,'Elixir.SupRestartLimit.Worker.Supervisor'},
{mfargs,
{'Elixir.Supervisor',start_link,
[[#{id => worker1,
start =>
{'Elixir.SupRestartLimit.Worker',
start_link,
[500]}},
#{id => worker2,
start =>
{'Elixir.SupRestartLimit.Worker',
start_link,
[700]}},
#{id => worker3,
start =>
{'Elixir.SupRestartLimit.Worker',
start_link,
[800]}}],
[{strategy,one_for_one},
{restart,transient},
{max_restarts,1},
{max_seconds,10}]]}},
{restart_type,permanent},
{shutdown,5000},
{child_type,worker}]
15:29:40.020 [info] Application sup_restart_limit exited: shutdown
{"Kernel pid terminated",application_controller,"{application_terminated,sup_restart_limit,shutdown}"}
Kernel pid terminated (application_controller) ({application_terminated,sup_restart_limit,shutdown})
Crash dump is being written to: erl_crash.dump...done
成功です。
ついにトップレベルSupervisorの再起動制限も超え、アプリケーションが終了し、ErlangVMもerl_crush.dumpを吐いてクラッシュしました。
まとめ
- Supervisor は万能ではありません 専用の Supervisor の下にいるからといって無制限にプロセスをクラッシュしていいわけではありません。
再起動回数には上限があり、トップレベルの Supervisor の再起動制限を超えれば ErlangVM ごと落ちます。