当前位置: 首页 > article >正文

Elixir Supervisor

Supervisor 是一个用于实现监督者的模块。

监督者是一个监督其他进程的进程,被监督的进程我们称为子进程。监督者用来构建一个进程层级结构,称为监督树。监督树提供了容错性,并封装了应用的启动和停止。

有两种方式启动监督者,一是通过 start_link/2 ,二是定义一个监督者模块并实现所需的回调。以下大部分示例都是使用前者,但也有使用后者的。

示例

为了启动一个监督者,我们首先需要定义一个将被监督的子进程。作为例子,我们将定义一个 GenServer ,一个通用计数服务器。其他进程可以向这个进程发送消息来读取计数器并增加其值。

免责声明
在实际中我们不会使用 GenServer 来做计数器,相反,如果你需要一个计数器,你会将它作为输入和输出传递给需要它的函数。我们在这里选择计数器作为示例是因为它比较简单,因为我们只关注监督者如何工作。

defmodule Counter do
  use GenServer

  def start_link(arg) when is_integer(arg) do
    GenServer.start_link(__MODULE__, arg, name: __MODULE__)
  end

  ## Callbacks

  @impl true
  def init(counter) do
    {:ok, counter}
  end

  @impl true
  def handle_call(:get, _from, counter) do
    {:reply, counter, counter}
  end

  def handle_call({:bump, value}, _from, counter) do
    {:reply, counter, counter + value}
  end
end

Counterstart_link 上接收的参数会被传递给 init/1 回调,作为计数器的初始值。我们的计数器处理两个操作(称为调用): :get 获取当前计数器的值, :bump 使计数器加上 value 并返回旧的计数器值。

现在我们可以启动一个监督者并监督我们的计数器进程了。第一步是定义一个子节点描述列表,用来控制每个子进程的行为。每个子节点描述是一个 map,如下所示:

children = [
  # The Counter is a child started via Counter.start_link(0)
  %{
    id: Counter,
    start: {Counter, :start_link, [0]}
  }
]

# 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 时,我们通过 name: __MODULE__ 选项使用名称 Counter 注册进程。这让我们可以直接调用它并获取它的值:

GenServer.call(Counter, :get)
#=> 0

GenServer.call(Counter, {:bump, 3})
#=> 0

GenServer.call(Counter, :get)
#=> 3

然而,我们的计数器服务器有一个问题。如果我们用一个非数字值调用 :bump ,它就会崩溃:

GenServer.call(Counter, {:bump, "oops"})
** (exit) exited in: GenServer.call(Counter, {:bump, "oops"}, 5000)

幸运的是,由于服务器被监督者监督,监督者会自动启动一个新的服务,重置初始值为 0 :

GenServer.call(Counter, :get)
#=> 0

监督者支持不同的策略;在上述示例中,我们选择了 :one_for_one 。此外,每个监督者可以有多个工作进程或监督者作为子进程,每个都有自己的配置。

接下来我们将讨论如何指定子进程,如何启动和停止它们,不同的监督策略等。

子进程描述

子进程描述描述了监督者如何启动、关闭和重启子进程。

子进程描述是一个包含多达6个元素的 map。如下所示,前两个是必需的,其余的是可选的:

  • :id - 用于在监督者内部唯一标识一个子进程描述的值,可以是任意类型,默认是模块名。此项是必须的。对于监督者,如果 :id 的值冲突了,它会拒绝初始化并要求指定ID。动态监督者则不是这样。
  • :start - 一个包含模块-函数-参数的元组用来启动子进程,此项是必须的。
  • :restart - 一个原子值,用来定义已终止的进程是否应该重启。它是可选的,默认是 :permanent
  • :shutdown - 一个整数或原子,用来定义子进程应该如何终止。它也是可选的, :worker 类型的默认值是 5_000:supervisor 类型的默认值是 :infinity
  • :type - 指定子进程是 :worker 还是 :supervisor 。它是可选的,默认为 :worker
  • :modules - 一个用于代码热更新机制的模块列表,用来确定哪些进程在使用指定的模块。它通常设置为 GenServerSupervisor 等的回调模块。它会根据 :start 的值自动设置,并且在实践中也极少修改。
  • :significant - 一个布尔值,表示监督者是否应用随着该子进程的退出而退出,也就是表示该子进程是否是”重要“的。只有 :transient:temporary 子进程可以设置为”重要“。该配置是也是可选的,默认是 false

