当前位置: 首页>>代码示例 >>用法及示例精选 >>正文


Elixir Supervisor用法及代码示例


Elixir语言中 Supervisor 相关用法介绍如下。

用于实现监督者的行为模块。

监督者是监督其他进程的进程,我们称之为child processes。主管用于构建称为 supervision tree 的分层流程结构。监督树提供fault-tolerance 并封装我们的应用程序如何启动和关闭。

主管可以通过 start_link/2 直接使用子列表启动,或者您可以定义一个基于模块的主管来实现所需的回调。下面的部分在大多数示例中使用 start_link/2 来启动supervisor,但它也包括一个关于基于模块的特定部分。

例子

为了启动监督者,我们需要首先定义一个将被监督的子进程。作为示例,我们将定义一个表示堆栈的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 函数。

到目前为止,我们已经启动了主管将单个孩子作为元组传递以及一个名为 :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 init/2 的第一个参数是上面"child_spec/1" 部分中定义的子规范列表。

第二个参数是选项的关键字列表:

  • :strategy - 监督策略选项。它可以是 :one_for_one:rest_for_one:one_for_all 。必需的。请参阅"Strategies" 部分。

  • :max_restarts - 在一个时间范围内允许的最大重启次数。默认为 3

  • :max_seconds - :max_restarts 适用的时间范围。默认为 5

  • :name - 注册主管进程的名称。支持的值在 GenServer 文档的 "Name registration" 部分进行了说明。可选的。

策略

监督者支持不同的监督策略(通过 :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-lang.org大神的英文原创作品 Supervisor。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。