Elixir语言中 Supervisor
相关用法介绍如下。
用于实现监督者的行为模块。
监督者是监督其他进程的进程,我们称之为child processes
。主管用于构建称为 supervision tree
的分层流程结构。监督树提供fault-tolerance 并封装我们的应用程序如何启动和关闭。
主管可以通过
直接使用子列表启动,或者您可以定义一个基于模块的主管来实现所需的回调。下面的部分在大多数示例中使用start_link/2
来启动supervisor,但它也包括一个关于基于模块的特定部分。start_link/2
例子
为了启动监督者,我们需要首先定义一个将被监督的子进程。作为示例,我们将定义一个表示堆栈的GenServer:
defmodule Stack do
use GenServer
def start_link(state) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
## Callbacks
@impl true
def init(stack) do
{:ok, stack}
end
@impl true
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
@impl true
def handle_cast({:push, head}, tail) do
{:noreply, [head | tail]}
end
end
堆栈是列表的一个小包装器。它允许我们将一个元素放在栈顶,通过预先添加到列表中,并通过模式匹配获得栈顶。
我们现在可以启动一个主管,它将启动并监督我们的堆栈过程。第一步是定义一个子规范列表,以控制每个子规范的行为方式。每个子规范都是一个映射,如下所示:
children = [
# The Stack is a child started via Stack.start_link([:hello])
%{
id: Stack,
start: {Stack, :start_link, [[:hello]]}
}
]
# Now we start the supervisor with the children and a strategy
{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)
# After started, we can query the supervisor for information
Supervisor.count_children(pid)
#=> %{active: 1, specs: 1, supervisors: 0, workers: 1}
请注意,在启动 GenServer 时,我们使用名称 Stack
注册它,这允许我们直接调用它并获取堆栈中的内容:
GenServer.call(Stack, :pop)
#=> :hello
GenServer.cast(Stack, {:push, :world})
#=> :ok
GenServer.call(Stack, :pop)
#=> :world
但是,我们的堆栈服务器中有一个错误。如果我们调用 :pop
并且堆栈为空,它将崩溃,因为没有子句匹配:
GenServer.call(Stack, :pop)
** (exit) exited in: GenServer.call(Stack, :pop, 5000)
幸运的是,由于服务器由主管监督,主管将自动启动一个新的,初始堆栈为 [:hello]
:
GenServer.call(Stack, :pop)
#=> :hello
主管支持不同的策略;在上面的示例中,我们选择了 :one_for_one
。此外,每个主管可以有许多工人和/或主管作为孩子,每个人都有自己的配置(如"Child specification" 部分所述)。
本文档的其余部分将介绍如何指定子进程、如何启动和停止它们、不同的监督策略等等。
子规格
子规范说明了主管如何启动、关闭和重新启动子进程。
子规范是一个最多包含 6 个元素的映射。以下列表中的前两个键是必需的,其余的是可选的:
-
:id
- 主管内部用于标识子规范的任何术语;默认为给定的模块。在:id
值冲突的情况下,主管将拒绝初始化并要求明确的 ID。此 key 是必需的。 -
:start
- 带有 module-function-args 的元组将被调用以启动子进程。此 key 是必需的。 -
:restart
- 定义终止子进程何时应该重新启动的原子(参见下面的"Restart values" 部分)。此键是可选的,默认为:permanent
。 -
:shutdown
- 定义子进程应如何终止的整数或原子(请参阅下面的 "Shutdown values" 部分)。此键是可选的,如果类型为:worker
则默认为5_000
,如果类型为:supervisor
则默认为:infinity
。 -
:type
- 指定子进程是:worker
或:supervisor
。此键是可选的,默认为:worker
。
还有第六个键 :modules
,它是可选的,很少更改。它是根据:start
值自动设置的。
让我们了解:shutdown
和:restart
选项控制的内容。
关机值 (:shutdown)
:shutdown
选项支持以下关闭值:
-
:brutal_kill
- 使用Process.exit(child, :kill)
无条件立即终止子进程。 -
任何整数 >= 0 - 主管在发出
Process.exit(child, :shutdown)
信号后等待其子级终止的时间量(以毫秒为单位)。如果子进程没有捕获退出,初始的:shutdown
信号将立即终止子进程。如果子进程正在捕获退出,它有给定的时间来终止。如果它没有在指定时间内终止,则子进程将由主管通过Process.exit(child, :kill)
无条件终止。 -
:infinity
- 作为整数工作,但主管将无限期地等待子进程终止。如果子进程是主管,则推荐值为:infinity
,以给主管及其子进程足够的时间关闭。此选项可用于普通工人,但不鼓励这样做并且需要格外小心。如果不小心使用,子进程将永远不会终止,从而阻止您的应用程序也终止。
重启值 (:restart)
:restart
选项控制主管应该认为什么是成功终止。如果终止成功,主管将不会重新启动子进程。如果子进程崩溃,主管将启动一个新进程。
:restart
选项支持以下重启值:
-
:permanent
- 子进程总是重新启动。 -
:temporary
- 无论监督策略如何,子进程都不会重新启动:任何终止(甚至异常)都被认为是成功的。 -
:transient
- 仅当子进程异常终止时才会重新启动,即,退出原因不是:normal
、:shutdown
或{:shutdown, term}
。
要更全面地了解退出原因及其影响,请参阅“退出原因和重新启动”部分。
child_spec/1
在启动主管时,我们传递一个子规范列表。这些规范是说明主管应该如何启动、停止和重新启动其每个子项的映射:
%{
id: Stack,
start: {Stack, :start_link, [[:hello]]}
}
上面的映射定义了一个具有 Stack
的 :id
的子节点,该子节点通过调用 Stack.start_link([:hello])
来启动。
但是,将每个子节点的子规范指定为映射可能很容易出错,因为我们可能会更改 Stack 实现并忘记更新其规范。这就是为什么 Elixir 允许您传递带有模块名称和 start_link
参数的元组而不是规范的原因:
children = [
{Stack, [:hello]}
]
然后主管将调用Stack.child_spec([:hello])
来检索子规范。现在Stack
模块负责构建自己的规范,例如,我们可以这样写:
def child_spec(arg) do
%{
id: Stack,
start: {Stack, :start_link, [arg]}
}
end
幸运的是,use GenServer
已经定义了一个与上面完全相同的 Stack.child_spec/1
。如果您需要自定义
,可以将选项直接传递给 GenServer
use GenServer
:
use GenServer, restart: :transient
最后,请注意,也可以简单地将 Stack
模块作为子模块传递:
children = [
Stack
]
仅给出模块名称时,它等效于 {Stack, []}
。通过将Map规范替换为 {Stack, [:hello]}
或 Stack
,我们使用 use GenServer
定义的默认实现将子规范封装在 Stack
模块中。我们现在可以与其他开发人员共享我们的Stack
worker,他们可以直接将其添加到他们的监督树中,而不必担心 worker 的底层细节。
总体而言,子规范可以是以下之一:
- 表示子规范本身的映射 - 如 "Child specification" 部分所述
- 一个元组,其中一个模块作为第一个元素,开始参数作为第二个元素 - 例如
{Stack, [:hello]}
。在这种情况下,调用Stack.child_spec([:hello])
来检索子规范 - 一个模块 - 例如
Stack
。在这种情况下,调用Stack.child_spec([])
来检索子规范
如果需要将元组或模块子规范转换为映射或修改子规范,可以使用
函数。例如,要使用不同的 Supervisor.child_spec/2
:id
和 10 秒(10_000 毫秒)的 :shutdown
值运行堆栈:
children = [
Supervisor.child_spec({Stack, [:hello]}, id: MyStack, shutdown: 10_000)
]
基于模块的主管
在上面的示例中,通过将监督结构传递给
来启动监督者。但是,也可以通过显式定义监督模块来创建监督者:start_link/2
defmodule MyApp.Supervisor do
# Automatically defines child_spec/1
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
children = [
{Stack, [:hello]}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
这两种方法之间的区别在于,基于模块的监督者可以让您更直接地控制监督者的初始化方式。我们不是使用自动初始化的子项列表调用
,而是通过在其Supervisor.start_link/2
回调中调用init/1
来手动初始化子项。Supervisor.init/2
use Supervisor
还定义了一个 child_spec/1
函数,它允许我们将 MyApp.Supervisor
作为另一个主管的孩子或在您的监督树的顶部运行:
children = [
MyApp.Supervisor
]
Supervisor.start_link(children, strategy: :one_for_one)
一般准则是仅在监督树的顶部使用不带回调模块的监督者,通常在
回调中。我们建议对应用程序中的任何其他主管使用基于模块的主管,以便它们可以作为树中另一个主管的子级运行。 Application.start/2
自动生成的Supervisor
child_spec/1
可以使用以下选项进行自定义:
:id
- 子规范标识符,默认为当前模块:restart
- 当应重新启动主管时,默认为:permanent
紧接在use Supervisor
之前的@doc
注释将附加到生成的child_spec/1
函数。
start_link/2
、 init/2
和策略
start_link/2
init/2
到目前为止,我们已经启动了主管将单个孩子作为元组传递以及一个名为 :one_for_one
的策略:
children = [
{Stack, [:hello]}
]
Supervisor.start_link(children, strategy: :one_for_one)
或从
回调内部:init/1
children = [
{Stack, [:hello]}
]
Supervisor.init(children, strategy: :one_for_one)
和start_link/2
的第一个参数是上面"child_spec/1" 部分中定义的子规范列表。init/2
第二个参数是选项的关键字列表:
-
:strategy
- 监督策略选项。它可以是:one_for_one
、:rest_for_one
或:one_for_all
。必需的。请参阅"Strategies" 部分。 -
:max_restarts
- 在一个时间范围内允许的最大重启次数。默认为3
。 -
:max_seconds
-:max_restarts
适用的时间范围。默认为5
。 -
:name
- 注册主管进程的名称。支持的值在GenServer
策略
监督者支持不同的监督策略(通过 :strategy
选项,如上所示):
-
:one_for_one
- 如果子进程终止,则仅重新启动该进程。 -
:one_for_all
- 如果子进程终止,则所有其他子进程都将终止,然后所有子进程(包括终止的子进程)将重新启动。 -
:rest_for_one
- 如果子进程终止,则终止的子进程和在它之后启动的其余子进程将终止并重新启动。
上面的进程终止是指终止不成功,由:restart
选项决定。
要动态监督孩子,请参阅
。DynamicSupervisor
名称注册
主管与
绑定到相同的名称注册规则。在 GenServer
的文档中阅读有关这些规则的更多信息。GenServer
启动和关闭
当主管启动时,它会遍历所有子规范,然后按照定义的顺序启动每个子规范。这是通过调用子规范中 :start
键下定义的函数来完成的,通常默认为 start_link/1
。
然后为每个子进程调用start_link/1
(或自定义)。 start_link/1
函数必须返回 {:ok, pid}
其中 pid
是链接到主管的新进程的进程标识符。子进程通常通过执行
回调来开始其工作。一般来说,init/1
init
回调是我们初始化和配置子进程的地方。
关闭过程以相反的顺序发生。
当主管关闭时,它会以与列出的相反顺序终止所有孩子。终止是通过 Process.exit(child_pid, :shutdown)
向子进程发送关闭退出信号,然后等待子进程终止的时间间隔来实现的。此时间间隔默认为 5000 毫秒。如果子进程在此时间间隔内没有终止,则主管突然终止子进程,原因是 :kill
。可以在子规范中配置关闭时间,这将在下一节中详细介绍。
如果子进程没有捕获退出,它会在收到第一个退出信号时立即关闭。如果子进程正在捕获退出,则调用terminate
回调,并且子进程必须在合理的时间间隔内终止,然后才被主管突然终止。
换句话说,如果一个进程在你的应用程序或监督树关闭时自行清理很重要,那么这个进程必须捕获出口,并且它的子规范应该指定正确的 :shutdown
值,确保它在合理的范围内终止间隔。
退出原因和重启
主管根据其:restart
配置重新启动子进程。例如,当 :restart
设置为 :transient
时,如果子进程因 :normal
、:shutdown
或 {:shutdown, term}
原因退出,则主管不会重新启动子进程。
那么有人可能会问:退出的时候应该选择哪个退出原因呢?有三个选项:
-
:normal
- 在这种情况下,不会记录退出,在瞬态模式下不会重新启动,并且链接的进程不会退出 -
:shutdown
或{:shutdown, term}
- 在这种情况下,不会记录退出,在瞬态模式下不会重新启动,并且链接进程以相同的原因退出,除非它们正在捕获退出 -
任何其他术语-在这种情况下,将记录退出,在瞬态模式下重新启动,并且链接的进程以相同的原因退出,除非它们正在捕获退出
请注意,达到最大重启强度的主管将退出 :shutdown
原因。在这种情况下,只有在将 :restart
选项设置为 :permanent
(默认值)的情况下定义其子规范时,才会重新启动主管。
相关用法
- Elixir Supervisor.Spec.worker用法及代码示例
- Elixir Supervisor.child_spec用法及代码示例
- Elixir Supervisor.init用法及代码示例
- Elixir Supervisor.Spec用法及代码示例
- Elixir Supervisor.Spec.supervise用法及代码示例
- Elixir Supervisor.Spec.supervisor用法及代码示例
- Elixir StringIO.flush用法及代码示例
- Elixir Stream用法及代码示例
- Elixir String.contains?用法及代码示例
- Elixir String.reverse用法及代码示例
- Elixir Stream.drop_while用法及代码示例
- Elixir String.to_integer用法及代码示例
- Elixir System.stop用法及代码示例
- Elixir Stream.map_every用法及代码示例
- Elixir Stream.iterate用法及代码示例
- Elixir String.pad_trailing用法及代码示例
- Elixir Stream.dedup_by用法及代码示例
- Elixir Stream.interval用法及代码示例
- Elixir String.split_at用法及代码示例
- Elixir String.valid?用法及代码示例
- Elixir String.replace_prefix用法及代码示例
- Elixir String.printable?用法及代码示例
- Elixir Stream.zip用法及代码示例
- Elixir System.fetch_env!用法及代码示例
- Elixir String.replace_trailing用法及代码示例
注:本文由纯净天空筛选整理自elixir-lang.org大神的英文原创作品 Supervisor。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。