接下来让我们看看 :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 函数

当启动一个监督者时,我们会传递一个子进程描述列表。这些描述是 map,它告诉监督者应该如何启动、停止和重启每个子进程:

%{
  id: Counter,
  start: {Counter, :start_link, [0]}
}

上面的 map 定义了一个子进程,其 :idCounter ,通过调用 Counter.start_link(0) 启动。

然而,使用 map 为每个子进程定义子进程描述可能会很容易出错,因为我们可能会更改 Counter 的实现却忘记更新其子进程描述。因此 Elixir 允许你传递一个包含模块名和 start_link 的参数的元组:

children = [
  {Counter, 0}
]

然后监督者会调用 Counter.child_spec(0) 来获取子进程描述。现在 Counter 模块会自己负责构建自己的子进程描述,例如,我们可以编写以下函数:

def child_spec(arg) do
  %{
    id: Counter,
    start: {Counter, :start_link, [arg]}
  }
end

然后监督者会调用 Counter.start_link(arg) 来启动子进程。这个流程如下表所示。调用者是派生监督者进程的进程。然后监督者继续调用你的代码(模块)来生成其子进程:

sequenceDiagram
    participant C as Caller (Process)
    participant S as Supervisor (Process)
    participant M as Module (Code)

    note right of C: child is a {module, arg} specification
    C->>+S: Supervisor.start_link([child])
    S-->>+M: module.child_spec(arg)
    M-->>-S: %{id: term, start: {module, :start_link, [arg]}}
    S-->>+M: module.start_link(arg)
    M->>M: Spawns child process (child_pid)
    M-->>-S: {:ok, child_pid} | :ignore | {:error, reason}
    S->>-C: {:ok, supervisor_pid} | {:error, reason}

幸运的是, use GenServer 已经定义了一个 Counter.child_spec/1 ,所以你不需要自己编写上面的函数。如果你想自定义自动生成的 child_spec/1 函数,可以直接向 use GenServer 传递选项:

use GenServer, restart: :transient

最后,其实你也可以简单地只传递 Counter 模块作为子进程描述:

children = [
  Counter
]

当只给出模块名时,它等同于 {Counter, []} ,当然这在我们的例子中是不行的,因此我们总是显示传递计数器初始值。

使用 {Counter, 0} ,我们将子进程描述封装在了 Counter 模块中。现在我们可以与其他开发人员分享我们的 Counter 实现,他们可以直接将其添加到他们的监督树中,而不必担心计数器的底层细节。

总的来说,子进程描述可以是以下形式之一:

  • 一个代表子进程描述的 map
  • 一个包含模块和启动参数的二元组 - 例如 {Counter, 0} 。此时, Count.child_spec(0) 会被调用以获取子进程描述。
  • 一个模块 - 如 Counter 。此时, Counter.child_spec([]) 会被调用,虽然在我们的例子中行不通,但是在其他情况下还是很有用的,特别是当你想向子进程传递一些选项时。

如果需要将 {module, arg} 元组或模块转换为子进程描述,或修改子进程描述,可以使用 Supervisor.child_spec/2 函数。例如,用不同的 :id 运行计数器,并设置 :shutdown 值为10秒(10_000毫秒):

children = [
  Supervisor.child_spec({Counter, 0}, id: MyCounter, shutdown: 10_000)
]

监督者策略和选项

到目前为止,我们已经启动了一个监督者,并传递给它一个子进程元组,还有一个称为 :one_for_one 的策略:

children = [
  {Counter, 0}
]

Supervisor.start_link(children, strategy: :one_for_one)

start_link/2 的第一个参数是如上所述的子进程描述列表。

第二个参数是一个关键字列表:

  • :strategy - 监督策略选项。它可以是 :one_for_one:rest_for_one:one_for_all 。必填项。
  • max_restarts - 在规定时间内的最大重启次数,默认为3。
  • max_seconds - 指定上面的”规定时间“。默认为5。
  • :auto_shutdown - 自动停止(指监督者)选项。可以是 :never:any_significantall_significant 。可选项。
  • :name - 注册监督者进程的名字。可选项。

策略

监督者支持不同的监督策略(:strategy 选项):

  • :one_for_one - 如果一个子进程停止,只有它会被重启。
  • :one_for_all - 如果一个进程终止,所有其他子进程否会被终止,然后所有子进程全部被重启。
  • :rest_for_one - 如果一个子进程终止,在它后面启动的进程会被终止,然后一起被重启。

以上说的进程终止指的是异常终止,是否重启由 :restart 选项决定。

要高效地监督动态启动的子进程,参见 DynamicSupervisor

自动停止

监督者可以在被标记为 :significant 的子进程退出时自动关闭自己。

监督者支持不同的自动关闭选项(:auto_shutdown 选项):

  • :never - 默认值,禁用自动关闭。
  • :any_significant - 任意”重要“进程退出,监督者都会关闭其子进程和自己。
  • :all_significant - 当所有”重要“进程退出时,监督者才会关闭其子进程和自己。

只有 :transient:temporary 子进程可以被标记为”重要”,配置才会生效。标记为”重要“的 :transient 子进程必须正常退出,自动关闭才会触发,而 :temporary 子进程则可以以任何原因退出。

名称注册

监督者受到与 GenServer 相同的名称注册规则的约束。

基于模块的监督者

在前面的例子中,监督者都是通过 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 = [
      {Counter, 0}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
end

这两种方法的区别在于,基于模块的监督者让你可以更直接地控制监督者的初始化。不同于调用 Supervisor.start_link/2 的隐式初始化,我们必须在 init/1 回调中调用 Supervisor.init/2 来显式初始化子进程。 Supervisor.init/2 接受与 start_link/2 相同的 :strategy:max_restarts:max_seconds 选项。

当你使用 use Supervisor 时, Supervisor 模块会设置 @behaviour Supervisor 并定义一个 child_spec/1 函数,因此你的模块也可以用作监督树的子进程。

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 键指定的函数启动,默认是 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} 原因退出,则不会被重启。

这些退出原因也影响日志记录。默认情况下,像 GenServers 这样的行为在退出原因是 :normal:shutdown{:shutdown, term} 时不会发送错误日志。

那么,人们可能会问:我应该选择哪种退出原因呢?有三种选项:

  • :normal - 这种情况下,退出不会被记录, :restart:transient 模式的话不会被重启,链接的进程也不会退出。
  • :shutdown{:shutdown, term} - 这种情况退出也不会被记录, :restart:transient 模式也不会被重启,链接的进程会以相同的原因退出,除非它们捕获了退出信号。
  • 其他任意值 - 在这种情况下,退出会被记录, :restart:transient 模式会被重启,链接的进程会以相同的原因退出,除非它们捕获了退出信号。

一般来说,如果你是因为预期的原因退出,你可以使用 :shutdown{:shutdown, term}

注意,达到最大重启次数的监督者将以 :shutdown 原因退出。在这种情况下,监督者只有在它的子进程描述的 :restart 选项被设置为 :permnent (默认值)时才会被重启。


http://www.kler.cn/a/447309.html

相关文章:

  • 【Java】递归算法
  • Java图片拼接
  • 基于Spring Boot的智慧农业专家远程指导系统
  • 多音轨视频使用FFmpeg删除不要音轨方法
  • Unity3D仿星露谷物语开发7之事件创建动画
  • 阿里云百炼大模型生成贪吃蛇小游戏
  • 青少年编程与数学 02-004 Go语言Web编程 12课题、本地数据存储
  • 智能电动汽车游智能化与电动化
  • IIC I2C子协议 SMBus协议 通信协议原理 时序 SMBus深度剖析
  • 智慧养老系统源码医院陪诊代办买药就医陪护上门护理小程序
  • 【蓝桥杯每日一题】扫描游戏——线段树
  • 以客户成功为核心,镜舟科技驱动数据库开源商业化创新
  • 【Spring】第二站:基于 <注解> 的方式实现Bean管理和注入管理
  • 算法刷题Day23:BM60 括号生成
  • CSS学习记录18
  • 什么是事务?隔离级别
  • 嵌入式单片机中外设的基本控制与实现
  • 游戏开发技能系统常用概念
  • Kafka 迁移 AutoMQ 时 Flink 位点管理的挑战与解决方案
  • 基于Spring Boot的智慧农业专家远程指导系统
  • 如何使用 Python 连接 MySQL 数据库?
  • Websocket客户端始终连接不上Server排查之Openai Realtime api
  • 基于 STM32 的多路火灾报警系统设计
  • LeetCode hot100-91
  • 高性能MySQL-查询性能优化
  • 标准库与HAL库的区别